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
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)
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.
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
The Correct Way to Override an Initializer in Swift 1.1
How to Set an Ordered Relationship with Nspersistentcloudkitcontainer
Skaudionode() Crashes When Plugging In/Out Headphones
Using Uiactivityindicatorview with Uiwebview in Swift
Making Nsdecimalnumber Codable
Why Avplayer Downloading First Instead Live Streaming
Closure Containing a Declaration Cannot Be Used with Function Builder 'Viewbuilder'
How to Pass Structure by Reference
Swift Await/Async - How to Wait Synchronously for an Async Task to Complete
Mock Third Party Classes (Firebase) in Swift
Sorting Nil Dates to the End of an Array
"An Error Occurred While Accessing the Keychain" When Signing in Using Firebase
What's a Good Way to Iterate Backwards Through the Characters of a String
Swift: How to Expand a Tilde in a Path String
iOS 13 Strange Search Controller Gap