Add shadow above SwiftUI's TabView

Viewed 1565

I'm trying to implement TabView in SwiftUI that has the same color as screen background but also has a shadow above it like in this picture: enter image description here

So far I've been able to properly display color, but I don't know how to add the shadow. This is my code:

struct ContentView: View {
    
    init() {
        let appearance = UITabBarAppearance()
        appearance.configureWithTransparentBackground()

        UITabBar.appearance().standardAppearance = appearance
    }
    
    var body: some View {
        TabView {
            Text("First View")
                .tabItem {
                    Image(systemName: "square.and.arrow.down")
                    Text("First")
                }
            Text("Second View")
                .tabItem {
                    Image(systemName: "square.and.arrow.up")
                    Text("Second")
                }
        }
    
    }
}

Does anyone know how to do this? I would appreciate your help :)

2 Answers

Short answer

I found a solution. You can create your own shadow image and add it to the UITabBar appearance like this:

// load your custom shadow image
let shadowImage: UIImage = ...

//you also need to set backgroundImage, without it shadowImage is ignored
UITabBar.appearance().backgroundImage = UIImage()
UITabBar.appearance().shadowImage = shadowImage

More detailed answer

Setting backgroundImage

Note that by setting

UITabBar.appearance().backgroundImage = UIImage()

you make your TabView transparent, so it is not ideal if you have content that can scroll below it. To overcome this, you can set TabView's color.

let appearance = UITabBarAppearance()
appearance.configureWithTransparentBackground()
appearance.backgroundColor = UIColor.systemGray6
UITabBar.appearance().standardAppearance = appearance

Setting shadowImage

I wanted to generate shadow image programatically. For that I've created an extension of UIImage. (code taken from here)

extension UIImage {
    static func gradientImageWithBounds(bounds: CGRect, colors: [CGColor]) -> UIImage {
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = bounds
        gradientLayer.colors = colors
        
        UIGraphicsBeginImageContext(gradientLayer.bounds.size)
        gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image!
    }
}

And finally I styled my TabView like this:

let image = UIImage.gradientImageWithBounds(
    bounds: CGRect( x: 0, y: 0, width: UIScreen.main.scale, height: 8),
    colors: [ 
        UIColor.clear.cgColor, 
        UIColor.black.withAlphaComponent(0.1).cgColor
    ]
)

let appearance = UITabBarAppearance()
appearance.configureWithTransparentBackground()
appearance.backgroundColor = UIColor.systemGray6
        
appearance.backgroundImage = UIImage()
appearance.shadowImage = image

UITabBar.appearance().standardAppearance = appearance

Result

enter image description here

Your best bet in order to achieve pretty much exactly what you wish is to create a custom TabView.

In fact, in SwiftUI you could use UITabBarAppearance().shadowColor, but that won't do much apart from drawing a 2px line on top of the TabView itself.

Instead, with the below code, you could create a custom TabView and achieve the desired graphical effect.

import SwiftUI


enum Tab {
    case borrow,ret,device,you
}

struct TabView: View {
    @Binding var tabIdx: Tab
    
    var body: some View {
        HStack {
            Group {
                Spacer()
                
                Button (action: {
                    self.tabIdx = .borrow
                }) {
                    VStack{
                        Image(systemName: "arrow.down.circle")
                        Text("Borrow")
                            .font(.system(size: 10))
                        
                    }
                }
                .foregroundColor(self.tabIdx == .borrow ? .purple : .secondary)
                
                Spacer()
                
                Button (action: {
                    self.tabIdx = .ret
                }) {
                    VStack{
                        Image(systemName: "arrow.up.circle")
                        Text("Return")
                            .font(.system(size: 10))
                        
                    }
                }
                .foregroundColor(self.tabIdx == .ret ? .purple : .secondary)
                
                Spacer()
                
                Button (action: {
                    self.tabIdx = .device
                }) {
                    VStack{
                        Image(systemName: "safari")
                        Text("Device")
                            .font(.system(size: 10))
                        
                    }
                }
                .foregroundColor(self.tabIdx == .device ? .purple : .secondary)
                
                Spacer()
                
                Button (action: {
                    self.tabIdx = .you
                }) {
                    VStack{
                        Image(systemName: "person.circle")
                        Text("You")
                            .font(.system(size: 10))
                        
                    }
                }
                .foregroundColor(self.tabIdx == .you ? .purple : .secondary)
                
                Spacer()
            }
        }
        .padding(.bottom, 30)
        .padding(.top, 10)
        .background(Color(red: 0.95, green: 0.95, blue: 0.95))
        .font(.system(size: 30))
        .frame(height: 80)
    }
}


struct ContentView: View {
    @State var tabIdx: Tab = .borrow
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                Spacer()
                
                if tabIdx == .borrow {
                    Text("Borrow")
                } else if tabIdx == .ret {
                    Text("Return")
                } else if tabIdx == .device {
                    Text("Device")
                } else if tabIdx == .you {
                    Text("You")
                }
                Spacer(minLength: 0)
                TabView(tabIdx: self.$tabIdx)
                    .shadow(radius: 10)
            }
            .ignoresSafeArea()
            
        }
    }
    
}

Remember that when you do this, all your tabs are specified as cases within enum Tab {}, and the TabView() contains some Button elements, which will change the tab by using the @State var tabIdx. So you can adjust your actions by modifying the self.tabIdx = <yourtab> statement.

The active color is set with this statement after each button:

.foregroundColor(self.tabIdx == .borrow ? .purple : .secondary)

Here you can just change .purple to whatever suits you.

You can see that on the ContentView, the if-else if block catches the SubView. Here I have placed some Text() elements like Text("Borrow"), so you can replace them by calling something like BorrowView() or whatever you have.

I have added the shadow when I call the TabView from within the ContentView, but you could even add the .shadow(radius: 10) after the HStack of the TabView itself.

This would be the final output:

                 enter image description here

Related