I have a layout that has a tabbar-like control at the bottom. I want a change in the tab tap to change the color of the tab item (this works) as well as change the index variable to trigger the display of different views. This last part is not being triggered. Here is my code with comments where the code is failing. I am writing the tab bar UI like this because its for MacOS.
@Published var index = 0
}
struct MaskingView: View {
// A change in this re-render the UI. It isn't.
@ObservedObject var progress: MaskingTabProgress
var body: some View {
ZStack {
Rectangle()
.foregroundColor(AppColor.lightGrayUIBackground)
.contentShape(Rectangle())
VStack(spacing: .zero) {
MenuBar()
if progress.index == 0 {
MaskingHomeView()
} else {
SomeOtherView() // Never changes
}
Spacer()
MaskingTabControl(items: ["Tab One","Tab Two"], icons:["shippingbox.fill","shippingbox.fill"], selectedIndex: 0)
.padding([.leading, .bottom, .trailing], 15)
}
}
.background(AppColor.lightGrayUIBackground)
}
}
struct SomeOtherView: View {
var body: some View {
ZStack {
VStack(alignment: .leading, spacing: .zero)
{
HeaderView(bannerSprite: "HomeViewHeader", logoSprite: "HomeViewIcon-White")
.padding(.bottom, 15)
// This provides padding on the sides. Like a div in HTML.
VStack(alignment: .leading)
{
Text("This is the SECOND Settings screen.")
}.padding([.leading, .bottom, .trailing], 15)
}
}
}
}
struct MaskingHomeView: View {
var body: some View {
ZStack
{
VStack(alignment: .leading, spacing: .zero)
{
HeaderView(bannerSprite: "HomeViewHeader", logoSprite: "HomeViewIcon-White")
.padding(.bottom, 15)
// This provides padding on the sides. Like a div in HTML.
VStack(alignment: .leading)
{
Text("This is the main Settings screen.")
}.padding([.leading, .bottom, .trailing], 15)
}
}
}
}
struct MaskingTabControl: View
{
@ObservedObject var progress = MaskingTabProgress()
private let tabText: [String]
private let icons: [String]
public init(items: [String], icons:[String], selectedIndex: Int) {
self.tabText = items
self.icons = icons
}
var body: some View {
ZStack {
Color.white
.ignoresSafeArea()
HStack {
ForEach(0..<tabText.count, id: \.self) { index in
HStack {
Image(systemName: self.icons[index])
.foregroundColor(progress.index == index ? .blue : .black)
.font(.system(size: 15, weight: .semibold))
Text(self.tabText[index])
.foregroundColor(progress.index == index ? .blue : .black)
.font(.system(size: 15, weight: .semibold))
.padding([.top,.bottom])
}.frame(width: 100)
.onTapGesture {
progress.index = index
}
}
}
}.frame(height: 60)
.clipShape(Capsule())
.shadow(color: .gray.opacity(0.5), radius: 5, x: 0, y: 0)
}
}
struct MaskingView_Previews: PreviewProvider
{
static var previews: some View
{
VStack (spacing: .zero)
{
MaskingView(progress: MaskingTabProgress())
}.frame(width: screenWidth, height: screenHeight)
.background(AppColor.lightGrayUIBackground)
}
}
Your issue is that you have multiple "sources of truth" for the selected index. You create a MaskingTabProgress
object that you pass as a parameter to MaskingView
then there is a completely independent instance of that you create for the MaskingTabControl
. You only need one instance of MaskingTabProgress
and you need to make sure your table control and your MaskingView
are using the index provided by that instance.
Below is a Playground, based on your code, that shows the general idea.
Instead of passing a constant value as the selectedIndex
for the MaskingTabControl
we pass a binding to the index value owned by the MaskingTabProgress
instance that is owned by the MaskingView
. That way each view is using the same index to control the behavior.
(note my playground eliminates parts of your code that you didn't provide source for).
import Cocoa
import SwiftUI
import PlaygroundSupport
class MaskingTabProgress: ObservableObject {
@Published var index = 0
}
struct MaskingView: View {
// A change in this re-render the UI. It isn't.
@ObservedObject var progress: MaskingTabProgress
var body: some View {
ZStack {
Rectangle()
.contentShape(Rectangle())
VStack(spacing: .zero) {
if progress.index == 0 {
MaskingHomeView()
} else {
SomeOtherView() // Never changes
}
Spacer()
MaskingTabControl(items: ["Tab One","Tab Two"], icons:["shippingbox.fill","shippingbox.fill"], selectedIndex: $progress.index)
.padding([.leading, .bottom, .trailing], 15)
}
}
}
}
struct SomeOtherView: View {
var body: some View {
ZStack {
VStack(alignment: .leading, spacing: .zero)
{
// This provides padding on the sides. Like a div in HTML.
VStack(alignment: .leading)
{
Text("This is the SECOND Settings screen.")
}.padding([.leading, .bottom, .trailing], 15)
}
}
}
}
struct MaskingHomeView: View {
var body: some View {
ZStack
{
VStack(alignment: .leading, spacing: .zero)
{
VStack(alignment: .leading)
{
Text("This is the main Settings screen.")
}.padding([.leading, .bottom, .trailing], 15)
}
}
}
}
struct MaskingTabControl: View
{
@Binding var selectedIndex: Int;
private let tabText: [String]
private let icons: [String]
public init(items: [String],
icons:[String],
selectedIndex: Binding<Int>) {
self.tabText = items
self.icons = icons
self._selectedIndex = selectedIndex
}
var body: some View {
ZStack {
Color.white
.ignoresSafeArea()
HStack {
ForEach(0..<tabText.count, id: \.self) { index in
HStack {
Image(systemName: self.icons[index])
.foregroundColor(selectedIndex == index ? .blue : .black)
.font(.system(size: 15, weight: .semibold))
Text(self.tabText[index])
.foregroundColor(selectedIndex == index ? .blue : .black)
.font(.system(size: 15, weight: .semibold))
.padding([.top,.bottom])
}.frame(width: 100)
.onTapGesture {
selectedIndex = index
}
}
}
}.frame(height: 60)
.clipShape(Capsule())
.shadow(color: .gray.opacity(0.5), radius: 5, x: 0, y: 0)
}
}
PlaygroundSupport.PlaygroundPage.current.liveView = NSHostingController(rootView: MaskingView(progress: MaskingTabProgress()))