Add Shadow Above Swiftui's Tabview

Add shadow above SwiftUI's TabView

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

Sample Image

Add top shadow to TabView in SwiftUI

The shadowColor is applied to default tab shadow which is currently absent, so we need a custom template image (eg. with gradient transparency) so shadowColor would tint it.

Here is working approach (Xcode 13.3 / iOS 15.4)

demo

Main part:

appearance.shadowColor = .white
appearance.shadowImage = UIImage(named: "tab-shadow")?.withRenderingMode(.alwaysTemplate)

Complete findings and code

How to add shadow for PageTabViewStyle on SwiftUI?

You can add a background behind the dots using indexViewStyle:

.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))

SwiftUI Add top line to TabBar

I forgot that I have added this code for making the TabBar completely white.
But my shadow color was white, so I just changed it to Gray

extension UITabBarController {
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let appearance = UITabBarAppearance()
appearance.configureWithOpaqueBackground()

appearance.backgroundColor = .white
appearance.shadowImage = UIImage()
appearance.shadowColor = .gray

appearance.stackedLayoutAppearance.normal.iconColor = .black
appearance.stackedLayoutAppearance.normal.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.black]

self.tabBar.standardAppearance = appearance
}

}

How to add a bottom curve in TabView in SwiftUI?

Ok, actually we need to solve two problems here, first - find a height of tab bar, and second - correctly align view custom view with represented selected item over standard tab bar. Everything else is mechanics.

Here is simplified demo. Tested with Xcode 14 / iOS 16.

demo

Main part:

  • a possible solution for problem #1
struct TabContent<V: View>: View {
@Binding var height: CGFloat
@ViewBuilder var content: () -> V

var body: some View {
GeometryReader { gp in // << read bottom edge !!
content()
.onAppear {
height = gp.safeAreaInsets.bottom
}
.onChange(of: gp.size) { _ in
height = gp.safeAreaInsets.bottom
}
}
}
}
  • a possible solution for problem #2
    // Just put customisation in z-ordered over TabView
ZStack(alignment: .bottom) {
TabView(selection: $selection) {
// .. content here
}

TabSelection(height: tbHeight, item: selected)
}

struct TabSelection: View {
let height: CGFloat
let item: Item

var body: some View {
VStack {
Spacer()
Curve() // put curve over tab bar !!
.frame(maxWidth: .infinity, maxHeight: height)
.foregroundColor(item.color)
}
.ignoresSafeArea() // << push to bottom !!
.overlay(
// Draw overlay
Circle().foregroundColor(.black)
.frame(height: height).aspectRatio(contentMode: .fit)
.shadow(radius: 4)
.overlay(Image(systemName: item.icon)
.font(.title)
.foregroundColor(.white))
, alignment: .bottom)
}
}

Test module is here

Making TabView not translucent on SwiftUI produces a new view on top

This is the correct way to do it.

It works with SwiftUI too as the TabView and NavigationView are actually UIHostedController for the legacy UITabBarController and UINavigationController.

Edit: Just watched Modernizing Your UI for iOS 13
This is the way to do it :

let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
appearance.largeTitleTextAttributes = [.foregroundColor: UIColor .white]

Then set the appearance on the various type of appearance.

navigationBar.standardAppearance = appearance
navigationBar.compactAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance

Reference: https://developer.apple.com/videos/play/wwdc2019/224/

2nd Edit: Need a figure out a clean way to get to the UINavigationController from a SwiftUI view.

In the meantime, this will help:

extension UINavigationController {
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
navigationBar.standardAppearance = appearance
navigationBar.compactAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
}
}

extension UITabBarController {
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let appearance = UITabBarAppearance()
appearance.configureWithOpaqueBackground()
tabBar.standardAppearance = appearance
}
}

.tag() in TabView in SwiftUI challenge

The issue you are having is that you have stringly typed everything. So, you simply a lack of having access to the image that is associated with each view in your tab bar. Your ForEach iterates through your allViews which only has the name of the view, not the image. So, when you need to set the tab, you do not have the image name available. I changed your strings to an enum which gives you a limited set of choices and more control over what you are trying to accomplish. A great deal of tutorials use strongly typing when they are trying to demonstrate something, but it is a bad habit that can create difficult to find bugs.

struct ContentView: View {
var body: some View {

CustomTabView()

}
}

struct CustomTabView: View {

@State var selectedTab = ViewSelect.House // This is an enum defined below.
@State var viewData : AllViewData!

var body: some View {

ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {

TabView(selection: $selectedTab) {
ForEach(ViewSelect.allCases, id: \.self) { view in // This iterates through all of the enum cases.
ViewsCardView(viewData: view.allViewData)
.tag(view.rawValue) // by having the tag be the enum's raw value,
// you can always compare enum to enum.
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.ignoresSafeArea(.all, edges: .bottom)

ScrollView(.horizontal, showsIndicators: false, content: {

HStack {

ForEach(ViewSelect.allCases ,id: \.self){viewSelect in

TabButton(viewSelect: viewSelect, selectedTab: $selectedTab)

}
}

})
.padding(.horizontal, 25)
.padding(.vertical, 5)
.background(Color.white)
.clipShape(Capsule())
.shadow(color: Color.black.opacity(0.15), radius: 5, x: 5, y: 5)
.shadow(color: Color.black.opacity(0.15), radius: 5, x: -5, y: -5)
.padding(.horizontal)
}
.background(Color.black.opacity(0.05).ignoresSafeArea(.all, edges: .all))
}
}

struct TabButton: View {
let viewSelect: ViewSelect
@Binding var selectedTab: ViewSelect

var body: some View {

Button(action: {selectedTab = viewSelect}) {

Image(systemName: viewSelect.tabIcon)
.renderingMode(.template)
// this compares the selection to the button's associated enum.
// The enum provides the image name to the button,
// but we are always dealing with a case of the enum.
.opacity(selectedTab == viewSelect ? (1) : (0.5))
.padding()
}
}
}

struct ViewsCardView: View {

@State var viewData: AllViewData

var body: some View {

VStack {

Text(viewData.name)

}
}
}

struct AllViewData : Identifiable, Hashable {

var id = UUID().uuidString
var name : String

}

public enum ViewSelect: String, CaseIterable {
case House, Envelope, Folder, Settings, Slider, Video, Up

var tabIcon: String {
switch self {
case .House:
return "house"
case .Envelope:
return "envelope"
case .Folder:
return "folder"
case .Settings:
return "gear"
case .Slider:
return "slider.horizontal.3"
case .Video:
return "video"
case .Up:
return "arrow.up.doc"
}
}

var allViewData: AllViewData {
return AllViewData(name: self.rawValue)
}
}


Related Topics



Leave a reply



Submit