How to get a Value in a Class before the View begins? | SwiftUI - swift

I know wired question, but I dont know how to name the Problem, I am very new to Swift and I am trying to get the #ObservedObject messagesManager(HERE) changed to the ChatId both is coming within a navigation Link, so I have both informations, but I can't merge them before the View starts, and I dont want to have a button to update it, when the View loads, it should change to the new Value. Here the code:
import SwiftUI
struct ChatView: View {
#State var infos : SessionServiceImpl
#State var ChatId : String
#ObservedObject var messagesManager = messagingManager(chatID: "")
var body: some View {
VStack {
VStack {
TitleRow(infos: infos)
ScrollViewReader { proxy in
ScrollView{
ForEach(messagesManager.messages, id: \.id) { message in
MessageBubbleView(infos: infos, message: message)
}
}
When the chatID gets changed it will change the Result in the ForEach. I hope u understand my Problem, because currently it just uses the empty Placeholder.

Related

Bindings from the sheet modifier

I am using the SwiftUI sheet modifier on a View that has a collection published from its view model. When a user clicks on a list item, I am setting an optional State value to the corresponding item from the list. The sheet modifier uses the $selectedSubscription value to present and hide the sheet when needed, and passes the value to the closure in order for me to use it in a details view. I have set up the details view to receive a #Binding to a list item in order to edit it at a later stage. The problem is that SwiftUI will not let me pass the item in the closure as a binding.
struct SubscriptionsListView: View {
#StateObject var vm = SubscriptionsListVM()
#State private var selectedSubscription: PVSubscription? = nil
var body: some View {
NavigationView {
ZStack {
...
}
.sheet(item: $selectedSubscription) { subscription in
SubscriptionDetailsView(subscription: $subscription) <--- error here
.presentationDetents([.medium, .large])
}
}
}
}
struct SubscriptionDetailsView: View {
#Binding var subscription: PVSubscription
...
}
I already tried to get the index of the item from the array and to pass that as a binding, but I was unsuccessful.
The problem arises from the use of the sheet modifier. You are trying to use the "selectedSubscription" variable both for showing a sheet and showing the details of your model. You have to create another variable for showing the sheet.
Since your sheet only shows one view, you don't need the sheet modifier with the item argument. Item argument is used for showing different views in the same sheet modifier (settings, profile etc.). It doesn't give you a binding in the trailing closure to put into the subview.
struct SubscriptionsListView: View {
#StateObject var vm = SubscriptionsListVM()
#State private var selectedSubscription: PVSubscription? = nil
#State private var showSheet: Bool = false
var body: some View {
NavigationView {
ZStack {
... // Toggle show sheet here
}
.sheet(isPresented: $showSheet) {
SubscriptionDetailsView(subscription: $selectedSubscription)
.presentationDetents([.medium, .large])
}
}
}
}

SwiftUI NavigationView popping when observed object changes

There are three views. Main view, a List view (displaying a list of locations) and a Content view (displaying a single location). Both the List view and the Content view are watching for changes from LocationManager(GPS position).
If the user NavigateLinks to Content view from the List view and then the observed LocationManager changes then ContentView is popped off and the List view is displayed.
I have NavigationViewStyle set to .stack (which some found to help) but it had no effect.
Does anyone know what I am doing wrong?
// Main view
#StateObject var locations = Locations()
var body: some Scene {
WindowGroup {
TabView(){
NavigationView {
ListView()
}.navigationViewStyle(.stack).tabItem {
Text("List")
}
}.environmentObject(locations)
}
}
// List view
#EnvironmentObject var locations: Locations
#ObservedObject private var locationManager = LocationManager()
var body: some View {
List(){
ForEach(locations, id: \.id) {
loc in
NavigationLink(destination: ContentView(location: loc, locationManager: locationManager)) {
Text(loc.name)
}.isDetailLink(false)
}
}.listStyle(PlainListStyle())
}.navigationTitle("List")
// Content/Detail view
let location: Location
let locationManager: LocationManager
var body: some View { Text("I am detail") }
What I ended up doing was having two observable objects, one for the List and one for Detail.
The observable objects are GPS coordinates so that when the Content view is displayed (you can keep a track of that by adding a state in onAppear for the View) you don't allow changes to the observable object for the List.
In other words the observable object that the List view had, didn't change if the Content view was visible. I did this in code by simply ignoring any location updates in that object if the current view was Content/Detail.
This worked perfectly.
So instead of this:
I did this
It pops because your objects are not unique. Think about this:
You are inside a view called Alen now you refresh that view and it's called Alenx, SwiftUI pops you back because the data is different.
The way to make it stay is to make the elements unique (Identifiable), for this you can assign some unique id to your models, here's 1 way of doing it:
struct Example: Identifiable {
let id = UUID()
var someValue: String
private enum CodingKeys: String, CodingKey { case someValue }
}

#Published in an ObservableObject vs #State on a View leads to unpredictable update behavior in SwiftUI

This question is coming on the heels of this question that I asked (and had answered by #Asperi) yesterday, but it introduces a new unexpected element.
The basic setup is a 3 column macOS SwiftUI app. If you run the code below and scroll the list to an item further down the list (say item 80) and click, the List will re-render and occasionally "jump" to a place (like item 40), leaving the actual selected item out of frame. This issue was solved in the previous question by encapsulating SidebarRowView into its own view.
However, that solution works if the active binding (activeItem) is stored as a #State variable on the SidebarList view (see where I've marked //#1). If the active item is stored on an ObservableObject view model (see //#2), the scrolling behavior is affected.
I assume this is because the diffing algorithm somehow works differently with the #Published value and the #State value. I'd like to figure out a way to use the #Published value since the active item needs to be manipulated by the state of the app and used in the NavigationLink via isActive: (say if a push notification comes in that affects it).
Is there a way to use the #Published value and not have it re-render the whole List and thus not affect the scrolled position?
Reproducible code follows -- see the commented line for what to change to see the behavior with #Published vs #State
struct Item : Identifiable, Hashable {
let id = UUID()
var name : String
}
class SidebarListViewModel : ObservableObject {
#Published var items = Array(0...300).map { Item(name: "Item \($0)") }
#Published var activeItem : Item? //#2
}
struct SidebarList : View {
#StateObject private var viewModel = SidebarListViewModel()
#State private var activeItem : Item? //#1
var body: some View {
List(viewModel.items) {
SidebarRowView(item: $0, activeItem: $viewModel.activeItem) //change this to $activeItem and the scrolling works as expected
}.listStyle(SidebarListStyle())
}
}
struct SidebarRowView: View {
let item: Item
#Binding var activeItem: Item?
func navigationBindingForItem(item: Item) -> Binding<Bool> {
.init {
activeItem == item
} set: { newValue in
if newValue {
activeItem = item
}
}
}
var body: some View {
NavigationLink(destination: Text(item.name),
isActive: navigationBindingForItem(item: item)) {
Text(item.name)
}
}
}
struct ContentView : View {
var body: some View {
NavigationView {
SidebarList()
Text("No selection")
Text("No selection")
.frame(minWidth: 300)
}
}
}
(Built and tested with Xcode 13.0 on macOS 11.3)
Update. I still think that the original answer identified the problem, however seems that there's an even easier workaround to this: push the view model one level upstream, to the root ContentView, and inject the items array to the SidebarList view.
Thus, the following changes should fix the "jumping" issue:
struct SidebarList : View {
let items: [Item]
#Binding var activeItemId: UUID?
// ...
}
// ...
struct ContentView : View {
#StateObject private var viewModel = SidebarListViewModel()
var body: some View {
NavigationView {
SidebarList(items: viewModel.items,
activeItemId: $viewModel.activeItemId)
// ...
}
For some reason, this works, I don't have an explanation why. However, there's one problem left, that's caused by SwiftUI: programatically changing the selection won't make the list scroll to the new selection. Scroll SwiftUI List to new selection might help fixing this too.
Also, warmly recommending to move the NavigationLink from the body of SidebarRowView to the List part of SidebarList, this will help you limit the amount of details that get leaked to the row view.
Another recommendation I would make, would be to use the tag:selection: alternative to isActive. This works better when you have a pool of possible navigation links from which only one can be active at a certain time. This involves of course changing the view model from var activeItem: Item? to var activeItemId: UUID?, this will avoid the need of the hacky navigationBindingForItem function:
class SidebarListViewModel : ObservableObject {
#Published var items = // ...
#Published var activeItemId : UUID?
}
// ...
NavigationLink(destination: ...,
tag: item.id,
selection: $activeItemId) {
Original Answer
This is most likely what's causing the problematic behaviour:
func navigationBindingForItem(item: Item) -> Binding<Bool> {
.init {
activeItem == item
} set: { newValue in
if newValue {
activeItem = item
}
}
}
If you put a breakpoint on the binding setter, you'll see that the setter gets called every time you select something, and if you also print the item name, you'll see that when the problematic scrolling happens, it always scroll to the previous selected item.
Seems this "manual" binding interferes with the SwiftUI update cycle, causing the framework to malfunction.
The solution here is simple: remove the #Binding declaration from the activeItem property, and keep it as a "regular" one. You also can safely remove the isActive argument passed to the navigation link.
Bindings are needed only when you need to update values in parent components, most of the time simple values are enough. This also makes your views simpler, and more in line with the Swift/SwiftUI principles of using immutable values as much as possible.

Swift UI understanding on observable classes

Ok so my content view is this
import SwiftUI
struct ContentView: View {
#ObservedObject private var user = User()
var body: some View {
VStack {
ShowName()
TextField("First name", text: $user.firstName)
TextField("Last name", text: $user.lastName)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I migrated my class to swift file
import Foundation
class User: ObservableObject {
#Published var firstName = "Bilbo"
#Published var lastName = "Baggins"
}
Then I moved the single view into an external view file
import SwiftUI
struct ShowName: View {
#ObservedObject private var user = User()
var body: some View {
Text("Your name is \(user.firstName) \(user.lastName)")
.padding()
}
}
struct ShowName_Previews: PreviewProvider {
static var previews: some View {
Testing()
}
}
I have Observed the same class but it does not update, when the class and view were in the
Is there something I need to do special in the ShowName() view to make it observe the class, or am I missing the point entirely.
When the main view bound to the class updates the class, the ShowName view is not updated, I know this is me not understanding it but its so hard to ask Google with something like this, so please be kind, im 47 and learning a new language ;)
Sorry ive only been doing swift a few days migrating from Angular/c++/PHP
In ContentView Change
#StateObject private var user = User()
And
ShowName(user: user)
Then in ShowName remove the initializer
#ObservedObject var user = User
Every time you initialize User() you get a different instance one does not know what the other is doing.
The change to state object is because it is unsafe to use #ObservedObject like that.
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
That is because the instance of User is unique in both of those views. To correct it, you would want to pass in the user to the ShowName view instead of create a new one in the view itself. It would look something like this:
struct ShowName: View {
#ObservedObject private var use: User
init(_ user: User) {
self.user = user
}
var body: some View {
Text("Your name is \(user.firstName) \(user.lastName)")
.padding()
}
}
Then in content view you would initialize it like:
ShowName(user)
The important thing to realize is that every time you write User() a new user instance is created. Where this change would pass the same user from content view to the show name view.
I’m writing this from my phone so something may be a little off.

Programatical navigation on NavigationView deeper than two views

Is there a way I can do programatical navigation with more than two views using NavigationView?
Like this: View 1 -> View 2 -> View 3
This is the sample code for what I'm trying to do:
class Coordinator: ObservableObject {
#Published var selectedTag: String?
}
struct ContentView: View {
#EnvironmentObject var coordinator: Coordinator
let things = ["first", "second", "third"]
var body: some View {
NavigationView {
List(things, id: \.self) { thing in
NavigationLink(destination: SecondView(thing: thing),
tag: thing,
selection: self.$coordinator.selectedTag) {
Text(thing)
}
}
}
}
}
struct SecondView: View {
#EnvironmentObject var coordinator: Coordinator
let thing: String
let things = ["fourth", "fifth", "sixth"]
var body: some View {
List(things, id: \.self) { thing2 in
NavigationLink(destination: ThirdView(thing: self.thing, thing2: thing2),
tag: thing2,
selection: self.$coordinator.selectedTag) {
Text(thing2)
}
}
}
}
struct ThirdView: View {
let thing: String
let thing2: String
var body: some View {
Text("\(thing) \(thing2)")
}
}
I hoped that I could select a specific tag and deep navigate to the ThirdView but even the simple navigation won't work. If I select a link on the SecondView it will navigate forward and then back, instead of just navigating forward as expected.
I also tried using 2 variables, one to represent the tag of each screen but it also doesn't work.
Is there a way to make this work? Am I doing something wrong?
Currently, your code is acting like there's another flow of navigation from SecondView to ThirdView, which I assume you're not intending to. If that's the intended case, you should also wrap SecondView's body into a NavigationView, too.
Caveat: You'll have 2 navigation bars.
Without tag and selection (in SecondView or also in ContentView), your code works as intended. Is there a specific reason for using tag and selection here? I couldn't find a proper solution that both uses them and have only one NavigationView.