Use XCode-like icon buttons to control tab view in sidebar [duplicate] - swift

I am trying to create a tabbed panel similar to the Xcode properties panel but the standard tabbed panel appears to have a different look and feel with no way to change it. What controls should be used to create a similar looking tabbed panel?
EDIT:
I was not using a NSTabViewController - just had the TabView !!

I just created a new project with storyboard and with the provided layout, added to the View Controllers view a custom view at top. To the custom view added buttons, style = square, type = toggle, and used provided icons of type template. Assigned tag to buttons 0-4 and did put them to a horizontal stack view. Then added a horizontal line and a container view. Then I add a Tab View Controller to the storyboard and embed it to container view. All buttons are connected to same action.
import Cocoa
class ViewController: NSViewController {
#IBOutlet var myStackView: NSStackView!
var oldSelection: Int = 0
var newSelection: Int = 0
var buttons: [NSButton]?
var tabViewDelegate: NSTabViewController?
#IBAction func selectedButton(_ sender: NSButton) {
newSelection = sender.tag
tabViewDelegate?.selectedTabViewItemIndex = newSelection
buttons![oldSelection].state = .off
sender.state = .on
oldSelection = newSelection
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
buttons = (myStackView.arrangedSubviews as! [NSButton])
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
// Once on load
tabViewDelegate = segue.destinationController as? NSTabViewController
}
}

If you use storyboards, just drag tab view controller to the surface and connect to the window.
Then go to IB settings of tab view controller and change style to toolbar as shown below:
Then add tabs and add image to each tab as shown below:
Run your app and enjoy the look as XCode settings view:

SwiftUI
Implementation
struct SystemSegmentControl : View {
// MARK: - Internal -
#Binding var selection : Int
let systemImages: [String]
var body : some View {
HStack(spacing: 5) {
ForEach (0..<systemImages.count) { i in
SystemSegmentButton(selection: self.$selection, selectionIndex: i, systemImage: systemImages[i])
}
}
}
}
struct SystemSegmentButton : View {
// MARK: - Internal -
#Binding var selection : Int
let selectionIndex: Int
let systemImage : String
var body : some View {
Button(action: { self.selection = self.selectionIndex }) {
Image(systemName: systemImage)
.padding(8)
.foregroundColor(selectionIndex == selection ? .controlAccentColor : .controlColor)
}
.buttonStyle(BorderlessButtonStyle())
}
}
Usage
struct SettingsView: View {
// MARK: - Internal -
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
SystemSegmentControl(selection: $selection, systemImages: ["slider.horizontal.3", "eye"])
Divider()
switch selection {
case 0:
Text("Tab 1")
default:
Text("Tab 2")
}
}
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
}
.frame(width: 250)
}
// MARK: - Private -
#State private var selection = 0
}
Result

Related

Is it possible to override a view modifier from a custom view?

Is it possible to override your own default modifier on a custom View? If not, is there any fancy way to adjust this without using an init?
Example
struct MainView: View {
var body: some View {
CustomView()
.font(.custom(weight: .medium, fontSize: 28)) // I want the custom view to change its' "sub"-font and use this modifier instead of using .footnote font.
}
}
struct CustomView: View {
var body: some View {
VStack {
Divider()
Text("Random")
.font(.footnote)
}
}
}
One solution is just to add an Font property in the CustomView init and use it inside the viewModifier like below. But would be gladly to know if it's possible to change it from its' parent viewModifier! I might just end up with using the solution below if it's not possible.
struct CustomView: View {
let customFont: Font = .callout
var body: some View {
VStack {
Divider()
Text("Random")
.font(customFont)
}
}
}
To make your MainView work we can use extension with custom implementation of font modifier, explicit for CustomView.
Here is a demo of approach (prepared & tested with Xcode 12.5 / iOS 14.5)
CustomView()
.font(.custom("Arial", size: 28, relativeTo: .caption))
struct CustomView: View {
private var customFont: Font = .footnote
var body: some View {
VStack {
Divider()
Text("Random")
.font(customFont)
}
}
}
extension CustomView {
func font(_ font: Font) -> some View {
var updatedView = self // make writable
updatedView.customFont = font // update in copy
return updatedView // return updated with external font
}
}
You can create one Appearance class and mention all the style property for your subview component and make an own function for all property inside the view.
Here is the demo code.
CustomViewAppearance
class CustomViewAppearance {
var customFont: Font = .footnote
var textColor: Color = .red
}
CustomView and property function.
struct CustomView: View {
private var appearance = CustomViewAppearance()
var body: some View {
VStack {
Divider()
Text("Random")
.font(appearance.customFont)
.foregroundColor(appearance.textColor)
}
}
}
extension CustomView {
func font(_ font: Font) -> some View {
self.appearance.customFont = font
return self
}
func foregroundColor(_ color: Color) -> some View {
self.appearance.textColor = color
return self
}
}
--
You can also set direct Appearance.
extension CustomView {
func appearance(_ appearance: CustomViewAppearance) -> some View {
var selfView = self
selfView.appearance = appearance
return selfView
}
}
struct MainView: View {
var body: some View {
CustomView()
.appearance(customStyle())
}
func customStyle() -> CustomViewAppearance {
let appearance = CustomViewAppearance()
appearance.customFont = .largeTitle
appearance.textColor = .yellow
return appearance
}
}

Why swift view does not update with UIKit?

I have swiftUI tabbar with animation also I have UITabbarViewController which contains this swiftUI view.
SwiftUI
struct MainTabBarView: View {
#ObservedObject var viewModel: MainTabBarViewModel
var body: some View {
VStack {
HStack(spacing: 0) {
TabItem(currentIndex: $viewModel.index, tabIndex: 0, tab: .home)
Spacer()
TabItem(currentIndex: $viewModel.index, tabIndex: 1, tab: .search)
Spacer()
TabItem(currentIndex: $viewModel.index, tabIndex: 2, tab: .library)
}.padding(.top, 8).padding(.leading, 28).padding(.trailing, 28)
.padding(.bottom, 8)
.frame(width: UIScreen.main.bounds.width)
.animation(.easeIn(duration: 0.2))
}
UITabbarViewController:
final class MainTabBarViewController: UITabBarController, Navigatable {
private var viewModel = MainTabBarViewModel()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
override func viewDidLoad() {
super.viewDidLoad()
configTabBarView()
}
private func configTabBarView() {
let view = UIHostingController(rootView: MainTabBarView(viewModel: viewModel))
addChild(child)
tabBar.addSubview(view.view)
view.didMove(toParent: self)
tabBar.setValue(true, forKey: "hidesShadow")
view.view.snp.makeConstraints { make in
make.leading.trailing.top.equalToSuperview()
make.height.equalTo(100)
}
}
ViewModel:
final class MainTabBarViewModel: NSObject, ObservableObject {
#Published var index: Int = 0
}
This is the code of my custom view with tabs.
When I press on tab -> index from viewModel is changing in this view and then
struct TabItem: View {
#Binding var currentIndex: Int
var tabIndex: Int
var isCurrentTab: Bool {
return currentIndex == tabIndex
}
var tab: Tabs
var body: some View {
HStack {
Image(uiImage: tab.icon).resizable().frame(width: 25, height: 25, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.foregroundColor(isCurrentTab ? R.color.primaryColor()!.swiftUI : R.color.grayScaleDark()!.swiftUI)
Text(isCurrentTab ? tab.title : "").font(Font(R.font.poppinsRegular(size: 16)! as CTFont))
.foregroundColor(isCurrentTab ? R.color.primaryColor()!.swiftUI : R.color.grayScaleDark()!.swiftUI)
}.padding(15)
.onTapGesture {
self.currentIndex = tabIndex
logWarn(String(tabIndex))
}
.background(self.tabIndex == currentIndex ? Color(R.color.pink()!) : Color.white)
.frame(height: 44.0)
.cornerRadius(22.0)
.clipped()
}
}
The problems that index is changing, but tab does not update view. It is look, like view does not update.
Although it would be helpful for the given code to compile, I think I've pieced together a solution that allows the current index to remain in MainTabBarViewModel, under the assumption that the it is intended to hold other things in the future. I also make the assumption that TabItem is supposed to be sufficiently generic that it doesn't have to depend on MainTabVarViewModel specifically.
My idea is based on providing a key path to the current index when creating the TabItem. Were it not for the fact that TabItem updates the current index that would be simple enough. However, because it does update it, in a onTapGesture closure, the compiler complains that it can't write through the KeyPath, because self is immutable. So... summoning the ghost a David Wheeler, I tried doing it through a closure saved in TabItem.init... which doesn't exist, so adding that is part of the solution.
First TabItem becomes a generic:
struct TabItem<TabBarViewModel: ObservableObject>: View {
#ObservedObject var viewModel: TabBarViewModel
let indexKeyPath: WritableKeyPath<TabBarViewModel, Int>
let tapClosure: (Int) -> Void
// #Binding var currentIndex: Int
var currentIndex: Int { viewModel[keyPath: indexKeyPath] }
...
init(currentIndexIn viewModel: TabBarViewModel, at indexKeyPath: WritableKeyPath<TabBarViewModel, Int>, tabIndex: Int, tab: Tabs)
{
self.viewModel = viewModel
self.indexKeyPath = indexKeyPath
self.tabIndex = tabIndex
self.tab = tab
self.tapClosure = { self.viewModel[keyPath: indexKeyPath] = $0 }
}
With that everything in TabItem compiles (after removing references to unprovided code) except for this bit in body
.onTapGesture {
currentIndex = tabIndex
print(String(tabIndex))
}
I change that to use the closure that was saved off in the init:
.onTapGesture {
tapClosure(tabIndex)
print(String(tabIndex))
}
Then in MainTabBarView's body creating a TabItem changes from this:
TabItem(currentIndex: $viewModel.index, tabIndex: 0, tab: .home)
to this
TabItem(currentIndexIn: viewModel, at: \.index, tabIndex: 0, tab: .home)
Since there is too much code missing for me to compile and test it, this is a bit of a guess. I stubbed out missing code just to silence the errors for it, so I can say that at least this solution compiles. It would need to be applied and tested in the actual app to verify whether it works.
Anyway, assuming it does work, this solution still allows keeping the index in an observable object without relying on the particular type of that object.

React on drag over view

I have a view that I want to change whenever long press or drag occurs over it. It can start outside of the view, but also inside.
struct ContentView: View {
#State var active: Bool = false
var body: some View {
Rectangle()
.fill(self.active ? Color.red : Color.secondary)
}
}
An example of this behaviour is the iPhone keyboard: when you long press on a key it pops up (active = true). When you move outside it, it pops down (active = false) but the next key is then active.
I have tried using LongPressGesture but cannot figure out how to make it behave as I want.
I made example for you in playgrounds to display how to use a LongPressGestureRecognizer.
You add the gesture recognizer to the view you want long pressed, with target being the parent controller handling the gesture recognition (ContentView in your case) and action being what happens with a long press.
In my implementation, a long press to the view bodyView changes its color from clear to red. This happens inside the didSet of the showBody property, which calls showBodyToggled(). I'm checking the state of the gesture, because the gesture recognizer will send messages for each state (I'm only performing the action if the state is .began).
Let me know if you have any questions:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
var bodyView: UIView!
var showBody = false {
didSet {
showBodyToggled()
}
}
override func viewDidLoad() {
super.viewDidLoad()
configureSubviews()
configureLongPressRecognizer()
}
func configureSubviews() {
bodyView = UIView()
bodyView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bodyView)
NSLayoutConstraint.activate([
bodyView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
bodyView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
bodyView.heightAnchor.constraint(equalToConstant: 200),
bodyView.widthAnchor.constraint(equalToConstant: 300)
])
}
func configureLongPressRecognizer() {
bodyView.addGestureRecognizer(
UILongPressGestureRecognizer(
target: self,
action: #selector(longPressed(_:))
)
)
}
#objc func longPressed(_ sender: UILongPressGestureRecognizer) {
// the way I'm doing it: only acts when the long press first happens.
// delete this state check if you'd prefer the default long press implementation
switch sender.state {
case .began:
showBody = !showBody
default:
break
}
}
func showBodyToggled() {
UIView.animate(withDuration: 0.4) { [weak self] in
guard let self = self else { return }
self.bodyView.backgroundColor = self.showBody ? .red : .clear
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
EDIT:
Here's an example in SwiftUI where a long press of 0.5 seconds toggles the color of a circle between red and black
struct ContentView: View {
#State var active = false
var longPress: some Gesture {
LongPressGesture()
.onEnded { _ in
withAnimation(.easeIn(duration: 0.4)) {
self.active = !self.active
}
}
}
var body: some View {
Circle()
.fill(self.active ? Color.red : Color.black)
.frame(width: 100, height: 100, alignment: .center)
.gesture(longPress)
}
}

SwiftUI hide TabBar in subview

I am working with SwiftUI, and I have some issues with the TabBar.
I want to hide the TabBar on a specific subview.
Have tried with
UITabBar.appearance().isHidden = true
It only works on the direct views in the TabView. But when I place it in a subview it doesn't work.
Have anyone a solution for this?
Thanks.
iOS 16 native way
.toolbar(.hidden, for: .tabBar)
iOS 14
Install the Introspect SwiftPM: https://github.com/siteline/SwiftUI-Introspect
struct SomeView: View{
#State var uiTabarController: UITabBarController?
var body: some View {
List {
-your code here-
}
.navigationBarTitle("Title", displayMode: .inline)
.introspectTabBarController { (UITabBarController) in
UITabBarController.tabBar.isHidden = true
uiTabarController = UITabBarController
}.onDisappear{
uiTabarController?.tabBar.isHidden = false
}
}
}
In this code in uiTabarController, we are taking the reference of UITabarController. When we go back then we enabled the Tabar again. So, that's why this is needed.
iOS 14 Simple Solution
Install the Introspect SwiftPM: https://github.com/siteline/SwiftUI-Introspect
var body: some View {
List {
-your code here-
}
.navigationBarTitle("Title", displayMode: .inline)
.introspectTabBarController { (UITabBarController) in
UITabBarController.tabBar.isHidden = true
}
}
NOTE: You have to re-enable the TabBar it in the parent view or it will still be hidden.
.introspectTabBarController { (UITabBarController) in
UITabBarController.tabBar.isHidden = false
}
It's working, only changes are need to be called on the main queue
struct ShowTabBar: ViewModifier {
func body(content: Content) -> some View {
return content.padding(.zero).onAppear {
DispatchQueue.main.async {
Tool.showTabBar()
}
}
}
}
struct HiddenTabBar: ViewModifier {
func body(content: Content) -> some View {
return content.padding(.zero).onAppear {
DispatchQueue.main.async {
Tool.hiddenTabBar()
}
}
}
}
Traverse the allsubview of the window to hide the UITabBar. You can write it as ViewModifier and use it in SwiftUI or use tools to hide it. This method works for me.
extension UIView {
func allSubviews() -> [UIView] {
var res = self.subviews
for subview in self.subviews {
let riz = subview.allSubviews()
res.append(contentsOf: riz)
}
return res
}
}
struct Tool {
static func showTabBar() {
UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.allSubviews().forEach({ (v) in
if let view = v as? UITabBar {
view.isHidden = false
}
})
}
static func hiddenTabBar() {
UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.allSubviews().forEach({ (v) in
if let view = v as? UITabBar {
view.isHidden = true
}
})
}
}
struct ShowTabBar: ViewModifier {
func body(content: Content) -> some View {
return content.padding(.zero).onAppear {
Tool.showTabBar()
}
}
}
struct HiddenTabBar: ViewModifier {
func body(content: Content) -> some View {
return content.padding(.zero).onAppear {
Tool.hiddenTabBar()
}
}
}
extension View {
func showTabBar() -> some View {
return self.modifier(ShowTabBar())
}
func hiddenTabBar() -> some View {
return self.modifier(HiddenTabBar())
}
}
iOS 15 solution
This solution works well except with view modifier in the SwiftUI.TabView.
Since my TabView is in the struct that conforms App, it looks like there still is not any UITabBar subview in the connected scenes.
With the code below, you only need to use showTabBar() or hiddenTabBar() in your SwiftUI.View.
extension UIApplication {
var key: UIWindow? {
self.connectedScenes
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?
.windows
.filter({$0.isKeyWindow})
.first
}
}
extension UIView {
func allSubviews() -> [UIView] {
var subs = self.subviews
for subview in self.subviews {
let rec = subview.allSubviews()
subs.append(contentsOf: rec)
}
return subs
}
}
struct TabBarModifier {
static func showTabBar() {
UIApplication.shared.key?.allSubviews().forEach({ subView in
if let view = subView as? UITabBar {
view.isHidden = false
}
})
}
static func hideTabBar() {
UIApplication.shared.key?.allSubviews().forEach({ subView in
if let view = subView as? UITabBar {
view.isHidden = true
}
})
}
}
struct ShowTabBar: ViewModifier {
func body(content: Content) -> some View {
return content.padding(.zero).onAppear {
TabBarModifier.showTabBar()
}
}
}
struct HiddenTabBar: ViewModifier {
func body(content: Content) -> some View {
return content.padding(.zero).onAppear {
TabBarModifier.hideTabBar()
}
}
}
extension View {
func showTabBar() -> some View {
return self.modifier(ShowTabBar())
}
func hiddenTabBar() -> some View {
return self.modifier(HiddenTabBar())
}
}
here's no way to hide TabView so I had to add TabView inside ZStack as this:
var body: some View {
ZStack {
TabView {
TabBar1().environmentObject(self.userData)
.tabItem {
Image(systemName: "1.square.fill")
Text("First")
}
TabBar2()
.tabItem {
Image(systemName: "2.square.fill")
Text("Second")
}
}
if self.userData.showFullScreen {
FullScreen().environmentObject(self.userData)
}
}
}
UserData:
final class UserData: ObservableObject {
#Published var showFullScreen = false
}
TabBar1:
struct TabBar1: View {
#EnvironmentObject var userData: UserData
var body: some View {
Text("TabBar 1")
.edgesIgnoringSafeArea(.all)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(Color.green)
.onTapGesture {
self.userData.showFullScreen.toggle()
}
}
}
FullScreen:
struct FullScreen: View {
#EnvironmentObject var userData: UserData
var body: some View {
Text("FullScreen")
.edgesIgnoringSafeArea(.all)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(Color.red)
.onTapGesture {
self.userData.showFullScreen.toggle()
}
}
}
check full code on Github
there's also some other ways but it depends on the structure of the views
The basic idea I'm using is to combine ObservableObject and ZStack. I have placed TabView into ZStack with conditional subview presentation.
It's look like.
Look through github repo
To hide TabBar when we jumps towards next screen we just have to place NavigationView to the right place. Makesure Embed TabView inside NavigationView so creating unique Navigationview for both tabs.
As explained here https://janeshswift.com/ios/swiftui/how-to-hide-tabbar-on-push-with-swiftui/
import SwiftUI
struct TabBarView: View {
#State var tabSelection: Int = 0
#State var tabArray = ["Profile", "Settings"]
var body: some View {
NavigationView {
TabView(selection: $tabSelection){
ForEach(0 ..< tabArray.count, id: \.self) { indexValue in
NavigationLink(destination: DetailView()){
VStack{
Text("\(tabArray[indexValue]) tab -- Click to jump next view")
}
}
.tabItem {
Image(systemName: "\(indexValue).circle.fill")
Text(tabArray[indexValue])
}
.tag(indexValue)
}
}
.navigationBarTitle(tabArray[tabSelection])
}
}
}
struct DetailView: View {
var body: some View {
Text("Detail View")
.navigationBarTitle("NavigatedView")
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("helllo")
}
}
Install the Introspect SwiftPM: https://github.com/siteline/SwiftUI-Introspect
in order to use this you need to create a variable of type UITabBar in the view you want the tabbar to be hidden...
enter code here
#State private var tabBar: UITabBar?
then below the navigationView in the same view you have to add this line:
.introspectTabBarController { UITabBarController in tabBar = UITabBarController.tabBar
self.tabBar?.isHidden = true } .onDisappear() { self.tabBar?.isHidden = false }
It is actually possible to get the underlying UITabbarController for TabView by using this handy little framework :
https://github.com/siteline/SwiftUI-Introspect
This solution uses the MVVM pattern as an example to have programmatic control over the Tabbar visibility, and be able to show, hide, enable, disable form anywhere in the code using NSNotifications
SwiftUI View : Setup the tabview like this
struct MainTabView: View {
var viewModel: MainTabViewModel
var body: some View {
TabView() {
Text("View1")
.tabItem {
Text("View1")
}
Text("View2")
.tabItem {
Text("View2")
}
}
.introspectTabBarController { tabBarController in
// customize here the UITabBarViewController if you like
self.viewModel.tabBarController = tabBarController
}
}
}
Then for the ViewModel
final class MainTabViewModel: ObservableObject {
var tabBarController: UITabBarController?
init() {
startListeningNotifications()
}
func startListeningNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(showTabbarView), name: "showBottomTabbar", object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(hideTabbarView), name: "hideBottomTabbar", object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(enableTabbarTouch), name: "enableTouchTabbar", object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(disableTabbarTouch), name: "disableTouchTabbar", object: nil)
}
#objc func showTabbarView() {
self.tabBarController?.tabBar.isHidden = false
}
#objc func hideTabbarView() {
self.tabBarController?.tabBar.isHidden = true
}
#objc func disableTabbarTouch() {
self.tabBarController?.tabBar.isUserInteractionEnabled = false
}
#objc func enableTabbarTouch() {
self.tabBarController?.tabBar.isUserInteractionEnabled = true
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
and finally to control the tabbar, just use these fonctions from wherever you feel like (will be in the viewmodels in the pattern of this example)
public func showTabbar() {
DispatchQueue.main.async {
NotificationCenter.default.post(name: .showBottomTabbar, object: nil)
}
}
public func hideTabbar() {
DispatchQueue.main.async {
NotificationCenter.default.post(name: .hideBottomTabbar, object: nil)
}
}
public func enableTouchTabbar() {
DispatchQueue.main.async {
NotificationCenter.default.post(name: .enableTouchTabbar, object: nil)
}
}
public func disableTouchTabbar() {
DispatchQueue.main.async {
NotificationCenter.default.post(name: .disableTouchTabbar, object: nil)
}
}
Increase the frame size of TabView like this:
.frame(width: UIScreen.main.bounds.width, height: showTabbar ? UIScreen.main.bounds.height : UIScreen.main.bounds.height + 100.00)
Not ideal and hacky but the simplest thing to do for me that is working very well was to hide the navigationBar of the outer navigationView and then add another navigationView in each of the TabView's views. Works well so far:
struct LaunchView: View {
var body: some View {
NavigationView {
TabView {
ViewA()
.tabItem {
Label("TabA", systemImage: "some.image")
}
ViewB()
.tabItem {
Label("TabB", systemImage: "some.image")
}
ViewC()
.tabItem {
Label("TabC", systemImage: "some.image")
}
}
.navigationBarHidden(true)
}
}
}
struct ViewA: View {
var body: some View {
NavigationView {
// Content
.navigationTitle("Settings")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
This way you can set the title but also the .toolBarItem's in each separate view.
in general, it's nice to be able to create pages with an w/o tabbar
it looks smooth and your page content doesn't change it's size while hiding tabbar on the page
solution is
hide tabbar from the root container
add custom tabbar modifier
use this modifier on navViews to show the tabbar for all nav view hierarchy OR use it on the specific pages in the view hierarchy
here is a small sample project how your app could look like with this approach
https://github.com/alexis-ag/swiftui_classic-tabview_show-hide
I have tried to use https://stackoverflow.com/a/62963499/11844048 solution but the TabBar hide in all views once I landed this view. I have modified it a bit to achieve to hide TabBar in single view.
struct AppInfoView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body: some View {
ZStack{
}
.frame(maxWidth: .infinity)
.background(Color("homepage_bg")).ignoresSafeArea(.all)
.onAppear{
UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.allSubviews().forEach({ (v) in
if let view = v as? UITabBar {
view.isHidden = true
}
})
}
.onDisAppear(...) //it works too. But seeing TabBar shown bit delay when naviagting back. So below the customizable back button.
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action : {
self.mode.wrappedValue.dismiss()
UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.allSubviews().forEach({ (v) in
if let view = v as? UITabBar {
view.isHidden = false
}
})
}){
Image(systemName: "chevron.left")
})
}
extension UIView {
func allSubviews() -> [UIView] {
var res = self.subviews
for subview in self.subviews {
let riz = subview.allSubviews()
res.append(contentsOf: riz)
}
return res
}
}
Most answers here deal with this requirement in one of two ways:
import a framework to locate the UITabBarController
modify the view hierarchy (ZStack, NavigationView, ...)
The first one is a clean approach: it locates the underlying element that enables the desired action. However, it may be overkill for a single use case.
The second approach involves some tradeoffs and could be generally considered a smell, since it introduces hierarchy changes for the sake of working around the lack of access to the required element.
Instead, we could follow a clean, simple approach by creating a protocol extension like so:
import UIKit
protocol TabBarAppearanceDelegate {
func toggleVisibility()
func hideTabBar()
func showTabBar()
// add more methods to control appearance as needed
}
extension TabBarAppearanceDelegate {
private var tabBarController: UITabBarController? {
// this is where we access the underlying element, no need to import a framework for a one-liner
UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController?.children.first as? UITabBarController
}
func toggleVisibility() {
tabBarController?.tabBar.isHidden.toggle()
}
func hideTabBar() {
tabBarController?.tabBar.isHidden = true
}
func showTabBar() {
tabBarController?.tabBar.isHidden = false
}
}
Then we can make any object conform to this protocol, and inject it as dependency in the views as needed. This will depend on your architecture but it could go like follows.
This is where you'd keep app-wide state, an ObservableObject (you could designate a different one, if preferred):
import Foundation
class StateController: ObservableObject {
// you would typically manage app-wide state here
}
// this is where we adopt the needed behaviour
extension StateController: TabBarAppearanceDelegate {}
We can now inject the object as a view dependency:
#main
struct TabBarVisibilityApp: App {
private let stateController = StateController()
var body: some Scene {
WindowGroup {
TabView {
NavigationView {
SampleView(tabBarAppearanceDelegate: stateController)
}
.tabItem {
Label("Home", systemImage: "house")
}
}
}
}
}
This is how you would use it (valid for in any view that requires the behaviour):
import SwiftUI
struct SampleView: View {
let tabBarAppearanceDelegate: TabBarAppearanceDelegate
var body: some View {
VStack {
Spacer()
Button(action: {
tabBarAppearanceDelegate.toggleVisibility()
} ) {
Text("Toggle tab bar visibility")
}
Spacer()
}
}
}
This approach is simple, testable, and requires no extra dependencies... until Apple provides a direct way to control tab bar visibility with a SwiftUI API.
iOS 16
Usage of .toolbar modifier. Create state property of type Visibility and manage its value from the pushed view
First view
struct FirstTab: View {
#State var tabBarVisibility: Visibility = .visible
var body: some View {
NavigationView {
NavigationLink(destination: WidgetDetailView(tab: self)) {
Text("test")
}
}
.toolbar(tabBarVisibility, for: .tabBar)
}
}
Second view
struct WidgetDetailView: View {
var tab: FirstTab
var body: some View {
Rectangle()
.foregroundColor(Color.red)
.onAppear {
tab.tabBarVisibility = .hidden
}
.onDisappear {
tab.tabBarVisibility = .visible
}
}
}
On iOS 16
Use .toolbar(.hidden, for: .tabBar).
For example:
var body: some View {
TabView {
FirstView()
.tabItem {
Text("First tab")
}
.toolbar(.hidden, for: .tabBar)
SecondView()
.tabItem {
Text("Second tab")
}
.toolbar(.hidden, for: .tabBar)
}
}
Note that .toolbar(.hidden, for: .tabBar) is applied to each tabItem, not to the parent view. YMMV if you have a different structure (like a parent NavigationView etc.)
Add NavigationView as root instead of TabView
NavigationView{
TabView(selection:$selectedIndex) {
}
}
If you don't want NavigationView in TabBar page just hide it.
.navigationBarHidden(true)
Refer sample project - https://github.com/TreatTrick/Hide-TabBar-In-SwiftUI
just use UIKit‘s UINavigationController, like this:
let host = UINavigationController(rootViewController:
UIHostingController(rootView: HLHome()))
It is possible!
Basically your task is to extract UITabBar somehow and then hide it programatically.
Below is the code which emulates tab bar hiding on push behaviour.
struct ContentView: View {
var body: some View {
TabView {
ForEach(titles, id: \.self) { title in
NavigationView {
view(fromTitle: title)
}
.tabItem {
Text(title)
Image(systemName: "photo")
}
.tag(title)
}
}
}
private let titles = ["one", "two"]
#ViewBuilder
private func view(fromTitle title: String) -> some View {
if title == "one" {
RegularView(title: title)
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
} else {
HideOnPushView(title: title)
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct RegularView: View {
let title: String
var body: some View {
VStack(spacing: 20) {
Text(title)
NavigationLink("Regular push") {
Text("1111111")
}
}
}
}
struct HideOnPushView: View {
let title: String
var body: some View {
VStack(spacing: 20) {
Text(title)
NavigationLink("Hide on push") {
Text("222222")
.onAppear {
tabBar?.hide()
}
}
}
.background(
TabBarExtractor(tabBar: $tabBar)
)
.onAppear {
tabBar?.show()
}
}
#State private var tabBar: UITabBar?
}
TabBar extractor code:
import SwiftUI
struct TabBarExtractor: UIViewControllerRepresentable {
#Binding var tabBar: UITabBar?
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
func makeUIViewController(context: Context) -> some UIViewController {
let controller = ViewController()
controller.onTabBarAppearance = {
tabBar = $0
}
return controller
}
}
private extension TabBarExtractor {
class ViewController: UIViewController {
var onTabBarAppearance: ((UITabBar) -> Void)?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let tabBar = self.tabBarController?.tabBar {
onTabBarAppearance?(tabBar)
} else {
print("Could not locate TabBar! Try change extractor place in views hierarchy.")
}
}
}
}
TabBar category:
import UIKit
extension UITabBar {
func toggleVisibility() {
if isHidden {
show()
} else {
hide()
}
}
func show() {
guard isHidden else { return }
let visibleY = frame.origin.y
let hiddenY = visibleY + frame.height
frame.origin.y = hiddenY
isHidden = false
UIView.animate(withDuration: 0.3) { [weak self] in
self?.frame.origin.y = visibleY
}
}
func hide() {
guard !isHidden else { return }
let visibleY = frame.origin.y
let hiddenY = visibleY + frame.height
UIView.animate(withDuration: 0.3) { [weak self] in
self?.frame.origin.y = hiddenY
} completion: { [weak self] completed in
guard completed else { return }
self?.isHidden = true
self?.frame.origin.y = visibleY
}
}
}
I had the same problem, and ended up using:
.opacity(hideTabBar == true ? 0 : 1)
where hideTabBar is a Bool that I pass around to the views that need to hide the TabBar().
So basically you should try something like this:
TabView(selection: your desired tab).opacity(hideTabBar == true ? 0 : 1)

How can I know if a SwiftUI Button is enabled/disabled?

There is no isEnabled property for a SwiftUI button. How can i tell if it is enabled?
In regular UIKit, i would simply do
if button.isEnabeld == true {
} else {
}
but there is no SwiftUI equivalent.
Inside a view, if you wish to react to the state set by .disabled(true), you can use:
#Environment(\.isEnabled) var isEnabled
Since the environment can be used from within a View or a ViewModifier, this can be used to change layout properties of a view based on the state set from outside.
Unfortunately, ButtonStyle cannot directly use #Environment, but you can use a ViewModifier to inject environment values into a ButtonStyle in order to use the value from within a ButtonStyle:
// First create a button style that gets the isEnabled value injected
struct MyButtonStyle: ButtonStyle {
private let isEnabled: Bool
init(isEnabled: Bool = true) {
self.isEnabled = isEnabled
}
func makeBody(configuration: Configuration) -> some View {
return configuration
.label
.background(isEnabled ? .green : .gray)
.foregroundColor(isEnabled ? .black : .white)
}
}
// Then make a ViewModifier to inject the state
struct MyButtonModifier: ViewModifier {
#Environment(\.isEnabled) var isEnabled
func body(content: Content) -> some View {
return content.buttonStyle(MyButtonStyle(isEnabled: isEnabled))
}
}
// Then create a convenience function to apply the modifier
extension Button {
func styled() -> some View {
ModifiedContent(content: self, modifier: MyButtonModifier())
}
}
// Finally, try out the button and watch it respond to it's state
struct ContentView: View {
var body: some View {
Button("Test", {}).styled().disabled(true)
}
}
You can use this method to inject other things into a ButtonStyle, like size category and theme.
I use it with a custom style enum that contains all the flavours of button styles found in our design system.
From outside a view you should know if you used .disabled(true) modifier.
From inside a view you can use #Environment(\.isEnabled) to get that information:
struct MyButton: View {
let action: () -> Void
#Environment(\.isEnabled) private var isEnabled
var body: some View {
Button(action: action) {
Text("Click")
}
.foregroundColor(isEnabled ? .green : .gray)
}
}
struct MyButton_Previews: PreviewProvider {
static var previews: some View {
VStack {
MyButton(action: {})
MyButton(action: {}).disabled(true)
}
}
}
The whole idea of SwiftUI, is to avoid duplication of the source of truth. You need to think differently, and consider where the source of truth is. This is where you need to go to find out the button's state. Not from the button itself.
In "Data Flow Through SwiftUI", at minute 30:50, they explain that every piece of data has a single source of truth. If your button gets its state from some #Binding, #State, #EnvironmentObject, etc, your if statement should get that information from the same place too, not from the button.
Short answer: Just use inside struct:
#Environment(\.isEnabled) private var isEnabled
Button style with:
animation on hover change
animation on disable/enable change
can be applied on any button in native way of swiftUI
you need manually set size of buttons outside of the button
usage:
#State var isDisabled = false
///.......
Button("Styled button") { isDisabled.toggle() }
.buttonStyle(ButtStyle.BigButton()) // magic inside
.frame(width: 200, height: 50)
.disabled(isDisabled)
Button("switch isDisabled") { isDisabled.toggle() }
source code:
public struct ButtStyle { }
// Added style to easy stylyng in native way for SwiftUI
#available(macOS 11.0, *)
public extension ButtStyle {
struct BigButton: ButtonStyle {
init() {
}
public func makeBody(configuration: Configuration) -> some View {
BigButtonStyleView(configuration: configuration)
}
}
}
#available(macOS 11.0, *)
struct BigButtonStyleView : View {
let configuration: ButtonStyle.Configuration
#Environment(\.isEnabled) var isEnabled // here we getting "disabled"
#State var hover : Bool = false
var body: some View {
// added animations
MainFrameMod()
.animation(.easeInOut(duration: 0.2), value: hover)
.animation(.easeInOut(duration: 0.2), value: isEnabled)
}
// added opacity on move hover change
// and disabled status
#ViewBuilder
func MainFrameMod() -> some View {
if isEnabled {
MainFrame()
.opacity(hover ? 1 : 0.8)
.onHover{ hover = $0 }
} else {
MainFrame()
.opacity(0.5)
}
}
// Main interface of button
func MainFrame() -> some View {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(Color(hex: 0xD8D8D8))
configuration.label
.foregroundColor(.black)
.font(.custom("SF Pro", size: 18))
}
}
}
As mentioned by other developers, the main idea of SwiftUI is that the UI remains synced with the data. You can perform this in many different ways. This includes #State, #EnvironmentObject, #Binding etc.
struct ContentView: View {
#State private var isEnabled: Bool = false
var body: some View {
VStack {
Button("Press me!") {
}.disabled(isEnabled)
}
.padding()
}
}