Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI NavigationLink selected value resets to nil

I have a SwiftUI watchOS app that uses NavigationLinks to route between a few views. I'm using NavigationLink's tag and selection parameters to be able to control which view is visible. In my real app, I want to be able to navigate back to the view at index 0 when the user launches the app from a complication. However, whenever the $selection value is anything other than nil or 0 and $selection is set to 0 from my view model, the view at index 0 displays briefly, then $selection is reset to nil by SwiftUI (twice) without any user interaction causing it.

I've created a sample app that demonstrates this behavior, with a timer approximating the user launching from a complication. I made everything other than the selection value a constant to try to eliminate diffing as a possible source.

Here is a gif demonstrating what happens:

A screen recording demonstrating this bug

import SwiftUI

struct Item: Identifiable {
    let id: Int
    let text: String
}

class ContentViewModel: ObservableObject {
    @Published public var selectedIndex: Int? {
        didSet {
            print("ContentViewModel selectedIndex = \(String(describing: selectedIndex))")
        }
    }
    public let items: [Item] = [
        Item(id: 0, text: "Zero"),
        Item(id: 1, text: "One"),
        Item(id: 2, text: "Two"),
        Item(id: 3, text: "Three"),
        Item(id: 4, text: "Four")
    ]

    private var timer: Timer?
    init() {
        self.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { [unowned self] _ in
            print("ContentViewModel timer fired")
            self.selectedIndex = 0
        }
    }
}

struct ContentView: View {
    @ObservedObject private var viewModel = ContentViewModel()

    var body: some View {
        List(self.viewModel.items) { (item: Item) in
            NavigationLink(destination: Text(item.text), tag: item.id, selection: self.$viewModel.selectedIndex) {
                Text(item.text)
            }
        }
    }
}

Here's what is logged in the console:

ContentViewModel selectedIndex = Optional(2)
ContentViewModel timer fired
ContentViewModel selectedIndex = Optional(0)
ContentViewModel selectedIndex = nil
ContentViewModel selectedIndex = nil
ContentViewModel selectedIndex = Optional(3)
ContentViewModel timer fired
ContentViewModel selectedIndex = Optional(0)
ContentViewModel selectedIndex = nil
ContentViewModel selectedIndex = nil

I'd love any help or pointers y'all can provide!

Note: I am running the latest versions of everything, Xcode 11.4, Swift 5.2, watchOS 6.2.

like image 642
Brock Batsell Avatar asked Nov 06 '22 08:11

Brock Batsell


1 Answers

It looks like watchOS-only issue, I don't observe such behaviour on iOS. Worth submitting feedback to Apple.

Here is possible temporary workaround. Tested with Xcode 11.4.

self.timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [unowned self] _ in
    print("ContentViewModel timer fired")
    self.selectedIndex = nil                                   // << reset
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {    // postpone !!
        self.selectedIndex = 0                                 // << next
    }
}
like image 158
Asperi Avatar answered Nov 15 '22 12:11

Asperi