I am looking to migrate my below file into swift 6.
There is one way to forcefully use @MainActor everywhere(ViewModels, protocols etc)
another is use uncheck-sendable or use @preconcurrency.
But I am not looking to silence the warning I am looking to solve in proper thread mechanism.
My code - Note use swift 6 from build settings to get error
import SwiftUI
// MARK: - Model
struct UserModel: Identifiable, Decodable {
let id: Int
let name: String
}
// MARK: - Protocol for User Service
protocol UserServiceProtocol {
func fetchUsers() async throws -> [UserModel]
}
// MARK: - Real API Implementation
class UserService: UserServiceProtocol {
func fetchUsers() async throws -> [UserModel] {
let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([UserModel].self, from: data)
}
}
// MARK: - ViewModel Protocol
protocol UserListViewModelProtocol: ObservableObject {
var users: [UserModel] { get }
var isLoading: Bool { get }
var error: String? { get }
func loadUsers() async
}
// MARK: - ViewModel
class UserListViewModel: UserListViewModelProtocol {
@Published private(set) var users: [UserModel] = []
@Published private(set) var isLoading = false
@Published private(set) var error: String? = nil
private let userService: UserServiceProtocol
init(userService: UserServiceProtocol = UserService()) {
self.userService = userService
}
func loadUsers() async {
isLoading = true
error = nil
do {
users = try await userService.fetchUsers()
} catch {
self.error = error.localizedDescription
}
isLoading = false
}
}
// MARK: - SwiftUI View
struct UserListView<VM: UserListViewModelProtocol>: View {
@StateObject private var viewModel: VM
init(viewModel: @autoclosure @escaping () -> VM) {
_viewModel = StateObject(wrappedValue: viewModel())
}
var body: some View {
NavigationView {
Group {
if viewModel.isLoading {
ProgressView("Loading...")
} else if let error = viewModel.error {
Text("❌ \(error)")
.foregroundColor(.red)
} else {
List(viewModel.users) { user in
Text(user.name)
}
}
}
.navigationTitle("Users")
}
.task {
await viewModel.loadUsers()//<<Error --- Sending 'self.viewModel' risks causing data races
}
}
}
// MARK: - Mock for Preview & Tests
class MockUserService: UserServiceProtocol {
func fetchUsers() async throws -> [UserModel] {
return [
UserModel(id: 1, name: "Mock Alice"),
UserModel(id: 2, name: "Mock Bob")
]
}
}
// MARK: - Preview
struct UserListView_Previews: PreviewProvider {
static var previews: some View {
UserListView(viewModel: UserListViewModel(userService: MockUserService()))
}
}
Since all properties of UserListViewModelProtocol
need to bind to UI components such as ProgressView
, Text
, List
. So, these properties should isolated to @MainActor
.
@MainActor // 👈
protocol UserListViewModelProtocol: ObservableObject {
...
}
Now, the warning on the .task
is resolved.
.task {
await viewModel.loadUsers() //<- ✅
}
However, your userService
is a normal protocol and is not isolated to any actor. It has to be somehow, safety within isolation context. And Sendable
is fit in this scenario.
protocol UserServiceProtocol: Sendable { // 👈
...
}
And the last one is the concrete type that conforms to UserServiceProtocol
, which must be Sendable
.
final // 👈
class UserService: UserServiceProtocol {
...
}
Finally, you will have:
@MainActor
isolated, ensures code runs on the main thread, for UI safety.await
)