crashing playground with ForEach - swiftui

I have this code, see below, it contains a ForEach loop which is commented out. the same view in an App runs just fine even if the ForEach loop is enabled. However, in a playground it crashes with a very unhelpful error message:
"error: Playground execution aborted: error: Execution was interrupted, reason: signal SIGABRT.
The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation."
I tried finding information about this message. From what I understand it means that lldb does not know exactly what goes wrong and prints this. So, I turn to stack overflow in the hope someone does know what exactly might by going wrong here....?
import Cocoa
import SwiftUI
import PlaygroundSupport
import Combine
struct RowModel : Identifiable, Hashable {
var text : String
var id : UUID = UUID()
}
class My : ObservableObject {
#Published var s: String = "Hi there"
#Published var elements = [
RowModel(text: "een"),
RowModel(text: "twee"),
RowModel(text: "drie"),
RowModel(text: "vier"),
]
}
struct Row : View {
var item : String
var body : some View {
Text(item)
}
}
struct Screen : View {
#StateObject var my = My()
var body: some View {
VStack {
Text("The screen")
VStack {
Row(item: my.elements[0].text)
Row(item: my.elements[1].text)
// ForEach(0 ..< my.elements.count, id: \.self){ (index : Int) in
//
// Row(item: my.elements[index].text)
// }
}.frame(height: 100)
TextField("enter values", text: $my.s)
}
}
}
var view = Screen()
PlaygroundPage.current.setLiveView(view)

I'm late to the party, but this bug is still not fixed at this point so I figured I'd leave this for any future viewer.
This seems to be an issue with results bar on the right, because moving the view to a separate file in Sources (press Command 0) works fine.

there is a workaround for this bug
move ContentView to a separate file in Sources directory
make your models public
more info
https://stackoverflow.com/a/67120969/2027018

Related

SwiftUI Navigation: why is Timer.publish() in View body breaking nav stack

SwiftUI n00b here. I'm trying some very simple navigation using NavigationView and NavigationLink. In the sample below, I've isolated to a 3 level nav. The 1st level is just a link to the 2nd, the 2nd to the 3rd, and the 3rd level is a text input box.
In the 2nd level view builder, I have a
private let timer = Timer.publish(every: 2, on: .main, in: .common)
and when I navigate to the 3rd level, as soon as I start typing into the text box, I get navigated back to the 2nd level.
Why?
A likely clue that I don't understand. The print(Self._printChanges()) in the 2nd level shows
NavLevel2: #self changed.
immediately when I start typing into the 3rd level text box.
When I remove this timer declaration, the problem goes away. Alternatively, when I modify the #EnvironmentObject I'm using in the 3rd level to just be #State, the problem goes away.
So trying to understand what's going on here, if this is a bug, and if it's not a bug, why does it behave this way.
Here's the full ContentView building code that repos this
import SwiftUI
class AuthDataModel: ObservableObject {
#Published var someValue: String = ""
}
struct NavLevel3: View {
#EnvironmentObject var model: AuthDataModel
var body: some View {
print(Self._printChanges())
return TextField("Level 3: Type Something", text: $model.someValue)
// Replacing above with this fixes everything, even when the
// below timer is still in place.
// (put this decl instead of #EnvironmentObject above
// #State var fff: String = ""
// )
// return TextField("Level 3: Type Something", text: $fff)
}
}
struct NavLevel2: View {
// LOOK HERE!!!! Removing this declaration fixes everything.
private let timer = Timer.publish(every: 2, on: .main, in: .common)
var body: some View {
print(Self._printChanges())
return NavigationLink(
destination: NavLevel3()
) { Text("Level 2") }
}
}
struct ContentView: View {
#StateObject private var model = AuthDataModel()
var body: some View {
print(Self._printChanges())
return NavigationView {
NavigationLink(destination: NavLevel2())
{
Text("Level 1")
}
}
.environmentObject(model)
}
}
First, if you remove #StateObject from model declaration in ContentView, it will work.
You should not set the whole model as a State for the root view.
If you do, on each change of any published property, your whole hierarchy will be reconstructed. You will agree that if you type changes in the text field, you don't want the complete UI to rebuild at each letter.
Now, about the behaviour you describe, that's weird.
Given what's said above, it looks like when you type, the whole view is reconstructed, as expected since your model is a #State object, but reconstruction is broken by this unmanaged timer.. I have no real clue to explain it, but I have a rule to avoid it ;)
Rule:
You should not make timers in view builders. Remember swiftUI views are builders and not 'views' as we used to represent before. The concrete view object is returned by the 'body' function.
If you put a break on timer creation, you will notice your timer is called as soon as the root view is displayed. ( from NavigationLink(destination: NavLevel2())
That's probably not what you expect.
If you move your timer creation in the body, it will work, because the timer is then created when the view is created.
var body: some View {
var timer = Timer.publish(every: 2, on: .main, in: .common)
print(Self._printChanges())
return NavigationLink(
destination: NavLevel3()
) { Text("Level 2") }
}
However, it is usually not the right way neither.
You should create the timer:
in the .appear handler, keep the reference,
and cancel the timer in .disappear handler.
in a .task handler that is reserved for asynchronous tasks.
I personally only declare wrapped values ( #State, #Binding, .. ) in view builders structs, or very simple primitives variables ( Bool, Int, .. ) that I use as conditions when building the view.
I keep all functional stuffs in the body or in handlers.
To stop going back to the previous view when you type in the TextField add .navigationViewStyle(.stack) to the NavigationView
in ContentView.
Here is the code I used to test my answer:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject var model = AuthDataModel()
var body: some View {
NavigationView {
NavigationLink(destination: NavLevel2()){
Text("Level 1")
}
}.navigationViewStyle(.stack) // <--- here the important bit
.environmentObject(model)
}
}
class AuthDataModel: ObservableObject {
#Published var someValue: String = ""
}
struct NavLevel3: View {
#EnvironmentObject var model: AuthDataModel
var body: some View {
TextField("Level 3: Type Something", text: $model.someValue)
}
}
struct NavLevel2: View {
#EnvironmentObject var model: AuthDataModel
#State var tickCount: Int = 0 // <-- for testing
private let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
var body: some View {
NavigationLink(destination: NavLevel3()) {
Text("Level 2 tick: \(tickCount)")
}
.onReceive(timer) { val in // <-- for testing
tickCount += 1
}
}
}

How to change values inside a picker using #Published property wrapper in swiftui

hi am having issues with the picker view in swiftui
i have created one file with just a class like this
import Foundation
import SwiftUI
class something: ObservableObject {
#Published var sel = 0
}
and then I created 2 views
import SwiftUI
struct ContentView: View {
#StateObject var hihi: something
var characters = ["Makima", "Ryuk", "Itachi", "Gojou", "Goku", "Eren", "Levi", "Jiraya", "Ichigo", "Sukuna"]
var body: some View {
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(characters[hihi.sel])
}
now(hihi: something())
}
}
}
struct now: View {
#StateObject var hihi: something
var body: some View {
Text("\(hihi.sel)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(hihi: something())
}
}
now the problem am facing is that the code compiles but the picker ain't working it won't change to any other value in the array I have provided it recoils back to its original value provided that is 0th index "Makima" and it won't select any other option, why so?
please help
There are three problems, the main one being the mismatching selection.
In the Picker, your selection is based on the string value for each character. This is because the ForEach identifies each Text by the name string, since you used id: \.self.
However, your something model (which ideally should start with a capital letter by convention) has a numeric selection. Because the items in the Picker have String IDs, and this is an Int, the selection can't be set.
You can change your model to this:
class something: ObservableObject {
#Published var sel = "Makima"
}
Which also requires a slight change in the body:
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(hihi.sel) // Now getting string directly
}
now(hihi: something())
}
Notice we now have two views showing the selected character - but only the top one updates. The bottom one may now be redundant (the now view), but I'll show you how you can get it working anyway. This is where we encounter the 2nd problem:
You are creating a new instance of something() when passing it to now (again, should start with a capital). This means that the current instance of hihi stored in ContentView is not passed along. You are just creating a new instance of something, which uses the default value. This is completely independent from the hihi instance.
Replace:
now(hihi: something())
With:
now(hihi: hihi)
The final problem, which may not be as visible, is that you shouldn't be using #StateObject in now, since it doesn't own the object/data. Instead, the object is passed in, so you should use #ObservedObject instead. Although the example now works even without this change, you will have issues later on when trying to change the object within the now view.
Replace #StateObject in now with #ObservedObject.
Full answer (something is initialized in ContentView only for convenience of testing):
struct ContentView: View {
#StateObject var hihi: something = something()
var characters = ["Makima", "Ryuk", "Itachi", "Gojou", "Goku", "Eren", "Levi", "Jiraya", "Ichigo", "Sukuna"]
var body: some View {
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(hihi.sel)
}
now(hihi: hihi)
}
}
}
struct now: View {
#ObservedObject var hihi: something
var body: some View {
Text(hihi.sel)
}
}
class something: ObservableObject {
#Published var sel = "Makima"
}

SwiftUI: Updating ui when view is not present causes "Unable to present view. Please file a bug."

I get the following error: Unable to present view. Please file a bug whenever I make an asynchronous call on a view and leave the view (e.g. navigate to another view in the navigation stack) before it can make changes to the ui. Consequently, the next view in the navigation stack is unable to update its view. How can I fix this problem?
An example of the problem occurring is when I switch from view1 to view2 before my GetIoTThingIndex() call finishes and makes an update to the ui.
GetIoTThingIndex.query(device) { error in
DispatchQueue.main.async { [self] in
...
}
}
EDIT:
After doing more investigating, I found that this problem is due to the fact that I am implementing my logic in an MVVM pattern. When I moved my logic directly into the the view and called the functions and state variables inside the view, everything worked fine. It's interesting because when I started building my app with just a few pages with minimal logic and dependencies, this MVVM pattern worked fine without any bugs. However, when my project grew to 20+ pages with more logic and dependencies, the MVVM pattern causes this bug. Is this just a problem I see or has anyone seen anything like this before and have any recommendations for fixing it?
This is the way I had things with MVVM.
View
struct DeviceView: View {
#ObservedObject var viewModel = DeviceViewModel()
var body: some View {
Text(viewModel.name)
...
}
}
View Model
class DeviceViewModel: ObservableObject {
#Published var name = ""
public func updateUI() {
...
}
...
}
This is the way I have things now (which works without this bug).
View
struct DeviceView: View {
var body: some View {
Text(name)
...
}
#State var name = ""
public func updateUI() {
...
}
...
}
Are you sure this is what is happening?
I've tested the idea of navigating to another view
before the parent can make a change to its view. And all works well.
This is the code I used for the test, click on the button first, then within 3 sec click on the NavigationLink.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var thingToUpdate = ""
var body: some View {
NavigationView {
VStack (spacing: 40) {
Text("text \(thingToUpdate)")
Button("click me first") {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
thingToUpdate = " is updated now"
}
}
NavigationLink(destination: Text("the detail view")) {
Text("then to DetailView")
}
}
}
}
}
Edit update using ObservableObject that works for me:
class DeviceViewModel: ObservableObject {
#Published var name = "no name"
public func updateUI() {
// simulated delay on the main thread
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.name = "success"
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = DeviceViewModel()
var body: some View {
NavigationView {
VStack (spacing: 40) {
Text("viewModel name is \(viewModel.name)")
Button("click me first") {
viewModel.updateUI()
}
NavigationLink(destination: Text("DetailView")) {
Text("then to DetailView")
}
}
}
}
}

Swiftui WatchKit List with onDelete and confirm alert does not update tableview correctly

Can someone point me in the right direction on how to implement a simple list with a confirm remove-function or at least show best practice here.
Below code will work if the alert-part is removed and delete action is immediate.
Somehow, the presentation of the confirmation-alert makes the the deletion act on the wrong list of persons. First removal also gives a console warning:
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window.
However, I have no idea on how to solve this without removing the alert. By the way, this exact code worked a couple of weeks ago before my mac updated xcode i believe.
import Foundation
import SwiftUI
import Combine
struct Person: Identifiable{
var id: Int
var name: String
init(id: Int, name: String){
self.id = id
self.name = name
}
}
class People: ObservableObject{
#Published var people: [Person]
init(){
self.people = [
Person(id: 1, name:"One"),
Person(id: 2, name:"Two"),
Person(id: 3, name:"Three"),
Person(id: 4, name:"Four")]
}
}
struct ContentView: View {
#ObservedObject var mypeople: People = People()
#State private var showConfirm = false
#State private var idx = 0
func setDeletIndex(at idxs:IndexSet) {
self.showConfirm = true
self.idx = idxs.first!
}
func delete() {
self.mypeople.people.remove(at: idx)
}
var body: some View {
VStack {
List {
Text("Currently \(mypeople.people.count) persons").font(.footnote)
.alert(isPresented: $showConfirm) {
Alert(title: Text("Delete"), message: Text("Sure?"),
primaryButton: .cancel(),
secondaryButton: .destructive(Text("Delete")) {
self.delete()
})
}
ForEach(mypeople.people){ person in
Text("\(person.name)")
}.onDelete { self.setDeletIndex(at: $0) }
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The problem is due to conflict of updating alert closing & List record removing. The working workaround is to delay deleting, as below (tested with Xcode 11.4)
Text("Currently \(mypeople.people.count) persons").font(.footnote)
.alert(isPresented: $showConfirm) {
Alert(title: Text("Delete"), message: Text("Sure?"),
primaryButton: .cancel(),
secondaryButton: .destructive(Text("Delete")) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { // here !!
self.delete()
}
})
}

Fatal error: Accessing State<Binding<String>> outside View.body

I'm having a problem trying to get textfields working in SwiftUI.
I get Fatal error: Accessing State> outside View.body whenever I try to run the following code.
Anyone have a suggestion?
struct SearchRoot : View {
#State var text: String = ""
var body: some View {
HStack {
TextField($text,
placeholder: Text("type something here..."))
Button(action: {
// Closure will be called once user taps your button
print(self.$text)
}) {
Text("SEND")
}
}
}
}
I'm running Xcode Version 11.0 beta (11M336w) on macOS 10.15 Beta (19A471t)
Edit: Simplified code, still getting the same error.
struct SearchRoot : View {
#State var text: String = ""
var body: some View {
TextField($text,
placeholder: Text("type something here..."))
}
}
The compiler emits an error if the $ operator is used outside body, in a View.
The button initializer is defined as:
init(action: #escaping () -> Void, #ViewBuilder label: () -> Label)
You're using $ in an escaping closure, in the first snippet of code.
That means the action may outlive (escape) the body, hence the error.
The second snippet compiles and works fine for me.
Eureka! SwiftUI wants a single source of truth.
What I neglected to include in my original code snippets is that this struct is within a tabbed application.
To fix this I needed to define the #State var text: String = "" in the struct that creates the top level TabbedView, then use $Binding in the SearchRoot.
I'm not sure if this is works as designed or just a beta 1 issue, but it's the way it works for now.
struct ContentView : View {
#State private var selection = 0
#State private var text: String = "searching ex"
var body: some View {
TabbedView(selection: $selection){
ShoppingListRoot().body.tabItemLabel(Text("Cart")).tag(0)
SearchRoot(text: $text).body.tabItemLabel(Text("Search")).tag(1)
StoreRoot().body.tabItemLabel(Text("Store")).tag(2)
BudgetRoot().body
.tabItemLabel(Text("Budget"))
.tag(3)
SettingsRoot().body
.tabItemLabel(Text("Settings"))
.tag(4)
}
}
}