Многопоточность по шагам: Чтение из хранилища

Это перевод статьи Concurrency Step-by-Step: Reading from Storage

Тема, которая снова и снова возникает в связи с многопоточностью (concurrency) в Swift, — это попытка «сделать компилятор довольным». Вы просто хотите, чтобы глупые ошибки исчезли. Пытаясь сделать это, вы натыкаетесь на множество вещей, таких как Sendable или @preconcurrency. Вы даже можете начать менять класс class на актор actor , и непонятно, насколько это может отличаться, но это даже то же самое количество символов. Поэтому вы просто начинаете бросаться синтаксисом в проблему. Это понятно!

Иногда это может даже работать в краткосрочной перспективе. Но этот путь обычно приводит к разочарованию и гневу. Часто он приводит к чрезвычайно сложным проектам, которые просто приводят к еще большим проблемам.

Добро пожаловать во вторую часть «Concurrency Swift шаг за шагом». Первая часть «Concurrency Swift шаг за шагом» находится здесь. Цель этих постов— проработать общую задачу, чтобы помочь сформировать реальное понимание того, что происходит. В прошлый раз мы рассматривали сетевой запрос. На этот раз мы загрузим модель из хранилища данных.

Краткие заметки

Я проигнорирую обработку ошибок, чтобы сосредоточиться на многопоточности.
Я не очень хорош в SwiftUI. Нам потребуется Xcode 16 или более поздняя версия.
Мне было очень трудно придумать пример, который был бы одновременно простым и иллюстрировал проблему. Я думаю, что локальное хранилище будет работать хорошо, но нам придется сделать его довольно надуманным. Я не думаю, что это действительно уведет нас от каких-либо идей. Но я все равно хочу это подчеркнуть, потому что идея «хранилища данных» здесь не будет похожа на SwiftData, CoreData или другие вещи, которые может использовать реальное приложение.

Кроме того, этот пост строится на темах, обсуждавшихся в предыдущем посте. Некоторые из этих вещей будет сложнее понять, если вы не знакомы с содержанием предыдущего поста.

Расставляем детали по местам

Итак, начнем с определения интерфейса нашей системы хранения данных.

class DataModel {
	let name: String

	init(name: String) {
		self.name = name
	}
}

class Store {
	func loadModel(named name: String) async -> DataModel {
		DataModel(name: name)
	}
}

Я же говорил, что это будет надуманно!
Есть только ТИП DataModel для хранения простого значения name, а также Store, который «загружает» для нас модели. Ни один из них не делает ничего полезного. Но на самом деле нас интересуют только ТИПы и их интерфейсы.

Теперь нам нужен SwiftUI View, чтобы связать все это воедино.

struct ContentView: View {
	@State private var store = Store()
	@State private var name: String = "---"

	var body: some View {
		Text("hello \(name)!")
			.task {
				self.name = await store.loadModel(named: "friends").name
			}
	}
}

Этот фрагмент кода должен очень удобно поместиться на одном экране. Неплохо!

Отступление: система ТИПов

Я вставил небольшой комментарий выше, который заслуживает большего внимания.
Но на самом деле нас интересуют только ТИПы и их интерфейсы.
Это важно и довольно нетривиально! 

Читать далее

Многопоточность по шагам:  Сетевой запрос

Это перевод статьи Concurrency Step-by-Step: A Network Request

Это было ошибкой, что я не уделял больше внимания людям, которые только начинают. Я попытаюсь исправить это, углубившись в некоторые основы. Давайте шаг за шагом рассмотрим сетевой запрос с помощью SwiftUI.

Предисловие

Несколько быстрых заметок. Во-первых, я практически опустил всю обработку ошибок. Я сделал это, чтобы сосредоточиться на теме многопоточности. Я также не особо искушенный разработчик SwiftUI, поэтому здесь могут быть некоторые неоптимальные шаблоны.

Важно, что этот пост был написан для Xcode 16. Если вы используете более раннюю версию, некоторые вещи будут работать не так.

Расставляем все по местам

Давайте рассмотрим очень простую программу SwiftUI, которая загружает что-то из сети. Мне нужно было найти бесплатный API для использования, и я остановился на Robohash. Это восхитительное сочетание простого, интересного и необычного.

Поскольку наши данные будут загружаться из сети, нам нужно обработать случай, когда нам нечего отображать. Начнем с небольшого View, которое может обрабатывать Optional изображение cgImage.

struct LoadedImageView: View {
	let cgImage: CGImage?
	
	var body: some View {
		if let cgImage {
			Image(cgImage, scale: 1.0, label: Text("Robot"))
		} else {
			Text("no robot yet")
		}
	}
}

Я использовал CGImage здесь, поэтому этот код может работать без изменений на всех платформах Apple.

Теперь мы можем перейти к более интересным вещам. Давайте создадим View, которое фактически загружает некоторые данные из сети.

struct RobotView: View {
	@State private var cgImage: CGImage?

	var body: some View {
		LoadedImageView(cgImage: cgImage)
			.onAppear {
				loadImageWithGCD()
			}
	}
	
	private func loadImageWithGCD() {
		let request = URLRequest(url: 
                 URL(string: "https://robohash.org/hash-this-text.png")!)

		let dataTask = URLSession.shared.dataTask(with: request) { data, _, _ in
			guard let data else { return }

			DispatchQueue.main.async {
				let provider = CGDataProvider(data: data as CFData)!

				self.cgImage = CGImage(
					pngDataProviderSource: provider,
					decode: nil,
					shouldInterpolate: false,
					intent: .defaultIntent
				)
			}
		}

		dataTask.resume()
	}
}

Я использовал GCD (Grand Central Dispatch). Надеюсь, это выглядит очень привычно, даже если вы вообще не разбираетесь в многопоточности. Но я все равно хочу отметить, что я использую здесь режим компиляции использования языка Swift 6, и этот код компилируется без ошибок.

Читать далее