Search code examples
swiftswiftuiswiftui-animation

SwiftUI View Transition matching to an Segmented Picker


Is it possible to have three (more than 2!) views that are animated in and out from the sides according to an Picker with the SegmentedPickerStyle() style?

Having only 2 items in the picker makes the job static and thus a .transition(.move(edge: .leading)) on the first and a .transition(.move(edge: .trailing)) on the last view is sufficient. But what about more than 2?

Maybe there is a way to make the edge: ... parameter of the transition dynamically set to .leading or .trailing and depending on whether how the new value of the binding is greater or less than the current on.

I created the following sketch to demonstrate the issue of having the same view (view 2) with a different transition depending on whether a view on its right or left is selected. Please see this only as an example, the problem is not restricted to only three segments, nor only the middle view, nor only disappearing transitions etc..

enter image description here


Solution

  • Update The secret sauce is to make sure the transition fires with an animation and tell it what direction to go before doing so. Give this a try:

    class TabControllerContext : ObservableObject {
        @Published var selected = panels.one { didSet {
            if previous != selected {
                insertion = selected.makeMove(previous)
                removal = previous.makeMove(selected)
    
                withAnimation {
                    trigger = selected
                    previous = selected
                }
            }
        }}
        
        @Published var trigger = panels.one
        @Published var previous = panels.one
        var insertion : AnyTransition = .move(edge: .leading)
        var removal : AnyTransition = .move(edge: .trailing)
    }
    
    struct TabsWithTransitionsView: View {
        @EnvironmentObject var context : TabControllerContext
        
        var body: some View {
            VStack {
                Picker("Select Panel", selection: $context.selected) {
                    ForEach(panels.allCases) { panel in
                        panel.label.tag(panel)
                    }
                }.pickerStyle(SegmentedPickerStyle())
                
                ForEach(panels.allCases) { panel in
                    if context.trigger == panel {
                        panel.label
                            .background(panel.color)
                            .transition(.asymmetric(insertion: context.insertion, removal: context.removal))
                    }
                }
            }
        }
    }
    
    enum panels : Int, CaseIterable, Identifiable {
        case one = 1
        case two = 2
        case three = 3
        
        var label : some View {
            switch self {
            case .one:
                return Label("Tab One", systemImage: "1.circle")
            case .two:
                return Label("Tab Two", systemImage: "2.square")
            case .three:
                return Label("Tab Three", systemImage: "asterisk.circle")
            }
        }
        
        var color : Color {
            switch self {
            case .one: return Color.red.opacity(0.5)
            case .two: return Color.green.opacity(0.5)
            case .three: return Color.blue.opacity(0.5)
            }
        }
        
        func makeMove(_ otherPanel: panels) -> AnyTransition {
            return otherPanel.rawValue < self.rawValue ? .move(edge: .trailing) : .move(edge: .leading)
        }
        
        // so the enum can be indentified when enumerated
        var id : Int { self.rawValue }
    }
    
    struct TabsWithTransitionsView_Previews: PreviewProvider {
        static var previews: some View {
            TabsWithTransitionsView().environmentObject(TabControllerContext())
        }
    }