Reputation: 1010
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
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
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