SwiftUI task에서 비동기 작업하기

  • 이번에는 iOS 15에서 새로 생긴 SwiftUI의 새로운 modifier인 task에 대해서 알아보겠습니다.
  • taskonAppear에 비교적 적합하지 않았던 비동기 작업을 더 적절하게 실행할수 있게 해줍니다.

소개

  • SwiftUI에서는 네트워크 요청이 필요할때 대부분 View가 생성되는 시점인 onAppear에서 진행했는데요.
  • 이제 네트워크 요청 같이 비동기 작업이 필요한 작업은 onAppear보다 task에서 진행하면 될 것 같습니다.
  • 하지만 task는 iOS 15에서 소개 된 만큼, iOS 15 미만의 버전에서 사용이 불가합니다.

정의

// Adds a task to perform before this view appears or when a specified value changes.
task(id:priority:_:)
  • task의 형태와 정의는 다음과 같은데요.
  • 쉽게 말해, task는 view가 나타기 전에 혹은 특정한 값이 변경 될 때 수행할 비동기 작업을 추가합니다.
  • 여기서 특정한 값이란 id에 들어가는 값을 의미합니다.

선언부와 리턴 값

  • task의 선언부와 리턴 값은 다음과 같은데요.
  • parameter로 id, priority, 그리고 action이 선택적으로 들어갑니다.
func task<T>(
    id value: T,
    priority: TaskPriority = .userInitiated,
    _ action: @escaping () async -> Void
) -> some View where T : Equatable
  • 여기서 id는 Equatable을 준수하는 타입의 값이고 priority는 TaskPriority 타입 값입니다.
  • action에는 onAppear에서 처럼 실행할 코드를 넣으면 되지만, 비동기 작업을 하는 코드라는 것이 핵심입니다.
  • return 값으로는 view가 나타나기 전에 비동기 action을 실행하거나 id 값이 변경될때 task를 재시작하는 view가 리턴됩니다.

장점

  • taskonAppear이 있음에도 필요한 이유는 좋은 점이 있기 때문이죠.
  • task의 좋은점은 task가 modifier로 추가된 view의 수명과 일치하는 수명을 갖기 때문입니다.
  • 즉, task에 넣은 작업이 완료되기 전에 View가 사라지면 SwiftUI가 task에 있던 작업을 취소하거나 재실행 해줍니다.

예시

  • task를 사용하는 예시를 들어보겠습니다.
  • 우선 Message라는 구조체가 다음과 같이 있습니다.
struct Message: Decodable, Identifiable {
    let id: Int
    let text: String
}
  • 그리고 다음과 같이 메시지들을 불러오는 비동기 작업을 하는 loadMessages라는 함수가 있습니다.
  • loadMessages와 같은 함수를 실행할때 task에서 실행하면 훨씬 적절한 타이밍에 실행할수 있습니다.
struct ContentView: View {
    @State private var messages = [Message]()

    var body: some View {
          List(messages) { message in
              VStack {
                  Text(message.text)
              }
          }
          .task {
              await loadMessages()
          }
    }

    func loadMessages() async {
        do {
            let url = URL(string: "https://example.com/messages.json")!
            let (data, _) = try await URLSession.shared.data(from: url)
            messages = try JSONDecoder().decode([Message].self, from: data)
        } catch {
            messages = [
                Message(id: 0, text: "Please try again later.")
            ]
        }
    }
}

추가 기능

  • task를 사용하는 효과가 더욱 부각되는 것은 id 값을 활용할 때 입니다.
  • taskEquatable을 준수하는 id 값을 넣는다고 생각해봅시다.
  • 이 때, id 값이 변경되면 SwiftUI는 자동으로 이전의 작업을 취소하고 새로운 값으로 새 작업을 생성합니다.
  • 예를들어, 다시 한번 다음과 같이 Message 구조체가 있다고 생각해봅시다.
struct Message: Decodable, Identifiable {
    let id: Int
    let text: String
}
  • 이전의 예시와는 다르게, selectedBox라는 변수가 추가 되었습니다.
  • 이는 .task(id: selectedBox)에서 사용되고 있는데요.
  • selectedBox의 값에 따라 다른 url에서 data를 fetch 해오고 작업이 재실행 되는 것을 볼 수 있습니다.
struct ContentView: View {
    @State private var messages = [Message]()
    @State private var selectedBox = "Inbox"
    
    let messageBoxes = ["Inbox", "Sent"]

    var body: some View {
        NavigationView {
          List(messages) { message in
              VStack {
                  Text(message.text)
              }
          }                    
          .task(id: selectedBox) {
              await fetchData()
          }
            
        }
    }

    func fetchData() async {
        do {
            let url = URL(string: "https://example.com/\(selectedBox.lowercased()).json")!
            let (data, _) = try await URLSession.shared.data(from: url)
            messages = try JSONDecoder().decode([Message].self, from: data)
        } catch {
            messages = [
                Message(id: 0, text: "Please try again later.")
            ]
        }
    }
}
  • 추가적으로, task modifier는 priority 파라미터도 존재해서 task의 우선순위도 정해줄수 있습니다.
  • 예를들어, .task(priority: .low)는 낮은 우선 순위의 작업을 생성 할 것입니다.

참고