Add shadow above SwiftUI's TabView - swift

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

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

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:
                 

Related

How can I make a smoothed custom Picker on SwiftUI?

I would like to replicate this picker in swiftUI. In particular, I have a button on the bottom left of the screen and when I click it I would like to show different icons (similar to the image below, but vertically). As soon as I click on one of the choices the button should shrink back to the initial form (circle) with the chosen icon.
When closed:
When open:
I am new to this language and to app in general, I tried with a Pop Up menu, but it is not the desired result, for now I have an horizontal segmented Picker.
You can't do this with the built-in Picker, because it doesn't offer a style like that and PickerStyle doesn't let you create custom styles (as of the 2022 releases).
You can create your own implementation out of other SwiftUI views instead. Here's what my brief attempt looks like:
Here's the code:
enum SoundOption {
case none
case alertsOnly
case all
}
struct SoundOptionPicker: View {
#Binding var option: SoundOption
#State private var isExpanded = false
var body: some View {
HStack(spacing: 0) {
button(for: .none, label: "volume.slash")
.foregroundColor(.red)
button(for: .alertsOnly, label: "speaker.badge.exclamationmark")
.foregroundColor(.white)
button(for: .all, label: "volume.2")
.foregroundColor(.white)
}
.buttonStyle(.plain)
.background {
Capsule(style: .continuous).foregroundColor(.black)
}
}
#ViewBuilder
private func button(for option: SoundOption, label: String) -> some View {
Button {
withAnimation(.easeOut) {
if isExpanded {
self.option = option
isExpanded = false
} else {
isExpanded = true
}
}
} label: {
Image(systemName: label)
.fontWeight(.bold)
.padding(10)
}
.frame(width: shouldShow(option) ? buttonSize : 0, height: buttonSize)
.opacity(shouldShow(option) ? 1 : 0)
.clipped()
}
private var buttonSize: CGFloat { 44 }
private func shouldShow(_ option: SoundOption) -> Bool {
return isExpanded || option == self.option
}
}
struct ContentView: View {
#State var option = SoundOption.none
var body: some View {
ZStack {
Color(hue: 0.6, saturation: 1, brightness: 0.2)
SoundOptionPicker(option: $option)
.shadow(color: .gray, radius: 3)
.frame(width: 200, alignment: .trailing)
}
}
}

How to make button for Dark Mode

I want to make possibility to switching between dark and light mode on my main screen. I don't want to make it with UISlider, but with just a button. You tap once and its dark mode, you tap again and its light. Is it possible to do?
UIKit
UPDATE
The updated version is more reliable, it checks the overrideUserInterfaceStyle and if it is .unspecified there is a fallback to UIScreen.main.traitCollection.userInterfaceStyle
//
// ViewController.swift
// ToggleInterfaceStyle
//
// Created by Sebastian on 23.09.22.
//
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Here is the button
let toggleInterfaceStyleButton = UIButton()
toggleInterfaceStyleButton.setTitle("Toggle Color Scheme", for: .normal)
toggleInterfaceStyleButton.setTitleColor(.tintColor, for: .normal)
toggleInterfaceStyleButton.frame = CGRect(x: 15, y: -50, width: 250, height: 500)
toggleInterfaceStyleButton.addTarget(self, action: #selector(toggleInterfaceStyle), for: .touchUpInside)
self.view.addSubview(toggleInterfaceStyleButton)
}
#objc func toggleInterfaceStyle() {
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
let interfaceStyle = window?.overrideUserInterfaceStyle == .unspecified ? UIScreen.main.traitCollection.userInterfaceStyle : window?.overrideUserInterfaceStyle
if interfaceStyle != .dark {
window?.overrideUserInterfaceStyle = .dark
} else {
window?.overrideUserInterfaceStyle = .light
}
}
}
SwiftUI
For SwiftUI, you can change the appearance (light mode / dark mode) with .preferredColorScheme, it changes the appearance for the entire app. In this example I added a second view and a navigationLink to show that the styles stays as selected:
import SwiftUI
struct ContentView: View {
#State private var scheme: ColorScheme = .light
#State private var isShowingNewAccountView = false
func toggleScheme() {
if scheme == .light {
scheme = .dark
} else {
scheme = .light
}
}
var body: some View {
NavigationView() {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
.padding()
Text("Hello, world!")
.padding()
NavigationLink(destination: SecondView()) {
Text("Second View")
}
Button(action: {
self.toggleScheme()
}) {
Text("Toggle Scheme")
}
.padding()
}
}
.preferredColorScheme(scheme)
}
}
struct SecondView: View {
var body: some View {
VStack {
Image(systemName: "airplane")
.imageScale(.large)
.foregroundColor(.accentColor)
.padding()
Text("Hello, second view!")
.padding()
}
}
}

How to to add Blur overlay on top of all views using .Blur as ViewModifier in SwiftUI

I have created a viewModifier which blurs a view when added.
Problem is when I add it on parent view of all the contents, all views are blurred differently.
I assume it's because it goes to all the contents and blur each of them individually instead of adding an overlay blur to the parent view.
I used Swift UI's native blur because UIBlurEffect has very limited configuration i.e. I can't adjust the blur intensity to a specific value.
Here it is when the modifier is added in the parent ZStack:
Here when I add it on background image only:
Adding it to background image looks good but I need it to be on top of all views. Here is my code:
Main View
import SwiftUI
struct BlurViewDemo: View {
#State private var isPressed = true
#State private var blurColor: BlurColor = .none
var body: some View {
ZStack {
GeometryReader { proxy in
let frame = proxy.frame(in: .global)
Image("blur-view-demo-image")
.resizable()
.frame(width: frame.size.width, height: frame.size.height)
// When added here only the background image is blurred.
.modifier(
BlurModifier(
showBlur: $isPressed, blurColor: $blurColor
)
)
}
VStack(spacing: 30) {
// TITLE STYLE
Text(getSelectedBlurTitle())
.font(.system(size: 60))
.offset(y: -40)
.foregroundColor(
blurColor == .dark && isPressed ? .white : .black
)
// TOGGLE BLUR BUTTON
Button(action: {
isPressed.toggle()
}, label: {
Text(isPressed ? "Blur: On" : "Blur: Off")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color(#colorLiteral(red: 0.2, green: 0.4, blue: 0.4, alpha: 1)))
// DEFAULT BUTTON
Button(action: {
isPressed = true
blurColor = .none
}, label: {
Text("Default")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color.gray)
// LIGHT BUTTON
Button(action: {
isPressed = true
blurColor = .light
}, label: {
Text("Light")
.foregroundColor(.black)
})
.frame(width: 80, height: 40)
.background(Color.white)
// DARK BUTTON
Button(action: {
isPressed = true
blurColor = .dark
}, label: {
Text("Dark")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color.black)
} //: VSTACK
} //: ZSTACK
.edgesIgnoringSafeArea(.all)
// When added here, the buttons are not blurred properly.
.modifier(
BlurModifier(
showBlur: $isPressed, blurColor: $blurColor
)
)
}
private func getSelectedBlurTitle() -> String {
guard isPressed else { return "Clear"}
switch blurColor {
case .none:
return "Default"
case .light:
return "Light"
case .dark:
return "Dark"
}
}
}
struct BlurViewDemo_Previews: PreviewProvider {
static var previews: some View {
BlurViewDemo()
}
}
View Modifier
public enum BlurColor {
case none
case light
case dark
}
import SwiftUI
struct BlurModifier: ViewModifier {
#Binding private var showBlur: Bool
#Binding private var blurColor: BlurColor
#State private var blurRadius: CGFloat = 14
public init(showBlur: Binding<Bool>, blurColor: Binding<BlurColor>) {
self._showBlur = showBlur
self._blurColor = blurColor
}
func body(content: Content) -> some View {
ZStack {
content
.blur(radius: showBlur ? blurRadius : 0, opaque: true)
.animation(Animation.easeInOut(duration: 0.3))
.edgesIgnoringSafeArea(.all)
.navigationBarHidden(showBlur ? true : false)
Rectangle()
.fill(showBlur ? getBlurColor() : Color.white.opacity(0.0001))
.edgesIgnoringSafeArea(.all)
}
}
private func getBlurColor() -> Color {
switch blurColor {
case .none:
return Color.white.opacity(0.5)
case .light:
return Color.white.opacity(0.6)
case .dark:
return Color.black.opacity(0.5)
}
}
}

Dynamically change and update color of Navigation Bar in SwiftUI

I'm trying to change the NavigationBar background color in SwiftUI which I have done with the following code. However, I have yet to figure out how I can dynamically change and update the background color of the navigation bar.
struct ContentView: View {
#State var color: Color = .red
init() {
let navbarAppearance = UINavigationBarAppearance()
navbarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
navbarAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
navbarAppearance.backgroundColor = UIColor(color)
UINavigationBar.appearance().standardAppearance = navbarAppearance
UINavigationBar.appearance().compactAppearance = navbarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navbarAppearance
}
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button(action: { color = .blue }) {
Text("Blue")
.font(.title)
.bold()
.foregroundColor(.white)
.frame(width: 100)
.padding()
.background(Color.blue)
.cornerRadius(15)
}
Button(action: { color = .red }) {
Text("Red")
.font(.title)
.bold()
.foregroundColor(.white)
.frame(width: 100)
.padding()
.background(Color.red)
.cornerRadius(15)
}
}
.offset(y: -50)
.navigationTitle("My Navigation")
}
}
}
This code gives me the correct result, but tapping one of the buttons to change the Color variable does not update the color of the NavigationBar. This is because on initialization the nav bar maintains all of its characteristics, so I need to find a way to change these after, and if possible animate this transition between changing colors. Thanks for any help!
You can use SwiftUI-Introspect, so you are only changing the navigation bar of this NavigationView. It will not affect any other instance.
You also won't be using .id(...), which is potentially bad because when a view changes identity it can break animations, unnecessarily reinitialize views, break view lifecycles, etc.
In my example, you save the current instance of the UINavigationController. When the value of color changes, the appearance of the navigation bar is set again.
Example with Introspect:
import Introspect
/* ... */
struct ContentView: View {
#State private var color: Color = .red
#State private var nav: UINavigationController?
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button(action: { color = .blue }) {
Text("Blue")
.font(.title)
.bold()
.foregroundColor(.white)
.frame(width: 100)
.padding()
.background(Color.blue)
.cornerRadius(15)
}
Button(action: { color = .red }) {
Text("Red")
.font(.title)
.bold()
.foregroundColor(.white)
.frame(width: 100)
.padding()
.background(Color.red)
.cornerRadius(15)
}
}
.offset(y: -50)
.navigationTitle("My Navigation")
}
.introspectNavigationController { nav in
self.nav = nav
updateNavBar()
}
.onChange(of: color) { _ in
updateNavBar()
}
}
private func updateNavBar() {
guard let nav = nav else { return }
let navbarAppearance = UINavigationBarAppearance()
navbarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
navbarAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
navbarAppearance.backgroundColor = UIColor(color)
nav.navigationBar.standardAppearance = navbarAppearance
nav.navigationBar.compactAppearance = navbarAppearance
nav.navigationBar.scrollEdgeAppearance = navbarAppearance
}
}
Result:
Yes, appearance is applied to all views created after appearance itself. So we need to reset appearance and then create NavigationView again on color changes.
Here is a demo of possible approach (tested with Xcode 13 / iOS 15)
struct ContentView: View {
#State var color: Color = .red
var body: some View {
MainNavView(barColor: $color)
.id(color) // << re-creates !!
}
}
struct MainNavView: View {
#Binding var color: Color
init(barColor: Binding<Color>) {
self._color = barColor
let navbarAppearance = UINavigationBarAppearance()
navbarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
navbarAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
navbarAppearance.backgroundColor = UIColor(color)
UINavigationBar.appearance().standardAppearance = navbarAppearance
UINavigationBar.appearance().compactAppearance = navbarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navbarAppearance
}
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button(action: { color = .blue }) {
Text("Blue")
.font(.title)
.bold()
.foregroundColor(.white)
.frame(width: 100)
.padding()
.background(Color.blue)
.cornerRadius(15)
}
Button(action: { color = .red }) {
Text("Red")
.font(.title)
.bold()
.foregroundColor(.white)
.frame(width: 100)
.padding()
.background(Color.red)
.cornerRadius(15)
}
}
.offset(y: -50)
.navigationTitle("My Navigation")
}
}
}

SwiftUI Segmented Picker does not switch when click on it

I'm trying to implement Segmented Control with SwiftUI. For some reason Segmented Picker does not switch between values when click on it. I went through many tutorials but cannot find any difference from my code:
struct MeetingLogin: View {
#State private var selectorIndex: Int = 0
init() {
UISegmentedControl.appearance().backgroundColor = UIColor(named: "JobsLightGreen")
UISegmentedControl.appearance().selectedSegmentTintColor = UIColor(named: "AlmostBlack")
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white,
.font: UIFont(name: "OpenSans-Bold", size: 13)!],
for: .selected)
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white,
.font: UIFont(name: "OpenSans-Regular", size: 13)!],
for: .normal)
}
var body: some View {
VStack {
...
Group {
Spacer().frame(minHeight: 8, maxHeight: 30)
Picker("video", selection: $selectorIndex) {
Text("Video On").tag(0)
Text("Video Off").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.padding([.leading, .trailing], 16)
Spacer().frame(minHeight: 8, maxHeight: 50)
Button(action: { self.sendPressed() }) {
ZStack {
RoundedRectangle(cornerRadius: 100)
Text("go")
.font(.custom("Roboto-Bold", size: 36))
.foregroundColor(Color("MeetingGreen"))
}
}
.foregroundColor(Color("MeetingLightGreen").opacity(0.45))
.frame(width: 137, height: 73)
}
Spacer()
}
}
}
Would appreciate any suggestions!
Update: The issue seem to have occured due to overlapping of the View's because the cornerRadius value of RoundedRectangle was set to 100. Bringing the value of cornerRadius to 55 would restore the Picker's functionality.
The issue seems to be caused because of the RoundedRectangle(cornerRadius: 100) within the ZStack. I don't have and explanation as to why this is happening. I'll add the reason if I ever find out. Could be a SwiftUI bug. I can't tell untill I find any related evidence. So here is the code that could make the SegmentedControl work without any issue.
struct MeetingLogin: View {
//...
#State private var selectorIndex: Int = 0
var body: some View {
VStack {
//...
Group {
Spacer().frame(minHeight: 8, maxHeight: 30)
Picker("video", selection: $selectorIndex) {
Text("Video On").tag(0)
Text("Video Off").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.padding([.leading, .trailing], 16)
Spacer().frame(minHeight: 8, maxHeight: 50)
Button(action: { self.sendPressed() }) {
ZStack {
RoundedRectangle(cornerRadius: 55)
Text("go")
.font(.custom("Roboto-Bold", size: 36))
.foregroundColor(Color("MeetingGreen"))
}
}
.foregroundColor(Color("MeetingLightGreen").opacity(0.45))
.frame(width: 137, height: 73)
}
Spacer()
}
}
}
Xcode 12 iOS 14
you can get it by adding a if-else condition on the tapGesture of your Segment Control(Picker)
#State private var profileSegmentIndex = 0
Picker(selection: self.$profileSegmentIndex, label: Text("Jamaica")) {
Text("My Posts").tag(0)
Text("Favorites").tag(1)
}
.onTapGesture {
if self.profileSegmentIndex == 0 {
self.profileSegmentIndex = 1
} else {
self.profileSegmentIndex = 0
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
If you need to use it with more than 2 segments, you can try with an Enum :)
Your code/View is missing the if-condition to know what shall happen when the selectorIndex is 0 or 1.
It has to look like this:
var body: some View {
VStack {
if selectorIndex == 0 {
//....Your VideoOn-View
} else {
//Your VideoOff-View
}
}