in swiftui, I have a state variable count ,which is optional, in the sheet present ,I unwrap the optional and show Detailview, but it seems never hit there.
any idea how why not hit there?
it seems never hit
DetailView(count: num)
import SwiftUI
struct ContentView: View {
#State var showDetailView = false
#State var count : Int?
var testArr = [1,2,3,4,5]
var body: some View {
NavigationView {
List(testArr.indices){ indice in
Text("row num \(indice)")
.onTapGesture{
self.showDetailView = true
self.count = 5
}
}
.sheet(isPresented: self.$showDetailView) {
if let num = self.count{
//never hit here
DetailView(count: num)
}
}
.navigationBarTitle("Your Reading")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct DetailView: View {
var count: Int
var body: some View {
if count == 5 {
Text("5555")
} else {
EmptyView()
}
}
}
The issue is not the optional unwrapping.
The issue is that you are executing update to count while the body is being computed. To overcome this use my solution of:
swiftui state variable count lost what it stored
Let me know if that solution does not work for you.
You could use this workaround (not recommended), if you really have to use #State var count.
DispatchQueue.main.async {
self.count = 5
}
You can use sheet(item:content:) instead of sheet(isPresented:content:):
struct ContentView: View {
#State var count: Int?
var testArr = [1, 2, 3, 4, 5]
var body: some View {
NavigationView {
List(testArr.indices) { index in
Text("row num \(index)")
.onTapGesture {
self.count = 5
}
}
.sheet(item: $count) { count in
DetailView(count: count)
}
.navigationBarTitle("Your Reading")
}
}
}
Note that item must conform to Identifiable, so you need to either use your own struct or create an Int extension:
extension Int: Identifiable {
public var id: Int { self }
}
Related
I have very simple "app" in SwiftUI
How i can passing stepper value from list to struct SumOfValue or to ContentView ? But i want passing sum of stepper value in case from image will be 8.
struct ContentView: View {
var body: some View {
VStack{
List{
ProductList()
ProductList()
}
Spacer()
Text("Sum of stepper value: ?????")
.padding(.bottom, 50
)
SumOfValue()
}
}
}
struct ProductList:View {
#State var stepperValueTest: Int = 0
var body: some View {
HStack {
Stepper("Value: \(stepperValueTest)", value: $stepperValueTest)
}
}
}
struct SumOfValue: View {
var body: some View {
Text("or here sum of value: ????? ")
.foregroundColor(Color.red)
}
}
I try use #Binding but didn`t work.
There are multiple approaches here, and it's ultimately a question of data organization.
One way to think about is that there is an array of values that a parent - ContentView in your case - "owns", and each child updates their allotted value in that array using a binding. This way, the parent can easily calculate the sum of these values since it has the entire array.
struct ContentView: View {
#State var values = [0,0,0]
var body: some View {
VStack {
List {
ProductList(stepperValueTest: $values[0])
ProductList(stepperValueTest: $values[1])
ProductList(stepperValueTest: $values[2])
}
Text("Sum: \(sum)")
}
}
var sum: Int { values.reduce(0, +) }
}
struct ProductList:View {
#Binding var stepperValueTest: Int // change to Binding
var body: some View {
HStack {
Stepper("Value: \(stepperValueTest)", value: $stepperValueTest)
}
}
}
The #State goes in the parent (ContentView), and the #Binding goes in the child (ProductList and SumOfValue).
Try this:
struct ContentView: View {
/// States here!
#State var firstStepperValue: Int = 0
#State var secondStepperValue: Int = 0
var body: some View {
VStack{
List{
/// pass it in here!
ProductList(stepperValueTest: $firstStepperValue)
ProductList(stepperValueTest: $secondStepperValue)
}
Spacer()
/// add the values here
Text("Sum of stepper value: \(firstStepperValue + secondStepperValue)")
.padding(.bottom, 50
)
/// you can also pass it in here
SumOfValue(first: $firstStepperValue, second: $secondStepperValue)
.padding(.bottom, 100)
}
}
}
struct ProductList:View {
/// Binding here!
#Binding var stepperValueTest: Int
var body: some View {
HStack {
Stepper("Value: \(stepperValueTest)", value: $stepperValueTest)
}
}
}
struct SumOfValue: View {
#Binding var first: Int
#Binding var second: Int
var body: some View {
Text("or here sum of value: \(first + second) ")
.foregroundColor(Color.red)
}
}
Result:
The code below creates a simple HStack that ends up looking like this:
The problem is that hitting "increment" increments "Count" but not "Nested". Does anyone know why this is the case and possibly how to fix this? Or do SwiftUI views just fundamentally break when they are nested in a State variable?
struct ContentView: View {
var body: some View {
VStack {
Text("Count: \(count)")
nested
Button(action: {
self.count += 1
self.nested.count += 1
}) { Text("Increment") }
}
}
#State var count = 0
struct Nested: View {
var body: some View {
Text("Nested: \(count)")
}
#State var count = 0
}
#State var nested = Nested()
}
SwiftUI was designed to "nest" views, but you're not using it as intended. State variables are for data owned by the view, and a nested view isn't (or at least, not typically) meant to be a data owned by the view, so it need not be a state variable.
Instead, you can just the count variable as a parameter to the Nested view, and any time the count state variable changes in the parent, its body will be re-rendered:
struct ContentView: View {
var body: some View {
VStack {
Text("Count: \(count)")
Nested(count: count) // pass the count as an init param
Button(action: {
self.count += 1
self.nested.count += 1
}) { Text("Increment") }
}
}
#State var count = 0
struct Nested: View {
var body: some View {
Text("Nested: \(count)")
}
var count: Int
}
}
I use a modal sheet whose content is updated for each call. However, when the content is marked as #State, the view body is never updated.
Is anyone seeing this as well? Is a workaround available?
This is the calling view:
struct ContentView: View {
#State var isPresented = false
#State var i = 0
var body: some View {
List {
Button("0") {
self.i = 0
self.isPresented = true
}
Button("1") {
self.i = 1
self.isPresented = true
}
}
.sheet(
isPresented: $isPresented,
content: {
SheetViewNOK(i: self.i)
}
)
}
}
This does work:
struct SheetViewOK: View {
var i: Int
var body: some View {
Text("Hello \(i)") // String is always updated
}
}
This does not work. But obviously, in a real app, I need to use #State because changes made by the user need to be reflected in the sheet's content:
struct SheetViewNOK: View {
#State var i: Int
var body: some View {
Text("Hello \(i)") // String is never updated after creation
}
}
In your .sheet you are passing the value of your ContentView #State to a new #State. So it will be independent from the ContentView.
To create a connection or a binding of your ContentView #State value, you should define your SheetView var as #Binding. With this edit you will pass the binding of your state value to your sheet view.
struct SheetView: View {
#Binding var i: Int
var body: some View {
Text("Hello \(i)")
}
}
struct ContentView: View {
#State var isPresented = false
#State var i: Int = 0
var body: some View {
List {
Button("0") {
self.i = 0
self.isPresented = true
}
Button("1") {
self.i = 1
self.isPresented = true
}
}.sheet(
isPresented: $isPresented,
content: {
SheetView(i: self.$i)
})
}
}
There are 3 different ways
use a binding
use multiple .sheets
use $item
This is fully explained in this video.
Multiple Sheets in a SwiftUI View
I wanted to create quiet a simple app on watchOS 6, but after Apple has changed the ObjectBindig in Xcode 11 beta 5 my App does not run anymore. I simply want to synchronize data between two Views.
So I have rewritten my App with the new #Published, but I can't really set it up:
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1})
{
Text("Adder")
}
NavigationLink(destination: secondScreen()) {
Text("Next View")
}
}
}
}
struct secondScreen: View {
#ObservedObject var input = UserInput()
var body: some View {
VStack {
Text("Button has been pushed \(input.score)")
Button(action: {self.input.score += 1
}) {
Text("Adder")
}
}
}
}
Your code has a couple of errors:
1) You didn't put your ContentView in a NavigationView, so the navigation between the two views never happened.
2) You used data binding in a wrong way. If you need the second view to rely on some state belonging to the first view you need to pass a binding to that state to the second view. Both in your first view and in your second view you had an #ObservedObject created inline:
#ObservedObject var input = UserInput()
so, the first view and the second one worked with two totally different objects. Instead, you are interested in sharing the score between the views. Let the first view own the UserInput object and just pass a binding to the score integer to the second view. This way both the views will work on the same value (you can copy paste the code below and try yourself).
import SwiftUI
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
NavigationView {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1})
{
Text("Adder")
}
NavigationLink(destination: secondScreen(score: self.$input.score)) {
Text("Next View")
}
}
}
}
}
struct secondScreen: View {
#Binding var score: Int
var body: some View {
VStack {
Text("Button has been pushed \(score)")
Button(action: {self.score += 1
}) {
Text("Adder")
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
If you really need it you can even pass the entire UserInput object to the second view:
import SwiftUI
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View {
#ObservedObject var input = UserInput() //please, note the difference between this...
var body: some View {
NavigationView {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1})
{
Text("Adder")
}
NavigationLink(destination: secondScreen(input: self.input)) {
Text("Next View")
}
}
}
}
}
struct secondScreen: View {
#ObservedObject var input: UserInput //... and this!
var body: some View {
VStack {
Text("Button has been pushed \(input.score)")
Button(action: {self.input.score += 1
}) {
Text("Adder")
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
I tried a lot of different approaches on how to pass data from one view to another and came up with a solution that fits for simple and complex views / view models.
Version
Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)
This solution works with iOS 14.0 upwards, because you need the .onChange() view modifier. The example is written in Swift Playgrounds. If you need an onChange like modifier for lower versions, you should write your own modifier.
Main View
The main view has a #StateObject viewModel handling all of the views logic, like the button tap and the "data" (testingID: String) -> Check the ViewModel
struct TestMainView: View {
#StateObject var viewModel: ViewModel = .init()
var body: some View {
VStack {
Button(action: { self.viewModel.didTapButton() }) {
Text("TAP")
}
Spacer()
SubView(text: $viewModel.testingID)
}.frame(width: 300, height: 400)
}
}
Main View Model (ViewModel)
The viewModel publishes a testID: String?. This testID can be any kind of object (e.g. configuration object a.s.o, you name it), for this example it is just a string also needed in the sub view.
final class ViewModel: ObservableObject {
#Published var testingID: String?
func didTapButton() {
self.testingID = UUID().uuidString
}
}
So by tapping the button, our ViewModel will update the testID. We also want this testID in our SubView and if it changes, we also want our SubView to recognize and handle these changes. Through the ViewModel #Published var testingID we are able to publish changes to our view. Now let's take a look at our SubView and SubViewModel.
SubView
So the SubView has its own #StateObject to handle its own logic. It is completely separated from other views and ViewModels. In this example the SubView only presents the testID from its MainView. But remember, it can be any kind of object like presets and configurations for a database request.
struct SubView: View {
#StateObject var viewModel: SubviewModel = .init()
#Binding var test: String?
init(text: Binding<String?>) {
self._test = text
}
var body: some View {
Text(self.viewModel.subViewText ?? "no text")
.onChange(of: self.test) { (text) in
self.viewModel.updateText(text: text)
}
.onAppear(perform: { self.viewModel.updateText(text: test) })
}
}
To "connect" our testingID published by our MainViewModel we initialize our SubView with a #Binding. So now we have the same testingID in our SubView. But we don't want to use it in the view directly, instead we need to pass the data into our SubViewModel, remember our SubViewModel is a #StateObject to handle all the logic. And we can't pass the value into our #StateObject during view initialization. Also if the data (testingID: String) changes in our MainViewModel, our SubViewModel should recognize and handle these changes.
Therefore we are using two ViewModifiers.
onChange
.onChange(of: self.test) { (text) in
self.viewModel.updateText(text: text)
}
The onChange modifier subscribes to changes in our #Binding property. So if it changes, these changes get passed to our SubViewModel. Note that your property needs to be Equatable. If you pass a more complex object, like a Struct, make sure to implement this protocol in your Struct.
onAppear
We need onAppear to handle the "first initial data" because onChange doesn't fire the first time your view gets initialized. It is only for changes.
.onAppear(perform: { self.viewModel.updateText(text: test) })
Ok and here is the SubViewModel, nothing more to explain to this one I guess.
class SubviewModel: ObservableObject {
#Published var subViewText: String?
func updateText(text: String?) {
self.subViewText = text
}
}
Now your data is in sync between your MainViewModel and SubViewModel and this approach works for large views with many subviews and subviews of these subviews and so on. It also keeps your views and corresponding viewModels enclosed with high reusability.
Working Example
Playground on GitHub:
https://github.com/luca251117/PassingDataBetweenViewModels
Additional Notes
Why I use onAppear and onChange instead of only onReceive: It appears that replacing these two modifiers with onReceive leads to a continuous data stream firing the SubViewModel updateText multiple times. If you need to stream data for presentation, it could be fine but if you want to handle network calls for example, this can lead to problems. That's why I prefer the "two modifier approach".
Personal Note: Please don't modify the StateObject outside the corresponding view's scope. Even if it is somehow possible, it is not what its meant for.
My question is still related to how to pass data between two views but I have a more complicated JSON data set and I am running into problems both with the passing the data and with it's initialization. I have something that works but I am sure it is not correct. Here is the code. Help!!!!
/ File: simpleContentView.swift
import SwiftUI
// Following is the more complicated #ObservedObject (Buddy and class Buddies)
struct Buddy : Codable, Identifiable, Hashable {
var id = UUID()
var TheirNames: TheirNames
var dob: String = ""
var school: String = ""
enum CodingKeys1: String, CodingKey {
case id = "id"
case Names = "Names"
case dob = "dob"
case school = "school"
}
}
struct TheirNames : Codable, Identifiable, Hashable {
var id = UUID()
var first: String = ""
var middle: String = ""
var last: String = ""
enum CodingKeys2: String, CodingKey {
case id = "id"
case first = "first"
case last = "last"
}
}
class Buddies: ObservableObject {
#Published var items: [Buddy] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {UserDefaults.standard.set(encoded, forKey: "Items")}
}
}
#Published var buddy: Buddy
init() {
if let items = UserDefaults.standard.data(forKey: "Items") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([Buddy].self, from: items) {
self.items = decoded
// ??? How to initialize here
self.buddy = Buddy(TheirNames: TheirNames(first: "c", middle: "r", last: "c"), dob: "1/1/1900", school: "hard nocks")
return
}
}
// ??? How to initialize here
self.buddy = Buddy(TheirNames: TheirNames(first: "c", middle: "r", last: "c"), dob: "1/1/1900", school: "hard nocks")
self.items = []
}
}
struct simpleContentView: View {
#Environment(\.presentationMode) var presentationMode
#State private var showingSheet = true
#ObservedObject var buddies = Buddies()
var body: some View {
VStack {
Text("Simple View")
Button(action: {self.showingSheet.toggle()}) {Image(systemName: "triangle")
}.sheet(isPresented: $showingSheet) {
simpleDetailView(buddies: self.buddies, item: self.buddies.buddy)}
}
}
}
struct simpleContentView_Previews: PreviewProvider {
static var previews: some View {
simpleContentView()
}
}
// End of File: simpleContentView.swift
// This is in a separate file: simpleDetailView.swift
import SwiftUI
struct simpleDetailView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var buddies = Buddies()
var item: Buddy
var body: some View {
VStack {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
Text("First Name = \(item.TheirNames.first)")
Button(action: {self.presentationMode.wrappedValue.dismiss()}){ Text("return"); Image(systemName: "gobackward")}
}
}
}
// ??? Correct way to make preview call
struct simpleDetailView_Previews: PreviewProvider {
static var previews: some View {
// ??? Correct way to call here
simpleDetailView(item: Buddy(TheirNames: TheirNames(first: "", middle: "", last: ""), dob: "", school: "") )
}
}
// end of: simpleDetailView.swift
Using directly #State variable will help you to achieve this, but if you want to sync that variable for both the screens using view model or #Published, this is what you can do. As the #State won't be binded to the #Published property. To achieve this follow these steps.
Step1: - Create a delegate to bind the value on pop or disappearing.
protocol BindingDelegate {
func updateOnPop(value : Int)
}
Step 2:- Follow the code base for Content View
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View , BindingDelegate {
#ObservedObject var input = UserInput()
#State var navIndex : Int? = nil
var body: some View {
NavigationView {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1}) {
Text("Adder")
}
ZStack {
NavigationLink(destination: secondScreen(score: self.$input.score,
del: self, navIndex: $navIndex),
tag: 1, selection: $navIndex) {
EmptyView()
}
Button(action: {
self.navIndex = 1
}) {
Text("Next View")
}
}
}
}
}
func updateOnPop(value: Int) {
self.input.score = value
}
}
Step 3: Follow these steps for secondScreen
final class ViewModel : ObservableObject {
#Published var score : Int
init(_ value : Int) {
self.score = value
}
}
struct secondScreen: View {
#Binding var score: Int
#Binding var navIndex : Int?
#ObservedObject private var vm : ViewModel
var delegate : BindingDelegate?
init(score : Binding<Int>, del : BindingDelegate, navIndex : Binding<Int?>) {
self._score = score
self._navIndex = navIndex
self.delegate = del
self.vm = ViewModel(score.wrappedValue)
}
private var btnBack : some View { Button(action: {
self.delegate?.updateOnPop(value: self.vm.score)
self.navIndex = nil
}) {
HStack {
Text("Back")
}
}
}
var body: some View {
VStack {
Text("Button has been pushed \(vm.score)")
Button(action: {
self.vm.score += 1
}) {
Text("Adder")
}
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: btnBack)
}
}
My code is a little more complex than this so I created an example that gets the same error.
When I navigate into a view, I have a function I want to perform with a variable passed into this view. That function then produces an array. I then want to put that array into a List, but I get an error.
How do I get the List to show the produced array?
I think the issue is the List can't be updated because it already has the declared blank array.
struct ContentView : View {
#State var array = [String]()
var body: some View {
List(self.array,id: \.self) { item in
Text("\(item)")
}
.onAppear(perform: createArrayItems)
}
func createArrayItems() {
array = ["item1", "item2", "item3", "item4", "item5"]
}
}
You can use ObservableObject data providers(eg : ViewModel) with #Published properties.
struct ListView: View {
#ObservedObject var viewModel = ListViewModel()
var body: some View {
NavigationView {
List(){
ForEach(viewModel.items) { item in
Text(item)
}
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
#endif
class ListViewModel: ObservableObject {
#Published var items = ["item1", "item2", "item3", "item4", "item5","item6"]
func addItem(){
items.append("item7")
}
}
You can use combine framework to update the list.
Whenever a change is made in DataProvider Object it will automatically update the list.
struct ContentView : View {
#EnvironmentObject var data: DataProvider
var body: some View {
NavigationView {
NavigationButton(destination: SecondPage()) {
Text("Go to Second Page")
}
List {
ForEach(data.array.identified(by: \.self)) { item in
Text("\(item)")
}
}
}
}
}
Add items in the list
struct SecondPage : View {
#State var counter = 1
#EnvironmentObject var tempArray: DataProvider
var body: some View {
VStack {
Button(action: {
self.tempArray.array.append("item\(self.counter)")
self.counter += 1
}) {
Text("Add items")
}
Text("Number of items added \(counter-1)")
}
}
}
It will simply notify the change
import Combine
final class DataProvider: BindableObject {
let didChange = PassthroughSubject<DataProvider, Never>()
var array = [String]() {
didSet {
didChange.send(self)
}
}
}
You also need to do some update in the SceneDelegate. This update ensures that ContentView has a DataProvider object in the environment.
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(DataProvider()))
#txagPman
I too have your problem to understand how to modify a list.
I was able to write this code.
I hope it's useful.
import SwiftUI
struct ContentView: View {
#State private var array = createArrayItems()
// #State private var array = [""] - This work
// #State private var array = [] - This not work
#State private var text = ""
var body: some View {
VStack {
TextField("Text", text: $text, onCommit: {
// self.array = createArrayItems() - This work after press return on textfield
self.array.append(self.text)
}).padding()
List (self.array, id: \.self) {item in
Text("\(item)")
}
}
// .onAppear {
// self.array = createArrayItems() - This not work
// }
}
}
func createArrayItems() -> [String] {
return ["item_01","item_02","item_03","item_04" ]
}
A dumb UI is a good UI
Keep your views dumb try the following code to create a dynamic List
import UIKit
import SwiftUI
import PlaygroundSupport
struct ContentView : View {
#State var array = [String]()
var body: some View {
List{
ForEach(array.identified(by: \.self)) { item in
Text("\(item)")
}
}
}
}
func createArrayItems()->[String] {
return ["item1", "item2", "item3", "item4", "item5","item6"]
}
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView(array: createArrayItems()))
Use this:
class ObservableArray<T>: ObservableObject {
#Published var array: [T]
init(array: [T] = ) {
self.array = array
}
init(repeating value: T, count: Int) {
array = Array(repeating: value, count: count)
}
}
struct YourView: View {
#ObservedObject var array = ObservableArray<String>()
var body: some View {
}
}