Martin Kopecký
Martin Kopecký

Reputation: 1010

Using State Machine State as TabView selection

There is an attempt to implement State Machine:

struct Transition {
    var targetState: any ContextState
    var effect: Effect? = nil
    
    typealias Effect = ( Context ) -> ()
}

/// Events may trigger transitions between the states.
/// Each method takes an instance of the 'Context' type as parameter
/// to be able to call methods on the 'Context' to effect actions as required
protocol Events {
    mutating func gotoState1(_: Context) -> Transition?
    mutating func gotoStateě(_: Context) -> Transition?
}

/// Protocol extension to provide DEFAULT implementations of all the Events protocol methods
extension Events {
    mutating func gotoState1(_: Context) -> Transition? { return nil }
    mutating func gotoState2(_: Context) -> Transition? { return nil }
}

/// Activities related to the states themselves.
/// These methods will allow each concrete state type to define 'entry' and 'exit' activities
/// for the state it represents
protocol Activities {
    func enter( _ : Context )
    func exit( _ : Context )
}

/// With DEFAULT empty implementation of both the methods in place, concrete state types will
/// need to implement these methods only for states that actually have 'entry' and/or 'exit'
/// activities respectively
extension Activities {
    func enter( _ : Context ) {}
    func exit( _ : Context ) {}
}

typealias ContextState = Events & Activities

with the concrete states implemented as:

struct State1: ContextState {
    func enter( _ : GoProRemote ) {
        // TODO: Perform the action to be taken once the state is entered
    }
    
    /// Go to State2
    mutating func gotoState2(_: Context) -> Transition? {
        return Transition(targetState: State2())
    }
}

struct State2: ContextState {
    func enter( _ : GoProRemote ) {
        // TODO: Perform the action to be taken once the state is entered
    }
    
    /// Go to State1
    mutating func gotoState1(_: Context) -> Transition? {
        return Transition(targetState: State1())
    }
}

And finally the controller:

struct Context {

    @State var state: ContextState = State1()
    
    /// Perform transition to the new state
    private func perform( transition : Transition? ) {
        guard let transition = transition else { return }
        state.exit( self )
        transition.effect?( self )
        state = transition.targetState
        state.enter( self )
    }
}

In SwiftUI there is a view containing the TabView which shall be the graphical representation of the particular state. Each tab within is designed for one state.

struct SMView: View {

    var context: Context

    var body: some View {
        TabView(selection: $context.state) {
            Text("State 1").tag(State1())
            Text("State 2").tag(State2())
        }
        .onChange(of: $context.state, {
                print("Going to \($context.state) state")
        })
    }
}

And this is the issue. The code above does not work as it is generating errors in Hashable nonconformity of the state variable etc...

Does someone has an idea how to use the state structs for indexing the tabs in TabView?

Upvotes: 0

Views: 99

Answers (2)

CouchDeveloper
CouchDeveloper

Reputation: 19154

To clarify my comment and for the sake of demonstration:

Here, the view acts as the actual machine implementing a Finite State Automaton, defined by the State, the Input (aka Event) and a transition function. Note that this is meant for demonstration only. Nonetheless, it should be clear, how you can map the state to different sub-views.

import SwiftUI


extension ContentView {
    enum State {
        case start
        case counter(Int)
    }
    
    enum Event {
        case start(Int)
        case increment
        case decrement
    }
    
    static func transition(_ state: State, event: Event) -> State {
        switch (state, event) {
        case (.start, .start(let value)):
            return .counter(value)
            
        case (.counter(let value), .increment):
            return .counter(value + 1)
        
        case (.counter(let value), .decrement):
            return .counter(value - 1)
            
        case (.counter(_), .start(_)):
            return state
            
        case (.start, .increment):
            return state
            
        case (.start, .decrement):
            return state
        }
    }

}

struct ContentView: View {
    
    @SwiftUI.State private var state: State = .start
    
    var body: some View {
        VStack {
            switch state {
            case .start:
                ContentUnavailableView(
                    "No Counter",
                    systemImage: "antenna.radiowaves.left.and.right"
                )
            case .counter(let counter):
                Text("Counter: \(counter)")
                    .padding()
                HStack {
                    Button("+") {
                        send(.increment)
                    }
                    .padding()
                    Button("-") {
                        send(.decrement)
                    }
                    .padding()
                }
            }
        }
        .task {
            try? await Task.sleep(for: .seconds(1))
            send(.start(0))
        }
    }
        
    private func send(_ event: Event) {
        let newState = Self.transition(self.state, event: event)
        self.state = newState
    }

}

#Preview {
    ContentView()
}

Upvotes: 1

malhal
malhal

Reputation: 30746

Change your var to:

@State private var context = Context()

Also remove the @State from inside your Context struct

Funcs inside structs need to be marked mutating, there is no need to use classes just to have funcs as some might suggest, e.g.

mutating func perform(...

FYI for animations it's

withAnimation {
   context.perform(...
}

Upvotes: 0

Related Questions