shaps80 / SwiftUIBackports

A collection of SwiftUI backports for iOS, macOS, tvOS and watchOS
MIT License
931 stars 59 forks source link

Fix the id update in the task view modifier #60

Closed hguandl closed 11 months ago

hguandl commented 11 months ago

Motivation

The following demo reveals an inconsistency between the official and backport implementation of task(id:priority:_:).

import Combine
import SwiftUI
import SwiftUIBackports

struct ContentView: View {
    @State private var text = "Hello world"

    var body: some View {
        VStack {
            InnerView(text: text)
        }
        .task {
            try? await Task.sleep(nanoseconds: 500_000_000)
            print("Will change `text`")
            text = "New world"
        }
    }
}

struct InnerView: View {
    let text: String

    var body: some View {
        VStack {
            Text(text)
        }
        .onChange(of: text) { newValue in
            print("[onChange] text: \(text), newValue: \(newValue)")
        }
        .onReceive(Just(text)) { _ in
            print("[onReceive] text: \(text)")
        }
        .task(id: text) {
            print("[task] text: \(text)")
        }
        .backport.onChange(of: text) { newValue in
            print("[backport.onChange] text: \(text), newValue: \(newValue)")
        }
        .backport.task(id: text) {
            print("[backport.task] text: \(text)")
        }
    }
}

The output is:

[onReceive] text: Hello world
[task] text: Hello world
[backport.task] text: Hello world
Will change `text`
[onChange] text: Hello world, newValue: New world
[onReceive] text: New world
[backport.onChange] text: Hello world, newValue: New world
[backport.task] text: Hello world
[task] text: New world

We can notice that the backport version is still using the old value of text. This is probably because SwiftUI executes onChange(of:perform:) before the value is updated. Since the official implementation of iOS 15 is using the new value, which is the expected behavior, we need to change the backport implementation.

Code changes

We can use Publisher.Just to make the view depend on id, so that SwiftUI would immediately update the value. The code is similar to Backport.onChange(of:perform:).

After the changes, the output is as expected:

[onReceive] text: Hello world
[task] text: Hello world
[backport.task] text: Hello world
Will change `text`
[onChange] text: Hello world, newValue: New world
[onReceive] text: New world
[backport.onChange] text: Hello world, newValue: New world
[backport.task] text: New world
[task] text: New world
shaps80 commented 11 months ago

Nice catch! This makes complete sense 👍🏻 I'll merge this now and get it into the next release thank you!