[SwiftUI] Task & .task

Junyoung Park·2022년 8월 28일
0

SwiftUI

목록 보기
57/136
post-thumbnail

How to use Task and .task in Swift | Swift Concurrency #4

Task & .task

구현 목표


1. async 데이터를 받아오는 코드를 실행할 때 Task 안에 넣어서 실행하는 방법
2. Task의 우선순위 파악하기
3. Task 내부의 스레드 환경 확인하기
4. Task 내부 작업을 취소하는 방법 확인하기

구현 태스크

  1. 서로 다른 Task를 실행할 때 우선순위를 커스텀하기
  2. Task.yield() 사용해보기
  3. Task.detached() 사용해보기
  4. Task.cancel() 사용해보기
  5. .task 사용해보기

핵심 코드

    func fetchImage() async {
        guard let url = getURL() else { return }
        do {
            let (data, _) = try await URLSession.shared.data(from: url, delegate: nil)
            await MainActor.run {
                self.image = UIImage(data: data)
            }
        } catch {
            print(error.localizedDescription)
        }
    }
  • 데이터를 async await로 받기
  • 받은 데이터를 UI가 사용할 데이터로 할당하는 부분은 MainActor를 통해 동기화 이슈 사전에 해결하기
        .task {
            try? await Task.sleep(nanoseconds: 5_000_000_000)
            await viewModel.fetchImage()
            await viewModel.fetchImage2()
        }
  • .task 블럭을 통해 컴파일러가 자체적으로 cancel 상황을 고려함
  • 작업 도중 취소 시 비동기적 데이터를 다운로드받는 동작을 지속하는 게 아니라 중도 멈춤

소스 코드

import SwiftUI

class TaskBootCampViewModel: ObservableObject {
    @Published var image: UIImage? = nil
    @Published var image2: UIImage? = nil

    
    func getURL() -> URL? {
        guard let url = URL(string: "https://picsum.photos/1000") else { return nil }
        return url
    }
    
    func fetchImage() async {
        guard let url = getURL() else { return }
        do {
            let (data, _) = try await URLSession.shared.data(from: url, delegate: nil)
            await MainActor.run {
                self.image = UIImage(data: data)
            }
        } catch {
            print(error.localizedDescription)
        }
    }
    func fetchImage2() async {
        guard let url = getURL() else { return }
        do {
            let (data, _) = try await URLSession.shared.data(from: url, delegate: nil)
            await MainActor.run {
                self.image2 = UIImage(data: data)
            }
        } catch {
            print(error.localizedDescription)
        }
    }
    func printTaskPriority() {
        Task(priority: .low) {
            print("LOW: \(Thread.current) : \(Task.currentPriority)")
        }
        Task(priority: .medium) {
            print("MEDIUM: \(Thread.current) : \(Task.currentPriority)")
        }
        Task(priority: .high) {
            print("HIGH: \(Thread.current) : \(Task.currentPriority)")
        }
        Task(priority: .background) {
            print("BACKGROUND: \(Thread.current) : \(Task.currentPriority)")
        }
        Task(priority: .userInitiated) {
            print("USERINITIATED: \(Thread.current) : \(Task.currentPriority)")
        }
        Task(priority: .utility) {
            print("UTILITY: \(Thread.current) : \(Task.currentPriority)")
        }
        
        /*
         HIGH: <NSThread: 0x600002266980>{number = 4, name = (null)} : TaskPriority(rawValue: 25)
         USERINITIATED: <NSThread: 0x600002266980>{number = 4, name = (null)} : TaskPriority(rawValue: 25)
         MEDIUM: <NSThread: 0x60000224bc80>{number = 6, name = (null)} : TaskPriority(rawValue: 21)
         LOW: <NSThread: 0x60000223d9c0>{number = 5, name = (null)} : TaskPriority(rawValue: 17)
         UTILITY: <NSThread: 0x60000223d9c0>{number = 5, name = (null)} : TaskPriority(rawValue: 17)
         BACKGROUND: <NSThread: 0x60000224bc80>{number = 6, name = (null)} : TaskPriority(rawValue: 9)
         */
    }
    
    func printTaskPrioirty2() async {
        Task(priority: .low) {
            print("LOW: \(Thread.current) : \(Task.currentPriority)")
        }
        Task(priority: .medium) {
            print("MEDIUM: \(Thread.current) : \(Task.currentPriority)")
        }
        Task(priority: .high) {
//            try? await Task.sleep(nanoseconds:2_000_000_00)
            await Task.yield()
            print("HIGH: \(Thread.current) : \(Task.currentPriority)")
        }
        Task(priority: .background) {
            print("BACKGROUND: \(Thread.current) : \(Task.currentPriority)")
        }
        Task(priority: .userInitiated) {
            print("USERINITIATED: \(Thread.current) : \(Task.currentPriority)")
        }
        Task(priority: .utility) {
            print("UTILITY: \(Thread.current) : \(Task.currentPriority)")
        }
        /*
         USERINITIATED: <NSThread: 0x600003c5a400>{number = 7, name = (null)} : TaskPriority(rawValue: 25)
         MEDIUM: <NSThread: 0x600003c5f7c0>{number = 5, name = (null)} : TaskPriority(rawValue: 21)
         LOW: <NSThread: 0x600003c469c0>{number = 6, name = (null)} : TaskPriority(rawValue: 17)
         UTILITY: <NSThread: 0x600003c469c0>{number = 6, name = (null)} : TaskPriority(rawValue: 17)
         BACKGROUND: <NSThread: 0x600003c469c0>{number = 6, name = (null)} : TaskPriority(rawValue: 9)
         HIGH: <NSThread: 0x600003c469c0>{number = 6, name = (null)} : TaskPriority(rawValue: 25)
         -> YEILD THREAD
         */
    }
    
    func printParentChildTask() {
        Task(priority: .low) {
            print("Parent: \(Thread.current) : \(Task.currentPriority)")
            
            Task {
                print("Child: \(Thread.current) : \(Task.currentPriority)")
            }
            
            Task.detached(priority: .high) {
                print("Child Detached: \(Thread.current) : \(Task.currentPriority)")
            }
        }
        /*
         Parent: <NSThread: 0x600002b7cc80>{number = 6, name = (null)} : TaskPriority(rawValue: 17)
         Child Detached: <NSThread: 0x600002b11a00>{number = 4, name = (null)} : TaskPriority(rawValue: 25)
         Child: <NSThread: 0x600002b7cc80>{number = 6, name = (null)} : TaskPriority(rawValue: 17)
         -> Don't use detached method if possible as official Doc says
         */
    }
    
    func resetImages() {
        image = nil
        image2 = nil
    }
}
  • 일반적인 async await를 활용한 데이터 패치 함수
  • 태스크에 우선순위를 할당 가능
  • Task.yield()를 통해 태스크를 할당하거나 양보하는 부분을 지정 → 우선순위가 가장 높은 HIGH 스레드는 printTaskPriority에서 작동한 바와 같이 다른 스레드보다 먼저 실행되어야 하지만 printTaskPriority2에서 작동한 바와 같이 스레드 양보를 통해 마지막에 실행됨
  • Task.detached를 통해 부모 태스크와 자식 태스크를 분리 가능, 자식 태스크의 우선순위를 커스텀 가능 → 공식 문서에 따르자면 가능한 사용하지 않아야 할 방법
struct TaskBootCamp: View {
    @StateObject private var viewModel = TaskBootCampViewModel()
    @State private var count: Int = 0
    @State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    @State private var fetchImageTask: Task<(), Never>? = nil
    var body: some View {
        NavigationView {
            ZStack {
                NavigationLink {
                    imageView
                } label: {
                    Text("Image Download")
                }
            }
        }
    }
}

extension TaskBootCamp {
    private var imageView: some View {
        VStack {
            Text("TIMER COUNT: \(count)")
            if let image = viewModel.image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
            }
            if let image2 = viewModel.image2 {
                Image(uiImage: image2)
                    .resizable()
                    .scaledToFit()
            }
        }
        .onReceive(timer, perform: { _ in
            count = count + 1 <= 5 ? count + 1 : count
        })
        .onAppear {
            viewModel.resetImages()
            count = 0
//            taskFetchImages2()
        }
//        .onDisappear {
//            count = 0
//            fetchImageTask?.cancel()
//        }
        .task {
            try? await Task.sleep(nanoseconds: 5_000_000_000)
            await viewModel.fetchImage()
            await viewModel.fetchImage2()
        }
    }
    func taskFetchImages() {
        Task {
            try? await Task.sleep(nanoseconds: 5_000_000_000)
            await viewModel.fetchImage()
            await viewModel.fetchImage2()
        }
    }
    func taskFetchImages2() {
        fetchImageTask = Task {
            try? await Task.sleep(nanoseconds: 5_000_000_000)
            await viewModel.fetchImage()
        }
    }
}
  • TaskBootCamp 뷰 내부에서 네비게이션 뷰 활용, 유저의 중도 취소(백 버튼 기능) → Task.cancel 상황을 구현

구현 화면

profile
JUST DO IT

0개의 댓글