Status bar color in iOS 16 - ios

Thanks for taking your time to help others.
This is the original post, I publish here also hoping for better luck. Thanks!
Problem description:
Our SwiftUI app min deployment is iOS 14. We've been struggling with status bar color just in iOS 16.
We can't change its color/appearance from light to dark.
Previous valid solution (iOS 13 to iOS 15)
Since now we used to manage the appearance with ...
1- Call in specific view, when a condition is given, like no dark mode supported, or side menu have been open
UIApplication.setStatusBarStyle(.darkContent)
2- The extension to support this on any SwiftUI View
import UIKit
extension UIApplication {
class func setStatusBarStyle(_ style: UIStatusBarStyle) {
if let controller = UIApplication.getKeyWindow()?.rootViewController as? ContentHostingController {
controller.changeStatusBarStyle(style)
}
}
}
3- Actually, the controller class
import SwiftUI
import UIKit
class ContentHostingController: UIHostingController<AnyView> {
private var currentStatusBarStyle: UIStatusBarStyle = .default
override var preferredStatusBarStyle: UIStatusBarStyle {
currentStatusBarStyle
}
func changeStatusBarStyle(_ style: UIStatusBarStyle) {
self.currentStatusBarStyle = style
self.setNeedsStatusBarAppearanceUpdate()
}
}
But, as I said, this stopped working with iOS 16.
What we have checked?
As we googled it...
Using .preferredColorScheme modifier, changes the app appearance, and we want to keep, for example, the dark theme along light status bar. So it's not an option.
Applying .toolbarColorScheme (iOS 16), does nothing.
Manage Info.plist without success. Light content by default, could
be changed (with the method described above)
Questions
Why is this not working in iOS 16? Are we doing something wrong?
(worked like a charm for 13 to 15).
Is there any generic (iOS 13 to 16) way that you actually use and
work?
(If not) Do you know any workaround to apply on 16 only?
Final solution (by #ashley-mills and refined by me):
First, a View extension to keep it clean:
func iOS16navBarAdapter(_ colorScheme: ColorScheme) -> some View {
if #available(iOS 16, *) {
return self
.toolbarBackground(Color.navigationBar, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(colorScheme, for: .navigationBar)
} else {
return self
}
}
You can use as follows on your view:
#State private var iOS16colorScheme: ColorScheme = .dark
...
VStack {
...
}
.iOS16navBarAdapter(iOS16colorScheme)
.onChange(of: globalSideMenu.xCoord) { value in
iOS16colorScheme = value > sideMenuOffset ? .light : .dark
}

There are a couple of ways to change the status bar colour.
Starting with this View:
struct ContentView: View {
var body: some View {
ZStack {
Color.mint
.ignoresSafeArea()
Text("Hello")
}
}
}
and setting the following info.plist keys:
View controller-based status bar appearance: NO
Status bar style: Dark Content
gives:
Changing
Status bar style: Light Content
gives:
You can also change the status bar text colour on a per view basis, making sure you set
View controller-based status bar appearance: YES
then wrapping your view in a NavigationStack, and adding the following toolbar modifiers:
.toolbarBackground(.mint, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(<colorScheme>, for: .navigationBar)
for example:
struct ContentView: View {
var body: some View {
NavigationStack {
TestView(colorScheme: .dark)
}
}
}
struct TestView: View {
let colorScheme: ColorScheme
var body: some View {
ZStack {
Color.mint
.ignoresSafeArea()
VStack {
Text("Hello")
NavigationLink("Push") {
TestView(colorScheme: .dark)
}
}
}
.toolbarBackground(.mint, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(colorScheme, for: .navigationBar)
}
}
gives:

Related

Set .toolbarColorScheme to white with a transparent background in SwiftUI [duplicate]

Thanks for taking your time to help others.
This is the original post, I publish here also hoping for better luck. Thanks!
Problem description:
Our SwiftUI app min deployment is iOS 14. We've been struggling with status bar color just in iOS 16.
We can't change its color/appearance from light to dark.
Previous valid solution (iOS 13 to iOS 15)
Since now we used to manage the appearance with ...
1- Call in specific view, when a condition is given, like no dark mode supported, or side menu have been open
UIApplication.setStatusBarStyle(.darkContent)
2- The extension to support this on any SwiftUI View
import UIKit
extension UIApplication {
class func setStatusBarStyle(_ style: UIStatusBarStyle) {
if let controller = UIApplication.getKeyWindow()?.rootViewController as? ContentHostingController {
controller.changeStatusBarStyle(style)
}
}
}
3- Actually, the controller class
import SwiftUI
import UIKit
class ContentHostingController: UIHostingController<AnyView> {
private var currentStatusBarStyle: UIStatusBarStyle = .default
override var preferredStatusBarStyle: UIStatusBarStyle {
currentStatusBarStyle
}
func changeStatusBarStyle(_ style: UIStatusBarStyle) {
self.currentStatusBarStyle = style
self.setNeedsStatusBarAppearanceUpdate()
}
}
But, as I said, this stopped working with iOS 16.
What we have checked?
As we googled it...
Using .preferredColorScheme modifier, changes the app appearance, and we want to keep, for example, the dark theme along light status bar. So it's not an option.
Applying .toolbarColorScheme (iOS 16), does nothing.
Manage Info.plist without success. Light content by default, could
be changed (with the method described above)
Questions
Why is this not working in iOS 16? Are we doing something wrong?
(worked like a charm for 13 to 15).
Is there any generic (iOS 13 to 16) way that you actually use and
work?
(If not) Do you know any workaround to apply on 16 only?
Final solution (by #ashley-mills and refined by me):
First, a View extension to keep it clean:
func iOS16navBarAdapter(_ colorScheme: ColorScheme) -> some View {
if #available(iOS 16, *) {
return self
.toolbarBackground(Color.navigationBar, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(colorScheme, for: .navigationBar)
} else {
return self
}
}
You can use as follows on your view:
#State private var iOS16colorScheme: ColorScheme = .dark
...
VStack {
...
}
.iOS16navBarAdapter(iOS16colorScheme)
.onChange(of: globalSideMenu.xCoord) { value in
iOS16colorScheme = value > sideMenuOffset ? .light : .dark
}
There are a couple of ways to change the status bar colour.
Starting with this View:
struct ContentView: View {
var body: some View {
ZStack {
Color.mint
.ignoresSafeArea()
Text("Hello")
}
}
}
and setting the following info.plist keys:
View controller-based status bar appearance: NO
Status bar style: Dark Content
gives:
Changing
Status bar style: Light Content
gives:
You can also change the status bar text colour on a per view basis, making sure you set
View controller-based status bar appearance: YES
then wrapping your view in a NavigationStack, and adding the following toolbar modifiers:
.toolbarBackground(.mint, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(<colorScheme>, for: .navigationBar)
for example:
struct ContentView: View {
var body: some View {
NavigationStack {
TestView(colorScheme: .dark)
}
}
}
struct TestView: View {
let colorScheme: ColorScheme
var body: some View {
ZStack {
Color.mint
.ignoresSafeArea()
VStack {
Text("Hello")
NavigationLink("Push") {
TestView(colorScheme: .dark)
}
}
}
.toolbarBackground(.mint, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(colorScheme, for: .navigationBar)
}
}
gives:

SwiftUI - Dynamically refresh UINavigationBar.appearance().backgroundColor

I am trying to implement a colour theme selector in my app. The last hurdle I am trying to overcome is dynamically changing the colour of my UINavigationBar. At present, my function to change the colour does work, but it only takes effect when the app has been relaunched from a closed state.
My root view controller sets the initial state of UINavigationBar like so:
struct MyRootView: View {
init() {
UINavigationBar.appearance().backgroundColor = getPreferredThemeColour()
UINavigationBar.appearance().largeTitleTextAttributes = [
.foregroundColor: UIColor.white]
}
var body: some View {
...
getPreferredThemeColour() simply returns the desired UIColour from UserDefaults
My theme selector has buttons that change the colour attributes like so:
Button(action: {
UserDefaults.standard.set(UIColor.blue, forKey: "theme")
UINavigationBar.appearance().backgroundColor = UIColor.blue }) {
I can't seem to find any way to refresh UINavigationBar "on the fly" to reflect the changes made. The app always needs to be relaunched.
Any help would be hugely appreciated!!!! Thank you.
Appearance applied to the instances created after appearance itself. So the solution is to recreate NavigationView after appearance changed.
Here is a worked demo of approach. Tested with Xcode 11.4 / iOS 13.4
struct DemoNavigationBarColor: View {
#State private var force = UUID()
init() {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.configureWithOpaqueBackground()
navBarAppearance.backgroundColor = UIColor.systemRed
UINavigationBar.appearance().standardAppearance = navBarAppearance
}
var body: some View {
VStack {
NavigationView(){
List(1 ... 20, id: \.self) { item in
NavigationLink(destination:
Text("Details \(item)")
) {
Text("Row \(item)")
}
}
.navigationBarTitle("Title", displayMode: .inline)
}.id(force) // recreate NavigationView
Button("Be Green") {
UINavigationBar.appearance().standardAppearance.backgroundColor = UIColor.systemGreen
self.force = UUID() // << here !!
}
}
}
}

Remove back button text from navigationbar in SwiftUI

I've recently started working in SwiftUI, came to the conclusion that working with navigation isn't really great yet. What I'm trying to achieve is the following. I finally managed to get rid of the translucent background without making the application crash, but now I ran into the next issue. How can I get rid of the "back" text inside the navbaritem?
I achieved the view above by setting the default appearance in the SceneDelegate.swift file like this.
let newNavAppearance = UINavigationBarAppearance()
newNavAppearance.configureWithTransparentBackground()
newNavAppearance.setBackIndicatorImage(UIImage(named: "backButton"), transitionMaskImage: UIImage(named: "backButton"))
newNavAppearance.titleTextAttributes = [
.font: UIFont(name: GTWalsheim.bold.name, size: 18)!,
.backgroundColor: UIColor.white
]
UINavigationBar.appearance().standardAppearance = newNavAppearance
One possible way that I could achieve this is by overriding the navigation bar items, however this has one downside (SwiftUI Custom Back Button Text for NavigationView) as the creator of this issue already said, the back gesture stops working after you override the navigation bar items. With that I'm also wondering how I could set the foregroundColor of the back button. It now has the default blue color, however I'd like to overwrite this with another color.
Piggy-backing on the solution #Pitchbloas offered, this method just involves setting the backButtonDisplayMode property to .minimal:
extension UINavigationController {
open override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
navigationBar.topItem?.backButtonDisplayMode = .minimal
}
}
It's actually really easy. The following solution is the fastest and cleanest i made.
Put this at the bottom of your SceneDelegate for example.
extension UINavigationController {
// Remove back button text
open override func viewWillLayoutSubviews() {
navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
}
This will remove the back button text from every NavigationView (UINavigationController) in your app.
I have found a straightforward approach to remove the back button text using SwiftUI only, and keeping the original chevron.
A drag gesture is added to mimic the classic navigation back button
when user wants to go back by swiping right. Following this, an extension of View is created to create a SwiftUI like modifier.
This is how to use it in code:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
// Your main view code here with a ZStack to have the
// gesture on all the view.
}
.navigationBarBackButtonTitleHidden()
}
}
This is how to create the navigationBarBackButtonTitleHidden() modifier:
import SwiftUI
extension View {
func navigationBarBackButtonTitleHidden() -> some View {
self.modifier(NavigationBarBackButtonTitleHiddenModifier())
}
}
struct NavigationBarBackButtonTitleHiddenModifier: ViewModifier {
#Environment(\.dismiss) var dismiss
#ViewBuilder #MainActor func body(content: Content) -> some View {
content
.navigationBarBackButtonHidden(true)
.navigationBarItems(
leading: Button(action: { dismiss() }) {
Image(systemName: "chevron.left")
.foregroundColor(.blue)
.imageScale(.large) })
.contentShape(Rectangle()) // Start of the gesture to dismiss the navigation
.gesture(
DragGesture(coordinateSpace: .local)
.onEnded { value in
if value.translation.width > .zero
&& value.translation.height > -30
&& value.translation.height < 30 {
dismiss()
}
}
)
}
}
Standard Back button title is taken from navigation bar title of previous screen.
It is possible the following approach to get needed effect:
struct TestBackButtonTitle: View {
#State private var hasTitle = true
var body: some View {
NavigationView {
NavigationLink("Go", destination:
Text("Details")
.onAppear {
self.hasTitle = false
}
.onDisappear {
self.hasTitle = true
}
)
.navigationBarTitle(self.hasTitle ? "Master" : "")
}
}
}
So I actually ended up with the following solution that actually works. I am overwriting the navigation bar items like so
.navigationBarItems(leading:
Image("backButton")
.foregroundColor(.blue)
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
)
The only issue with this was that the back gesture wasn't working so that was solved by actually extending the UINavigationController
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
Now it's looking exactly the way I want it, the solution is kinda hacky... but it works for now, hopefully SwiftUI will mature a little bit so this can be done easier.
Using the Introspect framework, you can easily gain access to the underlying navigation item and set the backButtonDisplayMode to minimal.
Hereโ€™s how you might use that in the view that was pushed
var body: some View {
Text("Your body here")
.introspectNavigationController { navController in
navController.navigationBar.topItem?.backButtonDisplayMode = .minimal
}
}
If you want to:
Do it globally
Keep the standard back button (along with custom behaviours like an ability to navigate a few screens back on a long press)
Avoid introducing any third party frameworks
You can do it by setting the back button text color to Clear Color via appearance:
let navigationBarAppearance = UINavigationBarAppearance()
let backButtonAppearance = UIBarButtonItemAppearance(style: .plain)
backButtonAppearance.focused.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.disabled.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.highlighted.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.clear]
navigationBarAppearance.backButtonAppearance = backButtonAppearance
//Not sure you'll need both of these, but feel free to adjust to your needs.
UINavigationBar.appearance().standardAppearance = navigationBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
You can do it once when the app starts and forget about it.
A potential downside (depending on your preferences) is that the transition to the clear color is animated as the title of the current window slides to the left as you move to a different one.
You can also experiment with different text attributes.
Works on iOS 16
Solutions above didn't work for me. I wanted to make changes specific to view without any global (appearance or extension) and with minimal boilerplate code.
Since you can update NavigationItem inside the init of the View. You can solve this in 2 steps:
Get visible View Controller.
// Get Visible ViewController
extension UIApplication {
static var visibleVC: UIViewController? {
var currentVC = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController
while let presentedVC = currentVC?.presentedViewController {
if let navVC = (presentedVC as? UINavigationController)?.viewControllers.last {
currentVC = navVC
} else if let tabVC = (presentedVC as? UITabBarController)?.selectedViewController {
currentVC = tabVC
} else {
currentVC = presentedVC
}
}
return currentVC
}
}
Update NavigationItem inside init of the View.
struct YourView: View {
init(hideBackLabel: Bool = true) {
if hideBackLabel {
// iOS 14+
UIApplication.visibleVC?.navigationItem.backButtonDisplayMode = .minimal
// iOS 13-
let button = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
UIApplication.visibleVC?.navigationItem.backBarButtonItem = button
}
}
}
custom navigationBarItems and self.presentationMode.wrappedValue.dismiss() worked but you are not allow to perform swiping back
You can either add the following code to make the swipe back again
//perform gesture go back
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
but the problem is, sometimes it will make your app crashed when you swipe half the screen and then cancel.
I would suggest the other way to remove the "Back" text.
Adding the isActive state to monitor whether the current screen is active or not. :)
struct ContentView: View {
#State var isActive = false
var body: some View {
NavigationView() {
NavigationLink(
"Next",
destination: Text("Second Page").navigationBarTitle("Second"),
isActive: $isActive
)
.navigationBarTitle(!isActive ? "Title" : "", displayMode: .inline)
}
}
}
I am accomplishing this by changing the title of the master screen before pushing the detail screen and then setting it back when it re-appears. The only caveat is when you go back to the master screen the title's re-appearance is a little noticeable.
Summary:
on master view add state var (e.g. isDetailShowing) to store if detail screen is showing or not
on master view use the navigationTitle modifier to set the title based on the current value of isDetailShowing
on master view use onAppear modifier to set the value of isDetailShowing to false
on the NavigationLink in master screen use the simultaneousGesture modifier to set the isDetailShowing to true
struct MasterView: View {
#State var isDetailShowing = false
var body: some View {
VStack {
Spacer()
.frame(height: 20)
Text("Master Screen")
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
.frame(height: 20)
NavigationLink(destination: DetailView()) {
Text("Go to detail screen")
}
.simultaneousGesture(TapGesture().onEnded() {
isDetailShowing = true
})
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(isDetailShowing ? "" : "Master Screen Title")
.onAppear() {
isDetailShowing = false
}
}
}
struct DetailView: View {
var body: some View {
Text("This is the detail screen")
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Detail Screen Title")
}
}
you can use .toolbarRole(.editor)
Why not use Custom BackButton with Default Back Button Hidden
You could use Any Design You Prefer !
Works on iOS 16
First View
struct ContentView: View {
var body: some View {
NavigationView {
VStack(){
Spacer()
NavigationLink(destination: View2()) {
Text("Navigate")
.font(.title)
}
Spacer()
}
}
}
}
Second View
struct View2: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
ZStack{
VStack{
HStack(alignment:.center){
//Any Design You Like
Image(systemName: "chevron.left")
.font(.title)
.foregroundColor(.blue)
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
.padding()
Spacer()
}
Spacer()
}
}
}
.navigationBarBackButtonHidden(true)
}
}

iOS SwiftUI: Correctly stack NavigationView with ScrollView and ZStack?

So I've been playing around with SwiftUI and I can't seem to stack NavigationView correctly with ScrollView and ZStack.
I'm trying to achieve a view with a .black background. Code below:
MainView.swift
var body: some View {
NavigationView {
ChildView()
}
}
ChildView.swift
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
ScrollView {
...
}
}
}
The above code gives me a black background but breaks the navigation bar behaviour.
How it looks:
How it should look:
So it seems like the the navigation doesn't go from the scrollEdgeAppearance to standardAppearance when scrolling.
Any ideas?
Put the ScrollView up as far as possible, otherwise the header animation will not work. The following code does what you want:
struct ChildView: View {
var body: some View {
ScrollView {
VStack(spacing: 20) {
ForEach(0..<40) { _ in
Text("Hello, World!")
.frame(maxWidth: .infinity)
}
}
.background(Color.green.edgesIgnoringSafeArea(.all))
}
.navigationBarTitle("Discover")
}
}
struct ContentView: View {
var body: some View {
NavigationView {
ChildView()
}
}
}
To achieve a completely black background, even if you scroll up the bottom, you have to fiddle around in UINavigationController. While there, I also changed the navigation bar colors to achieve your look and feel. Note that this doesn't keep track of whether the user enabled dark mode or not.
extension UINavigationController {
override open func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = .dark
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = UIColor.black
appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
navigationBar.standardAppearance = appearance
navigationBar.compactAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
}
}

How to change selected segment color in SwiftUI Segmented Picker

I want to set the selected segment color in a SwiftUI segmented picker and change the text color to white.
I have tried both using the modifiers for the picker view and modifying the tint color from the appearance proxy. None of them seem to work, unfortunately.
import SwiftUI
struct PickerView: View {
#State var pickerSelection = 0
init() {
UISegmentedControl.appearance().tintColor = UIColor.blue
}
var body: some View {
Picker(selection: $pickerSelection, label: Text("")) {
Text("Active").tag(0).foregroundColor(Color.white)
Text("Completed").tag(1)
}.pickerStyle(SegmentedPickerStyle()).foregroundColor(Color.orange)
}
}
Is there any way to do this in SwiftUI, or should I just use the UISegmentedControl by using UIViewControllerRepresentable?
Native ( but limited )
SwiftUI is not currently supporting native SegmentedPicker styling (see the bottom of the answer for the working workaround). But there is a limited way to apply a color to the segmented picker using .colorMultiply() modifier:
Full control using UIAppearance
selectedSegmentTintColor is available since beta 3 for changing the color of the selected segment.
For changing the textColor, you should use setTitleTextAttributes for .selected state and .normal state (unselected).
So it will be like:
init() {
UISegmentedControl.appearance().selectedSegmentTintColor = .blue
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.blue], for: .normal)
}
Also as mike mentioned, you can set the background color too like:
UISegmentedControl.appearance().backgroundColor = .yellow
Also, don't forget you can set SwiftUI colors too! For example:
UISegmentedControl.appearance().selectedSegmentTintColor = UIColor(Color.accentColor)
Recently I had a similar issue that I needed a custom background and foreground colors for SwiftUI SegmentedControl I've found this post that also was helpful, but I also wanted to share other solution that I've found and I'm using now.
There is a very nice package called SwiftUI Introspect that allows to, well...
Introspect underlying UIKit components from SwiftUI
With this package I created the following SegmentedControl
With this code:
VStack {
Picker("Activity view", selection: $selectedActivity) {
ForEach(ActivityView.allCases) { activity in
Text(activity.description)
}
}
.introspectSegmentedControl { segmentedControl in
segmentedControl.backgroundColor = .clear
segmentedControl.tintColor = TCHColors.SegmentedControl.backgroundSelected
segmentedControl.selectedSegmentTintColor = TCHColors.SegmentedControl.backgroundSelected
segmentedControl.setTitleTextAttributes([
NSAttributedString.Key.foregroundColor: TCHColors.SegmentedControl.selectedText
], for: .selected)
segmentedControl.setTitleTextAttributes([
NSAttributedString.Key.foregroundColor: TCHColors.SegmentedControl.normalText
], for: .normal)
}
.pickerStyle(.segmented)
}
.padding(.horizontal, 16)
And you can use this package to access many other underlying components from SwiftUI.
๐Ÿ”ด Unfortunately, there is no SwiftUI-native way to change the default colors of the segmented control. The solution that will let us override the default colors is to change the appearance of the UISegmentedControl, like we would have done in a UIKit based application.
๐Ÿ”ด In order to achieve that, itโ€™s necessary to explicitly define the init() method of the view we are working on:
struct ContentView: View {
init() {
}
...
}
๐Ÿ”ด Inside the init() body, we will specify the tint color of the selected segment, the text color in normal state, and the text color in selected state. Here is how all that is done:
init() {
UISegmentedControl.appearance().selectedSegmentTintColor = .systemIndigo
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.systemYellow], for: .normal)
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
}
๐Ÿ”ด The results of the above code are shown right below:
Done ๐Ÿ˜Š
This answered helped me, but there is something I would like to add. The color I used was an argument to the view, so placing these methods in an init did not allow me to access the color.
Alternatively, you can use an onAppear method directly on the Segmented Picker, like so.
import SwiftUI
struct PickerView: View {
var color: UIColor
#State var pickerSelection = 0
var body: some View {
Picker(selection: $pickerSelection, label: Text("")) {
Text("Active").tag(0).foregroundColor(Color.white)
Text("Completed").tag(1)
}.pickerStyle(SegmentedPickerStyle()).foregroundColor(Color.orange)
.onAppear {
UISegmentedControl.appearance().tintColor = color
}
}
}
Here is a manual implementation of segmented picker functionality:
#ViewBuilder func viewSegmentedButtons(arr: [String], selIndex: Int, baseColor: Color, closure:#escaping (_ selectedIndex: Int) -> Void) -> some View {
let columns = Array(repeating: GridItem(spacing: 1), count:arr.count)
LazyVGrid(columns: columns, spacing: 1.0) {
ForEach(Array(arr.enumerated()), id: \.element) { index, translation in
Button {
closure(index)
} label: {
ZStack {
Rectangle()
.foregroundColor(index == selIndex ? baseColor : Color(.systemBackground))
.cornerRadius(radius: index==0 ? cRadius : 0, corners: [.topLeft, .bottomLeft])
.cornerRadius(radius: index==arr.count-1 ? cRadius : 0, corners: [.topRight, .bottomRight])
Text(translation)
.padding(.vertical, 10)
.font(.footnote)
.foregroundColor(index != selIndex ? baseColor : Color(.systemBackground) )
}
}
}
}
.foregroundColor(baseColor)
.overlay(
RoundedRectangle(cornerRadius: cRadius)
.stroke(baseColor, lineWidth: 2)
)
.font(.callout)
.background(baseColor)
.cornerRadius(cRadius)
.padding(.bottom, 10)
}
Usage example:
#State private var levelIndex: Int = 0
var body: some View {
VStack {
Text("Level:")
viewSegmentedButtons(arr: ["Easy", "Meduim", "Hard"], selIndex: levelIndex, baseColor: .orange) { selectedIndex in
withAnimation {
levelIndex = selectedIndex
}
}
}
}
Result:
Here is my solution that works in SwiftUI:
init(){
UISegmentedControl.appearance().selectedSegmentTintColor = .green
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.black], for: .selected)
}

Resources