Updating a #State var Double from a struct array in SwiftUI - swift

I have been looking on StackOverFlow (and on Internet) the entire day to make this button works but I haven't found anything.
As you can see, I want to change the value when I click on the button by using the simplest solution possible.
Here are the codes :
// Element.swift
import Foundation
import SwiftUI
struct Element: Identifiable {
let id = UUID()
#State var value: Double
let title: String
}
let elements = [
Element(value: 10, title: "This is a title"),
Element(value: 100, title: "This is another title")
]
// ElementView.swift
import SwiftUI
struct ElementView: View {
let element: Element
var body: some View {
HStack {
Text(element.title)
Text("\(element.value)")
Button("More", action: { element.value += 10 })
}
}
}
struct ElementView_Previews: PreviewProvider {
static var previews: some View {
ElementView(element: elements[0])
}
}
// ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
ForEach(elements) { element in
ElementView(element: element)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Surprisingly, I don't have any issue with Xcode.
What did I miss? Thanks in advance! :P

You have a couple of issues with how you're treating state. In general, in SwiftUI, state is held by a parent and passed down to a child via Binding. It's also very important that #State is for use inside a View -- not inside your model.
See inline comments for changes and explanations.
struct Element: Identifiable {
let id = UUID()
var value: Double //Remove #State here -- #State is for use in a View
let title: String
}
struct ElementView: View {
#Binding var element: Element //pass Element via Binding so it is mutable
var body: some View {
HStack {
Text(element.title)
Text("\(element.value)")
Button("More", action: { element.value += 10 })
}
}
}
struct ElementView_Previews: PreviewProvider {
static var previews: some View {
ElementView(element: .constant(Element(value: 10, title: "This is a title"))) //use .constant() for a Binding in a Preview
}
}
struct ContentView: View {
#State private var elements = [ //define elements with #State so they are mutable
Element(value: 10, title: "This is a title"),
Element(value: 100, title: "This is another title")
]
var body: some View {
VStack {
ForEach($elements) { $element in //use element binding syntax to get a binding to each item in the array
ElementView(element: $element)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

SwiftUI - Nested links within NavigationStack inside a NavigationSplitView not working

I'm playing around with the new navigation API's offered in ipadOS16/macOS13, but having some trouble working out how to combine NavigationSplitView, NavigationStack and NavigationLink together on macOS 13 (Testing on a Macbook Pro M1). The same code does work properly on ipadOS.
I'm using a two-column NavigationSplitView. Within the 'detail' section I have a list of SampleModel1 instances wrapped in a NavigationStack. On the List I've applied navigationDestination's for both SampleModel1 and SampleModel2 instances.
When I select a SampleModel1 instance from the list, I navigate to a detailed view that itself contains a list of SampleModel2 instances. My intention is to navigate further into the NavigationStack when clicking on one of the SampleModel2 instances but unfortunately this doesn't seem to work. The SampleModel2 instances are selectable but no navigation is happening.
When I remove the NavigationSplitView completely, and only use the NavigationStack the problem does not arise, and i can successfully navigate to the SampleModel2 instances.
Here's my sample code:
// Sample model definitions used to trigger navigation with navigationDestination API.
struct SampleModel1: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel1(), SampleModel1(), SampleModel1()]
}
struct SampleModel2: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel2(), SampleModel2(), SampleModel2()]
}
// The initial view loaded by the app. This will initialize the NavigationSplitView
struct ContentView: View {
enum NavItem {
case first
}
var body: some View {
NavigationSplitView {
NavigationLink(value: NavItem.first) {
Label("First", systemImage: "house")
}
} detail: {
SampleListView()
}
}
}
// A list of SampleModel1 instances wrapped in a NavigationStack with multiple navigationDestinations
struct SampleListView: View {
#State var path = NavigationPath()
#State var selection: SampleModel1.ID? = nil
var body: some View {
NavigationStack(path: $path) {
List(SampleModel1.samples, selection: $selection) { model in
NavigationLink("\(model.id)", value: model)
}
.navigationDestination(for: SampleModel1.self) { model in
SampleDetailView(model: model)
}
.navigationDestination(for: SampleModel2.self) { model in
Text("Model 2 ID \(model.id)")
}
}
}
}
// A detailed view of a single SampleModel1 instance. This includes a list
// of SampleModel2 instances that we would like to be able to navigate to
struct SampleDetailView: View {
var model: SampleModel1
var body: some View {
Text("Model 1 ID \(model.id)")
List (SampleModel2.samples) { model2 in
NavigationLink("\(model2.id)", value: model2)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I removed this unclear ZStack and all works fine. Xcode 14b3 / iOS 16
// ZStack { // << this !!
SampleListView()
// }
Apple just releases macos13 beta 5 and they claimed this was resolved through feedback assistant, but unfortunately this doesn't seem to be the case.
I cross-posted this question on the apple developers forum and user nkalvi posted a workaround for this issue. I’ll post his example code here for future reference.
import SwiftUI
// Sample model definitions used to trigger navigation with navigationDestination API.
struct SampleModel1: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel1(), SampleModel1(), SampleModel1()]
}
struct SampleModel2: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel2(), SampleModel2(), SampleModel2()]
}
// The initial view loaded by the app. This will initialize the NavigationSplitView
struct ContentView: View {
#State var path = NavigationPath()
enum NavItem: Hashable, Equatable {
case first
}
var body: some View {
NavigationSplitView {
List {
NavigationLink(value: NavItem.first) {
Label("First", systemImage: "house")
}
}
} detail: {
SampleListView(path: $path)
}
}
}
// A list of SampleModel1 instances wrapped in a NavigationStack with multiple navigationDestinations
struct SampleListView: View {
// Get the selection from DetailView and append to path
// via .onChange
#State var selection2: SampleModel2? = nil
#Binding var path: NavigationPath
var body: some View {
NavigationStack(path: $path) {
VStack {
Text("Path: \(path.count)")
.padding()
List(SampleModel1.samples) { model in
NavigationLink("Model1: \(model.id)", value: model)
}
.navigationDestination(for: SampleModel2.self) { model in
Text("Model 2 ID \(model.id)")
.navigationTitle("navigationDestination(for: SampleModel2.self)")
}
.navigationDestination(for: SampleModel1.self) { model in
SampleDetailView(model: model, path: $path, selection2: $selection2)
.navigationTitle("navigationDestination(for: SampleModel1.self)")
}
.navigationTitle("First")
}
.onChange(of: selection2) { newValue in
path.append(newValue!)
}
}
}
}
// A detailed view of a single SampleModel1 instance. This includes a list
// of SampleModel2 instances that we would like to be able to navigate to
struct SampleDetailView: View {
var model: SampleModel1
#Binding var path: NavigationPath
#Binding var selection2: SampleModel2?
var body: some View {
NavigationStack {
Text("Path: \(path.count)")
.padding()
List(SampleModel2.samples, selection: $selection2) { model2 in
NavigationLink("Model2: \(model2.id)", value: model2)
// This also works (without .onChange):
// Button(model2.id.uuidString) {
// path.append(model2)
// }
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Trouble getting EnvironmentObject to update the UI

I originally posted another question asking this in the context of a project I was trying to develop, but I can't even get it to work in a vacuum so I figured I'd start with the basics. As the title suggests, my EnvironmentObjects don't update the UI as they should; in the following code, the user enters text on the ContentView and should be able to see that text in the next screen SecondView.
EDITED:
import SwiftUI
class NameClass: ObservableObject {
#Published var name = ""
}
struct ContentView: View {
#StateObject var myName = NameClass()
var body: some View {
NavigationView {
VStack {
TextField("Type", text: $myName.name)
NavigationLink(destination: SecondView()) {
Text("View2")
}
}
}.environmentObject(myName)
}
}
struct SecondView: View {
#EnvironmentObject var myself: NameClass
var body: some View {
Text("\(myself.name)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(NameClass())
}
}
However, the SecondView doesn't show the text that the user has written, but the default value of name (blank). What am I doing wrong here?
class NameClass: ObservableObject {
#Published var name = ""
}
struct ContentView: View {
#StateObject var myName = NameClass()
var body: some View {
NavigationView {
VStack {
TextField("Type", text: $myName.name)
NavigationLink(destination: SecondView()) {
Text("View2")
}
}
}.environmentObject(myName)
}
}
struct SecondView: View {
#EnvironmentObject var myself: NameClass
var body: some View {
Text("\(myself.name)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(NameClass())
}
}

Missing argument for parameter 'message' in call

I'm back with another question. I was following this guide: https://medium.com/#fs.dolphin/passing-data-between-views-in-swiftui-793817bba7b1
Everything worked from it, but the SecondView_Previews is throwing an error Missing argument for parameter 'message' in call. Here is my ContentView and SecondView
// ContentView
import SwiftUI
struct ContentView: View {
#State private var showSecondView = false
#State var message = "Hello from ContentView"
var body: some View {
VStack {
Button(action: {
self.showSecondView.toggle()
}){
Text("Go to Second View")
}.sheet(isPresented: $showSecondView){
SecondView(message: self.message)
}
Button(action: {
self.message = "hi"
}) {
Text("click me")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct SecondView: View {
#State var message: String
var body: some View {
Text("\(message)")
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView() // Error here: Missing argument for parameter 'message' in call.
}
}
It tried changing it to SecondView(message: String) and the error changes to "Cannot convert value of type 'String.Type' to expected argument type 'String'"
Can someone please explain what I'm doing wrong, or how to correctly set up the preview. It all works fine when there's no preview. Thanks in advance!
struct ContentView: View {
#State var message: String //Define type here
var body: some View {
Text("\(message)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(message: "Some text") //Passing value here
}
}

Why doesn't the Text update when using #Binding?

The Working works as expected.
But using the #Binding in the NotWorking example, doesn't seem to update the Text control. Why doesn't the #Binding version work, what am I missing here?
Initial Launch:
After Typing:
struct Working: View {
//Binding from #State updates both controls
#State private var text = "working"
var body: some View {
VStack {
TextField("syncs to label...", text: $text)
Text($text.wrappedValue)
}
}
}
struct NotWorking: View {
//Using the #Binding only updates the TextField
#Binding var text: String
var body: some View {
//This does not works
VStack {
TextField("won't sync to label...", text: $text)
Text($text.wrappedValue)
}
}
}
struct Working_Previews: PreviewProvider {
#State static var text = "not working"
static var previews: some View {
VStack {
Working()
NotWorking(text: $text)
}
}
}
Static #States don't work. It's the fact that it being static means that the struct Working_Previews isn't mutated when text is changed, so it won't refresh.
We can test this by changing from a PreviewProvider to an actual View:
struct ContentView: View {
#State static var text = "not working"
var body: some View {
VStack {
Working()
NotWorking(text: ContentView.$text)
}
}
}
This code gives the following runtime message:
Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.
Thanks to #George_E. I define #State in a wrapper view and display that for the preview. The WrapperView simply displays the control that I want to preview but it contains the State.
struct Working_Previews: PreviewProvider {
//Define the State in a wrapper view
struct WrapperView: View {
#State var text = "Preview is now working!!!"
var body: some View {
NotWorking(text: $text)
}
}
static var previews: some View {
VStack {
Working()
//Don't display the view that needs the #Binding
//NotWorking(text: $text)
//Use the WrapperView that has #State and displays the view I want to preview.
WrapperView()
}
}
}

Mutable Binding in SwiftUI Live Preview

I have a ChildView with a variable:
#Binding var itemName: String
In this ChildView I have few buttons that change value of the variable:
Button(action: {
self.itemName = "different value"
})
I was trying to use Preview like this:
struct ChildView_Previews: PreviewProvider {
static var previews: some View {
ChildView(itemName: "test")
}
}
But I am getting an error:
Cannot convert value of type 'String' to expected argument type
'Binding'
I am aware that I can use Preview like below. And the error will be gone and preview will work, but... itemName will have constant value, it will not be mutable now, not interactive in Live Preview:
struct ChildView_Previews: PreviewProvider {
static var previews: some View {
ChildView(itemName: .constant("test"))
}
}
How to declare a binding in SwiftUI Preview to make it interactive?
Updates to a #State variable in a PreviewProvider appear to not update the the read-only computed property previews directly. The solution is to wrap the #State variable in a test holder view. Then use this test view inside the previews property so the Live Preview refreshes correctly. Tested and working in Xcode 11.2.1.
struct ChildView: View {
#Binding var itemName: String
var body: some View {
VStack {
Text("Name: \(itemName)")
Button(action: {
self.itemName = "different value"
}) {
Text("Change")
}
}
}
}
struct ChildView_Previews: PreviewProvider {
struct BindingTestHolder: View {
#State var testItem: String = "Initial"
var body: some View {
ChildView(itemName: $testItem)
}
}
static var previews: some View {
BindingTestHolder()
}
}
If you need a value that can be changed in the live preview, I like to use this helper class:
struct BindingProvider<StateT, Content: View>: View {
#State private var state: StateT
private var content: (_ binding: Binding<StateT>) -> Content
init(_ initialState: StateT, #ViewBuilder content: #escaping (_ binding: Binding<StateT>) -> Content) {
self.content = content
self._state = State(initialValue: initialState)
}
var body: some View {
self.content($state)
}
}
Use it like so:
struct YourView_Previews: PreviewProvider {
static var previews: some View {
var yourVar = "example"
BindingProvider(yourVar) { binding in
YourView(initVar: binding)
}
}
}
This allows you to test changing the binding in the live preview.