Перейти к содержанию

1. Практический путь

Прежде чем переходить к практике, необходимо прочитать теоретический материал.

  1. Tasks
  2. Actors
  3. Sequence

Если у вас не получается решить задачу, например:

  • Ваш ответ не совпадает с ответом автора
  • Не проходят тесты
  • Вы не знаете что делать и с чего начать

Возвращайтесь к теории и внимательно изучайте теоретический материал. Не стесняйтесь гуглить или использовать chatGPT. Самое важное - понимание.

Задача 1

Что будет выведено на экран?

final class SomeViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        Task {
            print("Is Main Thread - \(Thread.isMainThread)")
            await foo()
            print("Is Main Thread - \(Thread.isMainThread)")
        }

        Task.detached {
            print("Is Main Thread - \(Thread.isMainThread)")
            await foo()
            print("Is Main Thread - \(Thread.isMainThread)")
        }
    }

    func foo() async {
        print("Is Main Thread - \(Thread.isMainThread)")
        try? await Task.sleep(for: .seconds(1))
    }
}

Задача 2

1) Что будет выведено на экран? 2) Отменилась ли задача? - ответ обосновать. Если да - то почему? Если нет - то почему и как сделать чтобы отменилась? 3) Что будет если вместо Task написать Task.detached?

let task = Task {
    print("Task 1 has started")
    await startLongTask()
    print("Task 1 has finished")
   
    print("Task 2 has started")
    await startLongTask()
    print("Task 2 has finished")
}

func startLongTask() async {
    try? await Task.sleep(for: .seconds(5))
}
task.cancel()

Задача 3

Что происходит в момент вызова await? как система работает с потоком? что такое continuation?

Задача 4

Предположим вы разрабатываете чат и когда пользователь входит в чат, вам необходимо раз в n секунд дёргать запрос, который оповещает, что пользователь находится всё ещё в чате. Вам необходимо реализовать сервис, удовлетворяющий протоколу AnyLookingService, который бы отправлял события на сервер до тех пор пока не вызовется метод stop

public protocol AnyLookingService: Sendable {
    func start()
    func stop()
}

/// Метод отправки реализовывать не нужно достаточно использовать функцию ниже
/// Если метод выкинул ошибку - необходимо прекратить дальнейшую отсылку событий
/// В ином случае необходимо продолжать отправку спустя n секунд
func sendLooking() async throws { ... }

Задача 5

Чем отличается withCheckedContinuation и withUnsafeContinuation ?

Задача 6

К вам на ревью попала задача, которая имеет код ниже. Видите ли вы какие-нибудь проблемы? Если да, то необходимо подсветить их и обосновать почему это явялется проблемой?

import Foundation

actor ActivitiesStorage {

    private var cache = [UUID: Data?]()

    func retrieveHeavyData(for id: UUID) async -> Data? {
        if let data = cache[id] {
            return data
        }
        let data = await requestDataFromDatabase(for: id)
        cache[id] = data
        return data

    }

    private func requestDataFromDatabase(for id: UUID) async -> Data? {
        print("Performing heavy data loading!")
        try! await Task.sleep(for: .seconds(1))
        // ...
        return nil
    }
}

Задача 7

Расскажите как работает Actor? Какие нюансы есть при работе с Actor?

Задача 8

Вы снова являетесь ревьюером задачи, в проекте вы используете MVVM и вам приносят на ревью код:

@MainActor
class OnboardingViewModel: ViewModel {
///
}

Будут ли у вас какие-нибудь замечания? хорошая ли идея помечать целый класс @MainActor - какие неприятные последствия могут быть в будущем при расширении проекта?

Задача 9

Напишите реализацию своей AsyncSequence, например чтобы отдавались числа до определённого лимита с задержкой в 1 секунду. Учитывайте возможность отмены работы AsyncSequence.

Задача 10

У вас есть легаси функция, которая содержит completion, код который вы пишите - асинхронный. Как можно вызвать данную функцию?

func fetchImageData(id: UUID, completion: @escaping (Result<Data, Error>)) {
    /// ...
}


func loadImage(from id: UUID) async throws -> Data {
    // Ваш код
}

Задача 11

Вы ревьюер. Что скажете про код ниже? можно ли его оптимизировать?

final class SomeInteractor: Sendable {
    /// ...

    func fetchUser(uid: UUID) async {
        do {
            let user = try await userService.fetchUserFromDatabase(by: uid)
            await presenter.displayUser(user)
            let updatedUser = try await userService.loadUserFromNetwork(by: uid)
            try Task.checkCancellation()
            await presenter.displayUser(updatedUser)
        } catch {
            print("Error fetching user: \(error)")
        }
    }
}

Задача 12

Напишите реализацию кеша, который бы мог хранить в себе информацию, определённое время и по истечении времени возвращать nil. На инициализацию данный кеш должен принимать TimeInterval, который означает время жизни кеша. В функционале должны быть реализованы следующие методы:

private struct Cache {
    let value: T
    let date: Date
}

/// Ваш класс/эктор {

    private let cacheLifeTimeSeconds: TimeInterval
    private var cache: Cache?

    public var value: (value: T?, shouldUpdate: Bool) { ... }
    public func set(_ newValue: T?) { ... }
}

Задача 13

Можно ли в задаче выше обойтись без actor? Если да - то как, если нет - то почему? Использовать другие механизмы синхронизации НЕЛЬЗЯ

Задача 14*

Вам дали задачку, в которой необходимо реализовать сервис получения аватарок пользователя. Сервис должен удовлетворять протоколу ниже

public typealias AvatarUpdatePublisher = AsyncPublisher<AnyPublisher<(UUID, Avatar), Never>>

public protocol AnyAvatarService: Sendable {

    var updatesPublisher: AvatarPublisher { get }

    /// Запрашивает на сервере (если нужно), готовит и возвращает аватар нужного размера
    func requestAvatar(for user: UUID) async throws -> Avatar
    /// Порядок в возвращаемом массиве НЕ соответствует порядку запросов
    func requestAvatars(for userIds: [UUID]) async throws -> [Avatar]

    func setupCacheLimits(countLimit: Int, totalCostLimit: Int) async
    func clearAllAvatars() async
}

Данный сервис должен работать следующим образом. Когда UI функционал запрашивает аватарку, сервис должен проверить - находится ли она в рантайм кеше - если да, то отдать. Если нет - то необходимо проверить скачено ли изображение и хранится ли оно локально на телефоне, если да - тот необходимо обработать это изображение и вернуть пользователю в нужном размере. В остальном случае необходимо запросить изображение у сервера.

Дополнительные условия:

  • Если сервер отвечает ошибкой - то необходимо сгенерировать изображение, на котором нужно написать первую букву email
  • Если выполняется запрос на сервер для какого-нибудь userId, то он должен быть единственным, дублирования запросов с одинаковыми айдишниками быть не должно
  • У рантайм кеша должен быть лимит и он должен правильно подчищаться
  • Пользователь может запросить один из трех размеров изображения (64, 32, 16). С сервера возвращается исходное изображение размера 128x128
  • Также необходимо учитывать, что запрос аватарок можно происходить из нескольких мест приложения
  • *** Если вы чувствуете в себе силы, можно написать ViewModifier, чтобы было удобно работать с сервисом на UI. Подумайте, как это можно лучше сделать?

Примечание Начинайте реализовывать данный функционал последовательно - не пытайтесь написать сразу идеально. Начните с самого просто шага, когда запрос аватара происходит из 1 места на UI, подумайте что можно использовать для рантайм кеша? может есть что-то системное? или же вы захотите написать что-нибудь своё? Далее плавно повышайте сложность вашего сервиса, следите за потокобезопасностью, reetrancy. Данная задача может многому вас научить, она сочетает в себе все пройденные темы. По итогу реализации старайтесь думать о крайних кейсах, когда нужно загрузить множество аватаров, плохая сеть и другие.

Самым последним шагом приступайте к реализации updatesPublisher, он необходим для того, чтобы оповещать уже имеющиеся вьюхи о произошедших изменениях.