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

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
}
}
}

Related

Is this the right way for using #ObservedObject and #EnvironmentObject?

Cow you give me some confirmation about my understanding about #ObservedObject and #EnvironmentObject?
In my mind, using an #ObservedObject is useful when we send data "in line" between views that are sequenced, just like in "prepare for" in UIKit while using #EnvironmentObject is more like "singleton" in UIKit. My question is, is my code making the right use of these two teniques? Is this the way are applied in real development?
my model used as brain for funcions (IE urls sessions, other data manipulations)
class ModelClass_ViaObservedObject: ObservableObject {
#Published var isOn: Bool = true
}
class ModelClass_ViaEnvironment: ObservableObject {
#Published var message: String = "default"
}
my main view
struct ContentView: View {
//way to send data in views step by step
#StateObject var modelClass_ViaObservedObject = ModelClass_ViaObservedObject()
//way to share data more or less like a singleton
#StateObject var modelClass_ViaEnvironment = ModelClass_ViaEnvironment()
var myBackgroundColorView: Color {
if modelClass_ViaObservedObject.isOn {
return Color.green
} else {
return Color.red
}
}
var body: some View {
NavigationView {
ZStack {
myBackgroundColorView
VStack {
NavigationLink(destination:
SecondView(modelClass_viaObservedObject: modelClass_ViaObservedObject)
) {
Text("Go to secondary view")
.padding()
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(.black, lineWidth: 1)
)
}
Text("text received from second view: \(modelClass_ViaEnvironment.message)")
}
}
.navigationTitle("Titolo")
.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(modelClass_ViaEnvironment)
}
}
my second view
struct SecondView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var modelClass_viaObservedObject: ModelClass_ViaObservedObject
//global data in environment, not sent step by step view by view
#EnvironmentObject var modelClass_ViaEnvironment: ModelClass_ViaEnvironment
var body: some View {
VStack(spacing: 5) {
Text("Second View")
Button("change bool for everyone") {
modelClass_viaObservedObject.isOn.toggle()
dismiss()
}
TextField("send back", text: $modelClass_ViaEnvironment.message)
Text(modelClass_ViaEnvironment.message)
}
}
}
No, we use #State for view data like if a toggle isOn, which can either be a single value itself or a custom struct containing multiple values and mutating funcs. We pass it down the View hierarchy by declaring a let in the child View or use #Binding var if we need write access. Regardless of if we declare it let or #Binding whenever a different value is passed in to the child View's init, SwiftUI will call body automatically (as long as it is actually accessed in body that is).
#StateObject is for when a single value or a custom struct won't do and we need a reference type instead for view data, i.e. if persisting or syncing data (not using the new async/await though because we use .task for that). The object is init before body is called (usually before it is about to appear) and deinit when the View is no longer needed (usually after it disappears).
#EnvironmentObject is usually for the store object that holds model structs in #Published properties and is responsible for saving or syncing,. The difference is the model data is not tied to any particular View, like #State and #StateObject are for view data. This object is usually a singleton, one for the app and one with sample data for when previewing, because it should never be deinit. The advantage of #EnvironmentObject over #ObservedObject is we don't need to pass it down through each View as a let that don't need the object when we only need it further down the hierarchy. Note the reason it has to be passed down as a let and not #ObservedObject is then body would be needlessly called in the intermediate Views because SwiftUI's dependency tracking doesn't work for objects only value types.
Here is some sample code:
struct MyConfig {
var isOn = false
var message = ""
mutating func reset() {
isOn = false
message = ""
}
}
struct MyView: View {
#State var config = MyConfig() // grouping vars into their struct makes use of value semantics to track changes (a change to any of its properties is detected as a change to the struct itself) and offers testability.
var body: some View {
HStack {
ViewThatOnlyReads(config: config)
ViewThatWrites(config: $config)
}
}
}
struct ViewThatOnlyReads: View {
let config: MyConfig
var body: some View {
Text(config.isOn ? "It's on" : "It's off")
}
}
struct ViewThatWrites: View {
#Binding var config: MyConfig
var body: some View {
Toggle("Is On", isOn: $config.isOn)
}
}

Is it possible to reload only modified item in SwiftUI List/ForEach that uses #Binding

Simple sample code with toggle button (slightly modified from hackingwithswift:
This code(hackingwithswift original and my version) IS redrawing every list cell whenever any toggle happens. I modified code to better debug view drawing.
import SwiftUI
struct User: Identifiable {
let id = UUID()
var name: String
var isContacted = false
}
struct ProfileView: View {
#State private var users = [
User(name: "Taylor"),
User(name: "Justin"),
User(name: "Adele")
]
var body: some View {
let _ = Self._printChanges()
List($users) { $user in
ProfileCell(user: $user)
}
}
}
struct ProfileCell: View{
#Binding var user: User
var body: some View{
let _ = Self._printChanges()
Text(user.name)
Spacer()
Toggle("User has been contacted", isOn: $user.isContacted)
.labelsHidden()
}
}
Running app and toggling will print following in console for every toggle:
ProfileView: _users changed.
ProfileCell: #self, _user changed.
ProfileCell: #self, _user changed.
ProfileCell: #self, _user changed.
Hackingwithswift tutorial states "Using a binding in this way is the most efficient way of modifying the list, because it won’t cause the entire view to reload when only a single item changes.", however that does not seem to be true.
Is it possible to redraw only item that was changed?
Theoretically it should be working, but it seems they changed something since first introduction, because now on state change they recreate(!) bindings (all of them), so automatic view changes handler interpret that as view update (binding is a property after all).
A possible workaround for this is to help rendering engine and check view equitability manually.
Tested with Xcode 13.4 / iOS 15.5
Main parts:
// 1
List($users) { $user in
EquatableView(content: ProfileCell(user: $user)) // << here !!
}
// 2
struct ProfileCell: View, Equatable {
static func == (lhs: ProfileCell, rhs: ProfileCell) -> Bool {
lhs.user == rhs.user
}
// ...
// 3
struct User: Identifiable, Equatable {
Test module is here

SwiftUI clean up ContentView

I'm trying to simplify the ContentView within a project and I'm struggling to understand how to move #State based logic into its own file and have ContentView adapt to any changes. Currently I have dynamic views that display themselves based on #Binding actions which I'm passing the $binding down the view hierarchy to have buttons toggle the bool values.
Here's my current attempt. I'm not sure how in SwiftUI to change the view state of SheetPresenter from a nested view without passing the $binding all the way down the view stack. Ideally I'd like it to look like ContentView.overlay(sheetPresenter($isOpen, $present).
Also, I'm learning SwiftUI so if this isn't the best approach please provide guidance.
class SheetPresenter: ObservableObject {
#Published var present: Present = .none
#State var isOpen: Bool = false
enum Present {
case none, login, register
}
#ViewBuilder
func makeView(with presenter: Present) -> some View {
switch presenter {
case .none:
EmptyView()
case .login:
BottomSheetView(isOpen: $isOpen, maxHeight: UIConfig.Utils.screenHeight * 0.75) {
LoginScreen()
}
case .register:
BottomSheetView(isOpen: $isOpen, maxHeight: UIConfig.Utils.screenHeight * 0.75) {
RegisterScreen()
}
}
}
}
if you don't want to pass $binding all the way down the view you can create a StateObject variable in the top view and pass it with .environmentObject(). and access it from any view with EnvironmentObject
struct testApp: App {
#StateObject var s1: sViewModel = sViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(s1)
}
}
}
You are correct this is not the best approach, however it is a common mistake. In SwiftUI we actually use #State for transient data owned by the view. This means using a value type like a struct, not classes. This is explained at 4:18 in Data Essentials in SwiftUI from WWDC 2020.
EditorConfig can maintain invariants on its properties and be tested
independently. And because EditorConfig is a value type, any change to
a property of EditorConfig, like its progress, is visible as a change
to EditorConfig itself.
struct EditorConfig {
var isEditorPresented = false
var note = ""
var progress: Double = 0
mutating func present(initialProgress: Double) {
progress = initialProgress
note = ""
isEditorPresented = true
}
}
struct BookView: View {
#State private var editorConfig = EditorConfig()
func presentEditor() { editorConfig.present(…) }
var body: some View {
…
Button(action: presentEditor) { … }
…
}
}
Then you just use $editorConfig.isEditorPresented as the boolean binding in .sheet or .overlay.
Worth also taking a look at sheet(item:onDismiss:content:) which makes it much simpler to show an item because no boolean is required it uses an optional #State which you can set to nil to dismiss.

SwiftUI Navigation popping back when modifying list binding property in a pushed view

When I update a binding property from an array in a pushed view 2+ layers down, the navigation pops back instantly after a change to the property.
Xcode 13.3 beta, iOS 15.
I created a simple demo and code is below.
Shopping Lists
List Edit
List section Edit
Updating the list title (one view deep) is fine, navigation stack stays same, and changes are published if I return. But when adjusting a section title (two deep) the navigation pops back as soon as I make a single change to the property.
I have a feeling I'm missing basic fundamentals here, and I have a feeling it must be related to the lists id? but I'm struggling to figure it out or work around it.
GIF
Code:
Models:
struct ShoppingList {
let id: String = UUID().uuidString
var title: String
var sections: [ShoppingListSection]
}
struct ShoppingListSection {
let id: String = UUID().uuidString
var title: String
}
View Model:
final class ShoppingListsViewModel: ObservableObject {
#Published var shoppingLists: [ShoppingList] = [
.init(
title: "Shopping List 01",
sections: [
.init(title: "Fresh food")
]
)
]
}
Content View:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}
}
}
ShoppingListsView
struct ShoppingListsView: View {
#StateObject private var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
ShoppingListEditView
struct ShoppingListEditView: View {
#Binding var shoppingList: ShoppingList
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("Title", text: $shoppingList.title)
}
Section(header: Text("Sections")) {
List($shoppingList.sections, id: \.id) { $section in
NavigationLink(destination: ShoppingListSectionEditView(section: $section)) {
Text(section.title)
}
}
}
}
.navigationBarTitle("Edit list")
}
}
ShoppingListSectionEditView
struct ShoppingListSectionEditView: View {
#Binding var section: ShoppingListSection
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("title", text: $section.title)
}
}
.navigationBarTitle("Edit section")
}
}
try this, works for me:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}.navigationViewStyle(.stack) // <--- here
}
}
Try to make you object confirm to Identifiable and return value which unique and stable, for your case is ShoppingList.
Detail view seems will pop when object id changed.
The reason your stack is popping back to the root ShoppingListsView is that the change in the list is published and the root ShoppingListsView is registered to listen for updates to the #StateObject.
Therefore, any change to the list is listened to by ShoppingListsView, causing that view to be re-rendered and for all new views on the stack to be popped in order to render the root ShoppingListsView, which is listening for updates on the #StateObject.
The solution to this is to change the #StateObject to #EnvironmentObject
Please refactor your code to change ShoppingListsViewModel to use an #EnvironmentObject wrapper instead of a #StateObject wrapper
You may pass the environment object in to all your child views and also add a boolean #Published flag to track any updates to the data.
Then your ShoppingListView would look as below
struct ShoppingListsView: View {
#EnvironmentObject var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
Don't forget to pass the viewModel in to all your child views.
That should fix your problem.

How do I access data from a child view as the parent view at any time in SwiftUI?

I'm new to SwiftUI and understand that I may need to implement EnvironmentObject in some way, but I'm not sure how in this case.
This is the Trade class
class Trade {
var teamsSelected: [Team]
init(teamsSelected: [Team]) {
self.teamsSelected = teamsSelected
}
}
This is the child view. It has an instance trade from the Trade class. There is a button that appends 1 to array teamsSelected.
struct TeamRow: View {
var trade: Trade
var body: some View {
Button(action: {
self.trade.teamsSelected.append(1)
}) {
Text("Button")
}
}
}
This is the parent view. As you can see, I pass trade into the child view TeamRow. I want trade to be in sync with trade in TeamRow so that I can then pass trade.teamsSelected to TradeView.
struct TeamSelectView: View {
var trade = Trade(teamsSelected: [])
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: TradeView(teamsSelected: trade.teamsSelected)) {
Text("Trade")
}
List {
ForEach(teams) { team in
TeamRow(trade: self.trade)
}
}
}
}
}
}
I've taken your code and changed some things to illustrate how SwiftUI works in order to give you a better understanding of how to use ObservableObject, #ObservedObject, #State, and #Binding.
One thing to mention up front - #ObservedObject is currently broken when trying to run SwiftUI code on a physical device running iOS 13 Beta 6, 7, or 8. I answered a question here that goes into that in more detail and explains how to use #EnvironmentObject as a workaround.
Let's first take a look at Trade. Since you're looking to pass a Trade object between views, change properties on that Trade object, and then have those changes reflected in every view that uses that Trade object, you'll want to make Trade an ObservableObject. I've added an extra property to your Trade class purely for illustrative purposes that I'll explain later. I'm going to show you two ways to write an ObservableObject - the verbose way first to see how it works, and then the concise way.
import SwiftUI
import Combine
class Trade: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
var name: String {
willSet {
self.objectWillChange.send()
}
}
var teamsSelected: [Int] {
willSet {
self.objectWillChange.send()
}
}
init(name: String, teamsSelected: [Int]) {
self.name = name
self.teamsSelected = teamsSelected
}
}
When we conform to ObservableObject, we have the option to write our own ObservableObjectPublisher, which I've done by importing Combine and creating a PassthroughSubject. Then, when I want to publish that my object is about to change, I can call self.objectWillChange.send() as I have on willSet for name and teamsSelected.
This code can be shortened significantly, however. ObservableObject automatically synthesizes an object publisher, so we don't actually have to declare it ourselves. We can also use #Published to declare our properties that should send a publisher event instead of using self.objectWillChange.send() in willSet.
import SwiftUI
class Trade: ObservableObject {
#Published var name: String
#Published var teamsSelected: [Int]
init(name: String, teamsSelected: [Int]) {
self.name = name
self.teamsSelected = teamsSelected
}
}
Now let's take a look at your TeamSelectView, TeamRow, and TradeView. Keep in mind once again that I've made some changes (and added an example TradeView) just to illustrate a couple of things.
struct TeamSelectView: View {
#ObservedObject var trade = Trade(name: "Name", teamsSelected: [])
#State var teams = [1, 1, 1, 1, 1]
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: TradeView(trade: self.trade)) {
Text(self.trade.name)
}
List {
ForEach(self.teams, id: \.self) { team in
TeamRow(trade: self.trade)
}
}
Text("\(self.trade.teamsSelected.count)")
}
.navigationBarItems(trailing: Button("+", action: {
self.teams.append(1)
}))
}
}
}
struct TeamRow: View {
#ObservedObject var trade: Trade
var body: some View {
Button(action: {
self.trade.teamsSelected.append(1)
}) {
Text("Button")
}
}
}
struct TradeView: View {
#ObservedObject var trade: Trade
var body: some View {
VStack {
Text("\(self.trade.teamsSelected.count)")
TextField("Trade Name", text: self.$trade.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
}
}
Let's first look at #State var teams. We use #State for simple value types - Int, String, basic structs - or collections of simple value types. #ObservedObject is used for objects that conform to ObservableObject, which we use for data structures that are more complex than just Int or String.
What you'll notice with #State var teams is that I've added a navigation bar item that will append a new item to the teams array when pressed, and since our List is generated by iterating through that teams array, our view re-renders and adds a new item to our List whenever the button is pressed. That's a very basic example of how you would use #State.
Next, we have our #ObservedObject var trade. You'll notice that I'm not really doing anything different than you were originally. I'm still creating an instance of my Trade class and passing that instance between my views. But since it's now an ObservableObject, and we're using #ObservedObject, our views will now all receive publisher events whenever the Trade object changes and will automatically re-render their views to reflect those changes.
The last thing I want to point out is the TextField I created in TradeView to update the Trade object's name property.
TextField("Trade Name", text: self.$trade.name)
The $ character indicates that I'm passing a binding to the text field. This means that any changes TextField makes to name will be reflected in my Trade object. You can do the same thing yourself by declaring #Binding properties that allow you to pass bindings between views when you are trying to sync state between your views without passing entire objects.
While I changed your TradeView to take #ObservedObject var trade, you could simply pass teamsSelected to your trade view as a binding like this - TradeView(teamsSelected: self.$trade.teamsSelected) - as long as your TradeView accepts a binding. To configure your TradeView to accept a binding, all you would have to do is declare your teamsSelected property in TradeView like this:
#Binding var teamsSelected: [Team]
And lastly, if you run into issues with using #ObservedObject on a physical device, you can refer to my answer here for an explanation of how to use #EnvironmentObject as a workaround.
You can use #Binding and #State / #Published in Combine.
In other words, use a #Binding property in Child View and bind it with a #State or a #Published property in Parent View as following.
struct ChildView: View {
#Binding var property1: String
var body: some View {
VStack(alignment: .leading) {
TextField(placeholderTitle, text: $property1)
}
}
}
struct PrimaryTextField_Previews: PreviewProvider {
static var previews: some View {
PrimaryTextField(value: .constant(""))
}
}
struct ParentView: View{
#State linkedProperty: String = ""
//...
ChildView(property1: $linkedProperty)
//...
}
or if you have a #Publilshed property in your viewModel(#ObservedObject), then use it to bind the state like ChildView(property1: $viewModel.publishedProperty).
firstly thanks a lot to graycampbell for giving me a better understanding! However, my understanding does not seem to work out completely. I have a slightly different case which I'm not fully able to solve.
I've already asked my question in a separate thread, but I want to add it here as well, because it somehow fits the topic: Reading values from list of toggles in SwiftUI
Maybe somebody of you guys can help me with this. The main difference to the initial post if this topic is, that I have to collect Data from each GameGenerationRow in the GameGenerationView and then hand it over to another View.