swiftuitabviewdidset

SwiftUI onReceive is it possible to get oldValue?


I have a custom TabView and I want to Bind to a State to change tabs. I also want to detect if the user has tapped the same tab again in order to scroll to the top of that view.

didSet isn't called when I use a binding. onChange isn't called because the value hasn't changed, and onReceive doesn't give me the old value to compare.

Any ideas? (Trying to avoid using a published property)

struct ContentView: View {
    
    @State private var scrollToTop1: Bool = false
    @State private var scrollToTop2: Bool = false
    
    @State private var selectedTab: Int = 1
    
    var body: some View {
        ZStack(alignment: .bottom) {
            TabView(selection: $selectedTab) {
                NavigationView {
                    View1(scrollToTop: $scrollToTop1)
                }
                .tag(1)
                
                NavigationView {
                    View2(scrollToTop: $scrollToTop2)
                }
                .tag(2)
            }
            .onReceive(Just(selectedTab)) { [oldValue = selectedTab] newValue in
                print("Old: \(oldValue)") //Shows newValue
                print("New: \(newValue)")
                if oldValue == newValue {
                    switch selectedTab {
                    case 1:
                        scrollToTop1.toggle()
                    case 2:
                        scrollToTop2.toggle()
                    default:
                        break
                    }
                }
            }
            
            TabBar(selectedTab: $selectedTab)
        }
    }
}

struct TabBar: View {
    @Binding var selectedTab: Int
    
    var body: some View {
        HStack {
            TabItem(selectedTab: $selectedTab, text: "View 1", tab: 1)
            TabItem(selectedTab: $selectedTab, text: "View 2", tab: 2)
        }
        .background(Color.green)
    }
}

struct TabItem: View {
    @Binding var selectedTab: Int
    let text: String
    let tab: Int
    
    var body: some View {
        Button {
            selectedTab = tab
        } label: {
            Text(text)
        }
        .frame(maxWidth: .infinity)
        .frame(height: 50)
    }
}

Solution

  • I think this is a great scenario for a custom Binding, where you can intercept the value before its set and compare it:

    struct ContentView: View {
        
        @State private var scrollToTop1: Bool = false
        @State private var scrollToTop2: Bool = false
        
        @State private var selectedTab: Int = 1
        
        var customBinding: Binding<Int> {
            .init {
                selectedTab
            } set: { newValue in
                print("New value: ", newValue)
                if newValue == selectedTab {
                    print("Scroll to top")
                }
                selectedTab = newValue
            }
        }
        
        var body: some View {
            ZStack(alignment: .bottom) {
                TabView(selection: customBinding) {
                    NavigationView {
                        Text("1")
                    }
                    .tag(1)
                    
                    NavigationView {
                        Text("2")
                    }
                    .tag(2)
                }
                
                TabBar(selectedTab: customBinding)
            }
        }
    }
    
    struct TabBar: View {
        @Binding var selectedTab: Int
        
        var body: some View {
            HStack {
                TabItem(selectedTab: $selectedTab, text: "View 1", tab: 1)
                TabItem(selectedTab: $selectedTab, text: "View 2", tab: 2)
            }
            .background(Color.green)
        }
    }