8

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 :)

Ella Gogo
  • 1,051
  • 1
  • 11
  • 17

2 Answers2

12

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

Ella Gogo
  • 1,051
  • 1
  • 11
  • 17
  • 2
    +1 and very well done for having come up with this SwiftUI solution. Indeed, your way enables you to achieve what you want without creating your own TabView. I personally find that at some point I almost always need to do something for which I need my own TabView (i.e. : hiding it, as of to date, you can't hide a sub-viewed TabView in SwiftUI like you'd in UIKit with `UITabBar.appearance().isHidden = true`), and eventually I find it less of a pain to just have my own as it's flexible, and results in less code written eventually, but it's a matter of preference. – BiOS Mar 29 '21 at 09:23
  • not working in iOS 15 :|, works fine in lesser – Abhishek Thapliyal Oct 31 '21 at 04:47
  • 1
    Hi @AbhishekThapliyal I can confirm that the solution does not work for me on iOS 15. The shadow is displaying correctly but the tabbar is transparent. I will update the answer in the following weeks. – Ella Gogo Nov 01 '21 at 09:34
  • @EllaGogo: My bad it is working, if not you can try if #available(iOS 15.0, *) { UITabBar.appearance().scrollEdgeAppearance = appearance } else { // Fallback on earlier versions } – Abhishek Thapliyal Nov 01 '21 at 13:38
  • What's the new solution for iOS 15+? – Van Du Tran Mar 26 '22 at 13:16
2

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

BiOS
  • 2,282
  • 3
  • 10
  • 24