У меня есть несколько служб, реализованных через протоколы, чтобы иметь возможность внедрить фиктивную службу при инициализации ViewModels, и после включения строгой проверки параллелизма у меня появляется много предупреждений "Capture of 'self' with non-sendable type "ViewModelType" in 'async let' binding"
Вот минимальный воспроизводимый пример:
class ViewController: UIViewController {
let viewModel: UserProfileViewModel
init(service: UserProfileServiceProtocol) {
self.viewModel = UserProfileViewModel(service: service)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
getUserProfileData()
// Do any additional setup after loading the view.
}
func getUserProfileData() {
Task {
do {
try await viewModel.getProfileData()
// update UI
} catch {
print(error)
}
}
}
}
struct UserModel {
var userID: String
var username: String
var profilePictureURL: URL?
var profilePhoto: UIImage?
}
class UserProfileViewModel {
private let service: any UserProfileServiceProtocol
var user: UserModel?
var postsCount: Int?
var followersCount: Int?
var followedUsersCount: Int?
init(service: UserProfileServiceProtocol) {
self.service = service
}
func getProfileData() async throws {
async let user = service.getUserData()
async let followersCount = service.getFollowersCount()
async let followedUsersCount = service.getFollowingCount()
async let postsCount = service.getPostsCount()
self.user = try await user
self.followersCount = try await followersCount
self.followedUsersCount = try await followedUsersCount
self.postsCount = try await postsCount
}
}
protocol UserProfileServiceProtocol {
var followService: FollowSystemProtocol { get }
var userPostsService: UserPostsServiceProtocol { get }
var userDataService: UserDataServiceProtocol { get }
func getFollowersCount() async throws -> Int
func getFollowingCount() async throws -> Int
func getPostsCount() async throws -> Int
func getUserData() async throws -> UserModel
}
protocol FollowSystemProtocol {
func getFollowersNumber(for uid: String) async throws -> Int
func getFollowingNumber(for uid: String) async throws -> Int
}
protocol UserPostsServiceProtocol {
func getPostCount(for userID: String) async throws -> Int
}
protocol UserDataServiceProtocol {
func getUser(for userID: String) async throws -> UserModel
}
class UserService: UserProfileServiceProtocol {
let userID: String
let followService: FollowSystemProtocol
let userPostsService: UserPostsServiceProtocol
let userDataService: UserDataServiceProtocol
init(userID: String, followService: FollowSystemProtocol, userPostsService: UserPostsServiceProtocol, userDataService: UserDataServiceProtocol) {
self.userID = userID
self.followService = followService
self.userPostsService = userPostsService
self.userDataService = userDataService
}
func getFollowersCount() async throws -> Int {
let followersCount = try await followService.getFollowersNumber(for: userID)
return followersCount
}
func getFollowingCount() async throws -> Int {
let followersCount = try await followService.getFollowingNumber(for: userID)
return followersCount
}
func getPostsCount() async throws -> Int {
let postsCount = try await userPostsService.getPostCount(for: userID)
return postsCount
}
func getUserData() async throws -> UserModel {
let user = try await userDataService.getUser(for: userID)
return user
}
}
class FollowSystemService: FollowSystemProtocol {
func getFollowersNumber(for uid: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
return 5
}
func getFollowingNumber(for uid: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
return 19
}
}
class UserPostsService: UserPostsServiceProtocol {
func getPostCount(for userID: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
return 27
}
}
class UserProfileService: UserDataServiceProtocol {
func getUser(for userID: String) async throws -> UserModel {
try await Task.sleep(for: .seconds(1))
return UserModel(userID: "testUser_01", username: "testUser", profilePictureURL: nil)
}
}
У меня недостаточно опыта, чтобы судить о том, как правильно решить эту проблему, поэтому я просто нервно пытаюсь найти какую-либо информацию по этому поводу, но безуспешно.
Должен ли я привести протоколы обслуживания в соответствие с отправляемыми? Это вообще обычная практика? Или мне следует сделать что-то совершенно другое, чтобы это исправить?
В настоящее время ваша модель представления не соответствует Sendable
. Более глубокая проблема заключается в том, что он не является потокобезопасным. Поскольку модель представления не изолирована от какого-либо конкретного актера, все ее async
методы (в силу SE-0338) выполняются на «обобщенном исполнителе» (т. е. не в основном потоке). Итак, у вас есть свойства обновления фонового потока, к которым контроллер представления обращается из основного потока. Если вы установите для параметра сборки «Strict Concurrency Checking» значение «Complete», вы увидите больше предупреждений об отсутствии потокобезопасности.
Модель представления должна, по крайней мере, изолировать свойства, к которым представление имеет доступ, от главного актера. Проще говоря, мы часто изолировали всю модель представления от главного актера. Вся задача модели представления заключается в поддержке представления (которое находится на главном актере), поэтому имеет смысл также изолировать всю модель представления от главного актера:
@MainActor
class UserProfileViewModel {…}
Что касается услуг, да, вам тоже захочется составить эти протоколы Sendable
. Компилятор (особенно с настройкой сборки «Строгая проверка параллелизма» «Завершен») предупредит вас, что объект Sendable
не может иметь свойства, отличные от Sendable
. То есть объект не является потокобезопасным, если его свойства не являются потокобезопасными. Итак, составляйте протоколы Sendable
:
protocol UserPostsServiceProtocol: Sendable {
func getPostCount(for userID: String) async throws -> Int
}
А потом, конечно же, делайте и свои реализации Sendable
. Например, если у сервиса нет изменяемых свойств, вы можете просто объявить его как final
. (Он унаследует Sendable
из протокола). Возможно:
final class UserPostsService: UserPostsServiceProtocol {
func getPostCount(for userID: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
return 27
}
}
Но если у службы есть какое-то внутреннее изменяемое состояние, потребуется некоторая синхронизация, чтобы избежать гонок за данными. Или вместо добавления собственной синхронизации вручную проще изолировать всю службу. Вы можете либо изолировать его от основного актера (что менее привлекательно для сервиса, чем для модели представления), либо просто сделать его отдельным актером:
actor UserPostsService: UserPostsServiceProtocol {
private var value = 0
func getPostCount(for userID: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
value = 27
return value
}
}
Итак, собрав все это вместе, вы можете получить:
class ViewController: UIViewController {
let viewModel: UserProfileViewModel
private var task: Task<Void, Error>?
init(service: UserProfileServiceProtocol) {
self.viewModel = UserProfileViewModel(service: service)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
getUserProfileData() // you might consider doing this in `viewDidAppear` … it depends upon whether this view presents other view controllers and whether you want it to re-fetch user profile data when it re-appears
}
// If you use unstructured concurrency, you are responsible for
// canceling the task whenever its results are no longer needed.
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
task?.cancel()
}
// So, make sure to save this unstructured concurrency `Task` in a property so you can cancel it when no longer needed
func getUserProfileData() {
task = Task {
do {
try await viewModel.getProfileData()
// update UI
} catch {
print(error)
}
}
}
}
struct UserModel {
let userID: String // this probably should be immutable
let username: String // this probably should be immutable
let profilePictureURL: URL? = nil // this probably should be immutable
var profilePhoto: UIImage? = nil
}
extension UserModel: Identifiable { // you might want to make this `Identifiable`
var id: String { userID }
}
@MainActor
class UserProfileViewModel {
private let service: any UserProfileServiceProtocol
var user: UserModel?
var postsCount: Int?
var followersCount: Int?
var followedUsersCount: Int?
init(service: UserProfileServiceProtocol) {
self.service = service
}
func getProfileData() async throws {
async let user = service.getUserData()
async let followersCount = service.getFollowersCount()
async let followedUsersCount = service.getFollowingCount()
async let postsCount = service.getPostsCount()
self.user = try await user
self.followersCount = try await followersCount
self.followedUsersCount = try await followedUsersCount
self.postsCount = try await postsCount
}
}
protocol UserProfileServiceProtocol: Sendable {
var followService: FollowSystemProtocol { get }
var userPostsService: UserPostsServiceProtocol { get }
var userDataService: UserDataServiceProtocol { get }
func getFollowersCount() async throws -> Int
func getFollowingCount() async throws -> Int
func getPostsCount() async throws -> Int
func getUserData() async throws -> UserModel
}
protocol FollowSystemProtocol: Sendable {
func getFollowersNumber(for uid: String) async throws -> Int
func getFollowingNumber(for uid: String) async throws -> Int
}
protocol UserPostsServiceProtocol: Sendable {
func getPostCount(for userID: String) async throws -> Int
}
protocol UserDataServiceProtocol: Sendable {
func getUser(for userID: String) async throws -> UserModel
}
final class UserService: UserProfileServiceProtocol {
let userID: String
let followService: FollowSystemProtocol
let userPostsService: UserPostsServiceProtocol
let userDataService: UserDataServiceProtocol
init(userID: String, followService: FollowSystemProtocol, userPostsService: UserPostsServiceProtocol, userDataService: UserDataServiceProtocol) {
self.userID = userID
self.followService = followService
self.userPostsService = userPostsService
self.userDataService = userDataService
}
func getFollowersCount() async throws -> Int {
let followersCount = try await followService.getFollowersNumber(for: userID)
return followersCount
}
func getFollowingCount() async throws -> Int {
let followersCount = try await followService.getFollowingNumber(for: userID)
return followersCount
}
func getPostsCount() async throws -> Int {
let postsCount = try await userPostsService.getPostCount(for: userID)
return postsCount
}
func getUserData() async throws -> UserModel {
let user = try await userDataService.getUser(for: userID)
return user
}
}
final class FollowSystemService: FollowSystemProtocol {
func getFollowersNumber(for uid: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
return 5
}
func getFollowingNumber(for uid: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
return 19
}
}
actor UserPostsService: UserPostsServiceProtocol {
var value = 0
func getPostCount(for userID: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
value = 27
return value
}
}
final class UserProfileService: UserDataServiceProtocol {
func getUser(for userID: String) async throws -> UserModel {
try await Task.sleep(for: .seconds(1))
return UserModel(userID: "testUser_01", username: "testUser")
}
}