In the attached minimal reproducing app, the picker in the List works as expected while the one in the sheet resets every time the List view updates (this demo app displays a random byte value that is updated every second).
Can anyone explain why this happens and how to solve the issue, i.e. how can I have a picker in a sheet on a view without the picker getting reset when the parent view receives updates?
Somebody asked the same question here, but it is unanswered, it has different example code:
https://www.hackingwithswift.com/forums/swiftui/picker-value-resets-when-in-a-sheet-but-works-fine-in-parent-view/17211
import SwiftUI
@main
struct SwiftUITestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
// MARK: - ContentView
struct ContentView: View {
@State var selectedValue: Int = 0
@State var isSheetPresented: Bool = false
@State var randomByte: UInt8 = 0
var body: some View {
List {
Text("Frequently updated random byte: \(randomByte)")
Text("Note how the picker works as expected here:")
MyPicker()
Text("But not within a sheet:")
Button("Select a value inside a sheet") {
isSheetPresented = true
}
}
.sheet(
isPresented: $isSheetPresented,
onDismiss: {
isSheetPresented = false
},
content: {
List {
VStack(alignment: .leading, spacing: 16) {
Text("Select a value close to the bottom of the drop down menu.")
Text("Then open the drop down menu again.")
Text("Note that the drop down menu scrolls each time `randomByte` changes.")
}
MyPicker()
}
}
)
.task {
while true {
try! await Task.sleep(nanoseconds: 1_000_000_000)
randomByte = UInt8.random(in: .min ... .max)
}
}
}
}
// MARK: MyPicker
struct MyPicker: View {
@State var selectedValue: Int = 0
var body: some View {
Picker("Value", selection: $selectedValue) {
ForEach(0..<100, id: \.self) { value in
Text("\(value)").tag(value)
}
}
.onChange(of: selectedValue) { _ in
print("•••• selectedValue", selectedValue)
}
}
}
// MARK: Preview
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Post
Replies
Boosts
Views
Activity
I've made a small reproducing example app to demonstrate this issue. Just create an iOS App project and replace the App and ContentView files with the following code.
The app works as expected when running it in a simulator or on a device but the SwiftUI Preview will get stuck showing only the loading state, even though the print statement in the View's body has printed viewModel.state: showingContent(SwiftUITest.ViewModel.Content(text: "Hello world!", reloadButtonTitle: "Reload"))to the console.
When stuck showing the loading state (an animated ProgressView), If I change .padding(.all, 12) to e.g. .padding(.all, 2) or vice versa, then that will make the Preview render the expected content.
Also, if I tap the Reload-button, it will not show the ProgressView for 2 seconds as expected (and as the app will do when run in a simulator or on a device), instead it will just show a white screen, and the same workaround (changing the padding amount) can be used to make the it render the expected content.
Can other people reproduce this behavior, is it a known bug, or am I doing something wrong?
TestApp.swift
import SwiftUI
@main
struct SwiftUITestApp: App {
var body: some Scene {
WindowGroup {
ContentView(viewModel: ViewModel(
contentLoader: {
try! await Task.sleep(nanoseconds: 2_000_000_000)
return .init(text: "Hello world!", reloadButtonTitle: "Reload")
}
))
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
@StateObject var viewModel: ViewModel
var body: some View {
let _ = print("viewModel.state:", viewModel.state)
Group {
switch viewModel.state {
case .notStarted, .loading:
ProgressView()
case .showingContent(let content):
VStack {
Text(content.text)
.padding(.all, 12)
Button(content.reloadButtonTitle) {
viewModel.handle(event: .reloadButtonWasTapped)
}
}
}
}
.onAppear {
viewModel.handle(event: .viewDidAppear)
}
}
}
// MARK: - ViewModel
@MainActor
final class ViewModel: ObservableObject {
@Published var state: State = .notStarted
let contentLoader: () async -> Content
init(contentLoader: @escaping () async -> Content) {
self.contentLoader = contentLoader
}
func handle(event: Event) {
switch state {
case .notStarted:
if event == .viewDidAppear { loadContent() }
case .loading:
break
case .showingContent:
if event == .reloadButtonWasTapped { loadContent() }
}
}
func loadContent() {
guard state != .loading else { return }
state = .loading
Task {
print("starts loading", Date.now)
let content = await contentLoader()
print("finished loading", Date.now)
state = .showingContent(content)
}
}
enum State: Equatable {
case notStarted
case loading
case showingContent(Content)
}
struct Content: Equatable {
let text: String
let reloadButtonTitle: String
}
enum Event: Equatable {
case viewDidAppear
case reloadButtonWasTapped
}
}
// MARK: - Preview
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: ViewModel(
contentLoader: {
try! await Task.sleep(nanoseconds: 2_000_000_000)
return .init(text: "Hello world!", reloadButtonTitle: "Reload")
}
))
}
}
Here's the simple behavior of the app (recorded from simulator):
Each time the view appears, it loads it's content for two seconds while showing a ProgressView, then it shows the content, which includes a Reload button, and if you tap that button it will reload the content for 2 seconds again.
I would expect the Preview to behave in the same way.
On an iOS device, using the native keyboard (US english and "Predictive" turned on), if you write eg:"Have a " (with a space at the end)It will suggest three contextually relevant next-word-predictions on the bar at the top of the keyboard:good great niceI want to make use of this next-word prediction logic on iOS, but it turns out that I can only get it to work on OS X, where it's super simple.So here is a tiny command line app that does exactly what I want (allthough only on OS X):import AppKit
let str = "Have a "
let rangeForEndOfStr = NSMakeRange(str.utf16.count, 0)
let spellChecker = NSSpellChecker.sharedSpellChecker()
let completions = spellChecker.completionsForPartialWordRange(
rangeForEndOfStr,
inString: str,
language: "en",
inSpellDocumentWithTag: 0)
print(completions)Running that program will print:Optional(["good", "great", "nice", "lot", "problem", "new", "feeling", "chance", "few", "wonderful", "look", "big", "boyfriend", "better", "very", "job", "bad", "lovely", "crush", "blessed"])Note that the first three words are exactly the same as those displayed by the native (predictive) iOS keyboard. So it works (at least on OS X)!(It works by giving .completionsForPartialWordRange an zero-length range located at the end of the string (where the next word would go), rather than a range containing a partial word, allthough I guess you could say that a non-existing word is also a partial word.)But trying the same approach on iOS (using UITextChecker instead of NSSpellChecker, etc.) does not work:let str = "Have a "
let rangeForEndOfStr = NSMakeRange(str.utf16.count, 0)
let spellChecker = UITextChecker()
print(UITextChecker.availableLanguages())
let completions = spellChecker.completionsForPartialWordRange(
rangeForEndOfStr,
inString: str,
language: "en_US") // ( <-- .availableLanguages() says "en_US" on iOS and "en" on OS X. )
print(completions)(this code can be put in the viewDidLoad of the ViewController of an otherwise empty Single View iOS app.)Run the iOS app and it will just print nil. : (Turns out that UITextChecker's .completionsForPartialWordRange (contrary to NSSpellChecker's) simply returns nil if the range has zero length. I have tried all day to find any other way to get next-word-of-sentence-predictions/-suggestions/-completions on iOS but failed.(NOTE: I have no problem getting UITextChecker to return completions of partially entered wods, ie where the range is not empty, but contains partially written word(prefixe)s, allthough the resulting completions are not sorted so that the more probable comes first, as the documentation says. They are actually just sorted alphabetically ... Anyway, what I want is something else: Given a partially written sentence containing only complete words, I want a list of probable-next-word-in-sentence-completions, exactly as examplified by my OS X example and the native iOS keyboard above.)So, how do I write a working iOS-version of the OS X example above?