iosswiftgeneric-associated-types

Generic types in Swift 6+ with Protocols and Structs


Coming from an Android & Kotlin background, I am trying to adapt my Android app to iOS for which I have no dev experience whatsoever.

My app uses the state machine pattern to handle the UI and UI Events (either from the UI or from the domain layer).

This pattern involves generic types with protocols and structs with multiple layer of inheritance and it is very hard to make work.


This is what I have in Kotlin for a HomeScreen:

// Events
interface UiEvent

sealed interface HomeUiEvent : UiEvent {
    data object Start : HomeUiEvent
    data object Retry: HomeUiEvent
}

// Events Handler
fun interface UiEventHandler<Event : UiEvent> {

    fun handleEvent(event: Event)
}

fun interface HomeUiEventHandler : UiEventHandler<HomeUiEvent>

sealed interface UiActionWithEventHandler<Event : UiEvent, Handler : UiEventHandler<Event>> {

    val handler: Handler
}

Since user can perform action that triggers a UI event, I have created the following:

sealed interface HomeUiAction : UiActionWithEventHandler<HomeUiEvent, HomeUiEventHandler> {
    data class Retry internal constructor(
        override val handler: HomeUiEventHandler,
    ) : HomeUiAction {

        fun trigger() = handler.handleEvent(event = HomeUiEvent.Retry)
    }
} 

I tried adapting it in Swift, but to no avail:

protocol UiEvent {}

protocol UiEventHandler{
    associatedtype Event: UiEvent
    func handleUiEvent(uiEvent: Event)
}

enum HomeUiEvents: UiEvent {
    case start
    case retry
}

// Ensures the HomeUiEventHandler only consumes home-related UiEvents
protocol HomeUiEventHandler : UiEventHandler where Event == HomeUiEvents {}

// 2 generic types: the type of UI event and the type of handler to consume them
protocol UiActionWithEventHandler {
    associatedtype Event: UiEvent
    associatedtype Handler: UiEventHandler
    
    var handler: Handler { get }
}

// Ensures all home-related UI actions triggers only home-related UI events to be dispatched to a reference to a UI handler capable of consuming such UI events
protocol HomeUiAction: UiActionWithEventHandler {
    associatedtype Event = HomeUiEvents
    associatedtype Handler = HomeUiEventHandler
}

struct RetryHomeUiAction: HomeUiAction {

    private(set) var handler: any HomeUiEventHandler

    init(handler: any HomeUiEventHandler) {
        self.handler = handler
    }

    func trigger() {
        handler.handleUiEvent(uiEvent: .retry)
    }
}

The IDE complains for RetryHomeUiAction:

Type 'RetryHomeUiAction' does not conform to protocol 'HomeUiAction' Default type 'any HomeUiEventHandler' for associated type 'Handler' (from protocol 'HomeUiAction') does not conform to 'UiEventHandler'

Since project & targets uses Swift 6+ I also tried with primary associated alongside closure-type expression but to no avail as well (some error saying "requires protocol with associated types"?)

Where am I doing things wrong ? Thanks for the help !



Solution

  • The HomeUiEvent protocol is unnecessary. Once you remove that and use the concrete HomeUiEvents enum.

    Then, UiEventHandler should use primary associated types instead of having derived protocols inheriting from it. The same can be said for UiActionWithEventHandler, but in the code you have shown, this is not necessary.

    protocol UiEventHandler<Event> {
        associatedtype Event: UiEvent
        func handleUiEvent(uiEvent: Event)
    }
    
    protocol UiActionWithEventHandler<Event> {
        associatedtype Event: UiEvent
        
        var handler: any UiEventHandler<Event> { get }
    }
    
    // use these typealiases if you like...
    typealias HomeUiAction = UiActionWithEventHandler<HomeUiEvents>
    
    typealias HomeUiEventHandler = UiEventHandler<HomeUiEvents>
    

    Now this compiles:

    // unfortunately you can't use 'HomeUiAction' in the inheritance clause here,
    // which is an argument for keeping 'HomeUiAction' as a separate protocol
    struct RetryHomeUiAction: UiActionWithEventHandler {
        private(set) var handler: any HomeUiEventHandler
        
        init(handler: any HomeUiEventHandler) {
            self.handler = handler
        }
        
        func trigger() {
            handler.handleUiEvent(uiEvent: HomeUiEvents.retry)
        }
    }
    

    P.S. Instead of using a primary associated type, UiEventHandler can also be turned into a function type instead,

    typealias UiEventHandler<Event: UiEvent> = (Event) -> Void
    typealias HomeUiEventHandler = UiEventHandler<HomeUiEvents>