swiftswift-concurrencyswift6sendable

Swift 6 async await using services and domain models


I am coming from the C#/.NET world where async await feels quite intuitive and easy to use. Now, I started learning Swift to build my own application(s).

I am trying to build an app using testable components and an MVVM architecture (because of obvious reasons, having a .NET background).

I want one of my protocol to return a domain model asynchronously (for now this is just mocked). When I called this protocol, I got a compilation error,

'Non-sendable type 'Availability' returned by implicitly asynchronous call to nonisolated function...'

public struct Availability{
    public let availableDates: [Date]

    public init(availableDates: [Date]){
        self.availableDates = availableDates
    }
}

public class DIContainer {
    public let IProviderService: IProviderService

    public init(providerService: IProviderService) {
        self.IProviderService = providerService
    }
}

public protocol IProviderService {
    func getAvailableAppointments() async throws -> Availability
}

@Observable
@MainActor
public class AvailableTimeViewModel {
    private let DIContainer: DIContainer
    public var availability: Availability = Availability(dateIntervals: [])

    public init(DIContainer: DIContainer) {
        self.DIContainer = DIContainer
    }

    public func getAvailableAppointments() async {
        do {
            self.availability = try await DIContainer.IProviderService.getAvailableAppointments()
        } catch {
            print("Failed to fetch availability: \(error)")
        }
    }
}

I could fix this by making my domain model and the IProviderService to implement the Sendable protocol.

I have seen an answer here, but it still feels a little bit off to me, that in order to be able to use async await, I need my protocol services and domain models to implement the Sendable protocol.

Do I have to make all of my domain models and protocols serving data asynchronously implement the Sendable protocol or is there a better way to handle this? Are there any other best practices that I should know of?


Solution

  • A few observations:

    1. In Swift 6, model types do not necessarily need to conform to Sendable.

      In WWDC 2022 video, Eliminate data races using Swift Concurrency, they walk us through the use of Sendable types as the best way to declare an type that can safely cross actor boundaries. So, prior to Swift 6, this would have been the answer: Make your model types conform to Sendable.

      Swift 6 offers a potential refinement to this process, namely region-based isolation. The idea is that if getAvailableAppointments simply extracts/gets these model objects and returns them, but does not have any other interaction with these objects after that, we can declaring the method as sending:

      public protocol IProviderService: Sendable {
          func getAvailableAppointments() async throws -> sending Availability
      }
      

      And when you do this, the compiler can reason about this at the call point, resolving any potential warnings about returning this non-Sendable type across actor boundaries, because sending assures us that getAvailableAppointments will not do anything with these objects after returning them.

    2. Your provider service may well need to be sendable.

      To do that, you can either:

      • Make it Sendable with manual synchronization of any mutable state;

      • Isolate it to a global actor; or

      • Just make it an actor, itself.

    3. You might consider making your model object Sendable, anyway.

      I notice that your model object, Availability, is an immutable value type. If that is the case in your actual code, you might just declare it to be Sendable, anyway. Immutable value types can safely be transferred across actor boundaries, and this gets you out of the weeds of following the region-based isolation rules.