Skip to content

Commit 7f307d1

Browse files
authored
Merge pull request #4 from sebsto/sebsto/task
Add more details about `TaskGroup` and how to use the `task` modifier
2 parents 9c9c249 + 8ffbd48 commit 7f307d1

File tree

10 files changed

+858
-133
lines changed

10 files changed

+858
-133
lines changed

src/ar/index.md

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -468,9 +468,11 @@ class ViewModel {
468468
```
469469

470470
<div class="warning">
471-
<h4>Task.detached عادةً خاطئ</h4>
471+
<h4>Task و Task.detached نمط سيء</h4>
472472

473-
فريق Swift يوصي بـ [Task.detached كملاذ أخير](https://forums.swift.org/t/revisiting-when-to-use-task-detached/57929). لا ترث الأولوية، القيم المحلية للمهمة، أو سياق الـ actor. معظم الوقت، `Task` العادية هي ما تريده. إذا كنت تحتاج عملاً مكثفاً على المعالج بعيداً عن الـ main actor، ضع علامة `@concurrent` على الدالة بدلاً من ذلك.
473+
المهام التي تجدولها بـ `Task { ... }` غير مُدارة. لا توجد طريقة لإلغائها أو معرفة متى تنتهي، إن انتهت أصلاً. لا توجد طريقة للوصول لقيمة الإرجاع أو معرفة إذا واجهت خطأ. في غالبية الحالات، سيكون من الأفضل استخدام مهام مُدارة بواسطة `.task` أو `TaskGroup`، [كما هو موضح في قسم "الأخطاء الشائعة"](#managedtasks).
474+
475+
[Task.detached يجب أن يكون ملاذك الأخير](https://forums.swift.org/t/revisiting-when-to-use-task-detached/57929). المهام المنفصلة لا ترث الأولوية، القيم المحلية للمهمة، أو سياق الـ actor. إذا كنت تحتاج عملاً مكثفاً على المعالج بعيداً عن الـ main actor، ضع علامة `@concurrent` على الدالة بدلاً من ذلك.
474476
</div>
475477

476478
<div class="analogy">
@@ -609,24 +611,93 @@ func badIdea() async {
609611

610612
تجمع الخيوط التعاوني في Swift له خيوط محدودة. حجب واحد بـ `DispatchSemaphore` أو `DispatchGroup.wait()` أو استدعاءات مماثلة يمكن أن يسبب جموداً. إذا كنت تحتاج ربط كود متزامن وغير متزامن، استخدم `async let` أو أعد الهيكلة للبقاء غير متزامن بالكامل.
611613

612-
### إنشاء Tasks غير ضرورية
614+
<div id="managedtasks">
615+
616+
### إنشاء مهام غير مُدارة
617+
618+
المهام التي تنشئها يدوياً بـ `Task { ... }` أو `Task.detached { ... }` غير مُدارة. بعد إنشاء مهام غير مُدارة، لا يمكنك التحكم بها. لا يمكنك إلغاؤها إذا تم إلغاء المهمة التي بدأتها منها. لا يمكنك معرفة إذا أنهت عملها، إذا ألقت خطأ، أو جمع قيمة الإرجاع. بدء مثل هذه المهمة يشبه رمي زجاجة في البحر آملاً أن توصل رسالتها لوجهتها، دون رؤية تلك الزجاجة مرة أخرى.
619+
620+
<div class="analogy">
621+
<h4>مبنى المكاتب</h4>
622+
623+
`Task` يشبه تكليف موظف بعمل. الموظف يتعامل مع الطلب (بما في ذلك الانتظار لمكاتب أخرى) بينما تستمر في عملك الفوري.
624+
625+
بعد إرسال العمل للموظف، ليس لديك وسيلة للتواصل معها. لا يمكنك إخبارها بالتوقف عن العمل أو معرفة إذا انتهت وما كانت نتيجة ذلك العمل.
626+
627+
ما تريده فعلاً هو إعطاء الموظف جهاز اتصال لاسلكي للتواصل معها أثناء تعاملها مع الطلب. بجهاز الاتصال، يمكنك إخبارها بالتوقف، أو يمكنها إخبارك عندما تواجه خطأ، أو يمكنها الإبلاغ عن نتيجة الطلب الذي أعطيتها إياه.
628+
</div>
629+
630+
بدلاً من إنشاء مهام غير مُدارة، استخدم تزامن Swift للحفاظ على التحكم في المهام الفرعية التي تنشئها. استخدم `TaskGroup` لإدارة (مجموعة من) المهام الفرعية. Swift توفر عدة دوال `withTaskGroup() { group in ... }` للمساعدة في إنشاء مجموعات المهام.
613631

614632
```swift
615-
// إنشاء Task غير ضروري
616-
func fetchAll() async {
617-
Task { await fetchUsers() }
618-
Task { await fetchPosts() }
633+
func doWork() async {
634+
635+
// هذا سيرجع عندما ترجع جميع المهام الفرعية، تلقي خطأ، أو يتم إلغاؤها
636+
let result = try await withThrowingTaskGroup() { group in
637+
group.addTask {
638+
try await self.performAsyncOperation1()
639+
}
640+
group.addTask {
641+
try await self.performAsyncOperation2()
642+
}
643+
// انتظر واجمع نتائج المهام هنا
644+
}
619645
}
620646

621-
// أفضل - استخدم التزامن المنظم
622-
func fetchAll() async {
623-
async let users = fetchUsers()
624-
async let posts = fetchPosts()
625-
await (users, posts)
647+
func performAsyncOperation1() async throws -> Int {
648+
return 1
649+
}
650+
func performAsyncOperation2() async throws -> Int {
651+
return 2
652+
}
653+
```
654+
655+
لجمع نتائج المهام الفرعية للمجموعة، يمكنك استخدام حلقة for-await-in:
656+
657+
```swift
658+
var sum = 0
659+
for await result in group {
660+
sum += result
626661
}
662+
// sum == 3
627663
```
628664

629-
إذا كنت بالفعل في سياق async، فضّل التزامن المنظم (`async let`، `TaskGroup`) على إنشاء `Task`s غير منظمة. التزامن المنظم يتعامل مع الإلغاء تلقائياً ويجعل الكود أسهل للفهم.
665+
يمكنك معرفة المزيد عن [TaskGroup](https://developer.apple.com/documentation/swift/taskgroup) في توثيق Swift.
666+
667+
#### ملاحظة حول المهام وSwiftUI.
668+
669+
عند كتابة واجهة مستخدم، غالباً تريد بدء مهام غير متزامنة من سياق متزامن. مثلاً، تريد تحميل صورة بشكل غير متزامن كاستجابة للمس عنصر واجهة. بدء مهام غير متزامنة من سياق متزامن غير ممكن في Swift. لهذا ترى حلولاً تتضمن `Task { ... }`، مما يُدخل مهاماً غير مُدارة.
670+
671+
لا يمكنك استخدام `TaskGroup` من مُعدّل SwiftUI متزامن لأن `withTaskGroup()` دالة async أيضاً وكذلك دوالها المرتبطة.
672+
673+
كبديل، SwiftUI يوفر مُعدّلاً غير متزامن يمكنك استخدامه لبدء عمليات غير متزامنة. المُعدّل `.task { }`، الذي ذكرناه سابقاً، يقبل دالة `() async -> Void`، مثالية لاستدعاء دوال `async` أخرى. متاح على كل `View`. يُفعّل قبل ظهور الواجهة والمهام التي ينشئها مُدارة ومرتبطة بدورة حياة الواجهة، مما يعني أن المهام تُلغى عندما تختفي الواجهة.
674+
675+
عودة لمثال اللمس-لتحميل-صورة: بدلاً من إنشاء مهمة غير مُدارة لاستدعاء دالة `loadImage()` غير المتزامنة من دالة `.onTap() { ... }` المتزامنة، يمكنك تبديل علَم عند لفتة اللمس واستخدام المُعدّل `task(id:)` لتحميل الصور بشكل غير متزامن عندما تتغير قيمة `id` (العلَم).
676+
677+
هنا مثال:
678+
679+
```swift
680+
struct ContentView: View {
681+
682+
@State private var shouldLoadImage = false
683+
684+
var body: some View {
685+
Button("اضغط هنا!") {
686+
// بدّل العلَم
687+
shouldLoadImage = !shouldLoadImage
688+
}
689+
// الـ View تدير المهمة الفرعية
690+
// تبدأ قبل عرض الواجهة
691+
// وتتوقف عندما تختفي الواجهة
692+
.task(id: shouldLoadImage) {
693+
// عندما تتغير قيمة العلَم، SwiftUI يعيد تشغيل المهمة
694+
guard shouldLoadImage else { return }
695+
await loadImage()
696+
}
697+
}
698+
}
699+
```
700+
</div>
630701

631702
</div>
632703
</section>

src/en/index.md

Lines changed: 102 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -466,9 +466,11 @@ class ViewModel {
466466
```
467467

468468
<div class="warning">
469-
<h4>Task.detached is usually wrong</h4>
469+
<h4>Task and Task.detached are an anti-pattern</h4>
470470

471-
The Swift team recommends [Task.detached as a last resort](https://forums.swift.org/t/revisiting-when-to-use-task-detached/57929). It doesn't inherit priority, task-local values, or actor context. Most of the time, regular `Task` is what you want. If you need CPU-intensive work off the main actor, mark the function `@concurrent` instead.
471+
The tasks you schedule with `Task { ... }` are not managed. There is no way for you to cancel them or to know when they finish, if ever. There is no way to access their return value or to know if they encounter an error. In the majority of the cases, it will be better to use tasks managed by a `.task` or `TaskGroup`, [as explained in the "Common mistakes" section](#managedtasks).
472+
473+
[Task.detached should be your last resort](https://forums.swift.org/t/revisiting-when-to-use-task-detached/57929). Detached tasks don't inherit priority, task-local values, or actor context. If you need CPU-intensive work off the main actor, mark the function `@concurrent` instead.
472474
</div>
473475

474476
<div class="analogy">
@@ -607,24 +609,93 @@ func badIdea() async {
607609

608610
Swift's cooperative thread pool has limited threads. Blocking one with `DispatchSemaphore`, `DispatchGroup.wait()`, or similar calls can cause deadlocks. If you need to bridge sync and async code, use `async let` or restructure to stay fully async.
609611

610-
### Creating unnecessary Tasks
612+
<div id="managedtasks">
613+
614+
### Create unmanaged tasks
615+
616+
Tasks that you create manually with `Task { ... }` or `Task.detached { ... }` are not managed. After you create unmanaged tasks, you can't control them. You can't cancel them if the task from which you started it is cancelled. You can't know if they finished their work, if they threw an error, or collect their return value. Starting such a task is like throwing a bottle into the sea and hoping it will deliver its message to its destination, without ever seeing that bottle again.
617+
618+
<div class="analogy">
619+
<h4>The Office Building</h4>
620+
621+
A `Task` is like assigning work to an employee. The employee handles the request (including waiting for other offices) while you continue with your immediate work.
622+
623+
After you dispatch work to the employee, you have no means to communicate with her. You can't tell her to stop the work or know if she finished and what the result of that work was.
624+
625+
What you actually want is to give the employee a walkie-talkie to communicate with her while she handles the request. With the walkie-talkie, you can tell her to stop, or she can tell you when she encounters an error, or she can report the result of the request you gave her.
626+
</div>
627+
628+
Instead of creating unmanaged tasks, use Swift concurrency to keep control of the subtasks you create. Use `TaskGroup` to manage a (group of) subtask(s). Swift provides a couple of `withTaskGroup() { group in ... }` functions to help create task groups.
611629

612630
```swift
613-
// Unnecessary Task creation
614-
func fetchAll() async {
615-
Task { await fetchUsers() }
616-
Task { await fetchPosts() }
631+
func doWork() async {
632+
633+
// this will return when all subtasks return, throw an error, or are cancelled
634+
let result = try await withThrowingTaskGroup() { group in
635+
group.addTask {
636+
try await self.performAsyncOperation1()
637+
}
638+
group.addTask {
639+
try await self.performAsyncOperation2()
640+
}
641+
// wait for and collect the results of the tasks here
642+
}
617643
}
618644

619-
// Better - use structured concurrency
620-
func fetchAll() async {
621-
async let users = fetchUsers()
622-
async let posts = fetchPosts()
623-
await (users, posts)
645+
func performAsyncOperation1() async throws -> Int {
646+
return 1
647+
}
648+
func performAsyncOperation2() async throws -> Int {
649+
return 2
624650
}
625651
```
626652

627-
If you're already in an async context, prefer structured concurrency (`async let`, `TaskGroup`) over creating unstructured `Task`s. Structured concurrency handles cancellation automatically and makes the code easier to reason about.
653+
To collect the results of the group's child tasks, you can use a for-await-in loop:
654+
655+
```swift
656+
var sum = 0
657+
for await result in group {
658+
sum += result
659+
}
660+
// sum == 3
661+
```
662+
663+
You can learn more about [TaskGroup](https://developer.apple.com/documentation/swift/taskgroup) in the Swift documentation.
664+
665+
#### Note about Tasks and SwiftUI.
666+
667+
When writing a UI, you often want to start asynchronous tasks from a synchronous context. For example, you want to asynchronously load an image as a response to a UI element touch. Starting asynchronous tasks from a synchronous context is not possible in Swift. This is why you see solutions involving `Task { ... }`, which introduces unmanaged tasks.
668+
669+
You can't use `TaskGroup` from a synchronous SwiftUI modifier because `withTaskGroup()` is an async function too and so are its related functions.
670+
671+
As an alternative, SwiftUI offers an asynchronous modifier that you can use to start asynchronous operations. The `.task { }` modifier, which we already mentioned, accepts a `() async -> Void` function, ideal for calling other `async` functions. It is available on every `View`. It is triggered before the view appears and the tasks it creates are managed and bound to the lifecycle of the view, meaning the tasks are cancelled when the view disappears.
672+
673+
Back to the tap-to-load-an-image example: instead of creating an unmanaged task to call an asynchronous `loadImage()` function from a synchronous `.onTap() { ... }` function, you can toggle a flag on the tap gesture and use the `task(id:)` modifier to asynchronoulsy load images when the `id` (the flag) value changes.
674+
675+
Here is a example:
676+
677+
```swift
678+
struct ContentView: View {
679+
680+
@State private var shouldLoadImage = false
681+
682+
var body: some View {
683+
Button("Click Me !") {
684+
// toggle the flag
685+
shouldLoadImage = !shouldLoadImage
686+
}
687+
// the View manages the subtask
688+
// it starts before the view is displayed
689+
// and stops when the view is hidden
690+
.task(id: shouldLoadImage) {
691+
// when the flag value changes, SwiftUI restarts the task
692+
guard shouldLoadImage else { return }
693+
await loadImage()
694+
}
695+
}
696+
}
697+
```
698+
</div>
628699

629700
</div>
630701
</section>
@@ -702,8 +773,9 @@ Choose your agent and run the commands below:
702773
<div class="code-tabs">
703774
<div class="code-tabs-nav">
704775
<button class="active">Claude Code</button>
705-
<button>Codex</button>
706776
<button>Amp</button>
777+
<button>Codex</button>
778+
<button>Kiro</button>
707779
<button>OpenCode</button>
708780
</div>
709781
<div class="code-tab-content active">
@@ -720,6 +792,14 @@ curl -o .claude/skills/swift-concurrency/SKILL.md https://fuckingapproachableswi
720792
</div>
721793
<div class="code-tab-content">
722794

795+
```bash
796+
# Project instructions (recommended)
797+
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
798+
```
799+
800+
</div>
801+
<div class="code-tab-content">
802+
723803
```bash
724804
# Global instructions (all your projects)
725805
curl -o ~/.codex/AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
@@ -728,14 +808,20 @@ curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
728808
```
729809

730810
</div>
811+
731812
<div class="code-tab-content">
732813

733814
```bash
734-
# Project instructions (recommended)
735-
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
815+
# Global rules (all your projects)
816+
mkdir -p ~/.kiro/steering
817+
curl -o ~/.kiro/steering/swift-concurrency.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
818+
# Project rules (just this project)
819+
mkdir -p .kiro/steering
820+
curl -o .kiro/steering/swift-concurrency.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
736821
```
737822

738823
</div>
824+
739825
<div class="code-tab-content">
740826

741827
```bash

0 commit comments

Comments
 (0)