so I have two functions the first gets an array of structs and the second func gets another struct object and appends it to the first array. In my view I have a scroll view that displays all these elements in the array. When I run the code with the getAd function commented out (does not execute at all) the view displays the correct array of elements in the scroll view. but when I uncomment the getAd function the view does not display any elements at all. But I added a button to print the viewModel.new array, and when I click it, the correct array is printed out WITH the final element appended. Since the app works fine when the second function is commented out, it leads me to believe that the second func is causing the problem, but when I print the array with the button, the result looks just fine how I want it to look.
view
import SwiftUI
struct FeedView: View {
#StateObject var viewModel = FeedViewModel()
var body: some View {
mainInterFaceView
}
}
extension FeedView{
var mainInterFaceView: some View{
VStack(){
ZStack(){
ScrollView {
LazyVStack {
ForEach(viewModel.new) { tweet in
TweetRowView(tweet: tweet, hasDivider: true)
}
}
}
}
}
}
}
}
viewModel:
class FeedViewModel: ObservableObject{
#Published var new = [Tweet]()
var ads = [Tweet]()
let service = TweetService()
let userService = UserService()
init(){
fetchNew()
}
func fetchNew(){
service.fetchNew { tweets in
self.new = tweets
for i in 0 ..< tweets.count {
let uid = tweets[i].uid
self.userService.fetchUser(withUid: uid) { user in
self.new[i].user = user
}
}
self.getAd() //here
}
}
func getAd() {
service.fetchAd { ads in
self.ads = ads
for i in 0 ..< ads.count {
let uid = ads[i].uid
self.userService.fetchUser(withUid: uid) { user in
self.ads[i].user = user
}
}
self.new.append(self.ads[0])
}
}
Related
Using StateObject allows my view to rerender correctly after getting data asynchronously but ObservedObject does not, why?
I have two swiftui views. First is the root App view and the second is the one consuming an ObservedObject view model.
From the root view I create a ViewModel with a firebase object ID for the second view and pass it in. When the second view appears I'd like to then send a request to Firebase to retrieve some data. While the data is being retrieved I show a 'loading' screen.
When I use ObservedObject my second view never rerenders after the data is retrieved. Using StateObject though fixes the issue but I'm not sure why.
I think I understand the differences between ObservedObject and StateObject having read the docs. But not sure how the differences apply to my case since I don't think the root view should be rerendering and recreating the ViewModel I created there and passed to the second view.
There is a Login view which forces the RootView to rerender after login but from what I know, the SecondView still shouldn't be rendered more than once.
https://www.avanderlee.com/swiftui/stateobject-observedobject-differences/
RootView
#main
struct MyApp: App {
#ObservedObject private var userService = UserService()
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
NavigationView {
Group {
if (userService.currentUser != nil) {
SecondView(secondViewModel: SecondViewModel(somethingId: "someIdGoesHere"))
} else {
LoginView()
}
}
.environmentObject(userService)
}
}
}
}
View Model
class SecondViewModel: ObservableObject {
#Published private(set) var thing: Thing? = nil
private let thingService = ThingService()
private let thingId: String
init(thingId: String) {
self.thingId = thingId
}
func load() async {
self.thing = await thingService.GetThing(thingId: thingId) // Async call to firebase
}
}
Second View
struct SecondView: View {
// Task runs to completion but 'Loading...' never goes away.
#ObservedObject var secondViewModel: SecondViewModel
var body: some View {
VStack(spacing: 5) {
if let thingText = secondViewModel.thing {
Text(thingText)
} else {
Text("Loading...")
}
}
.onAppear {
Task {
await secondViewModel.load()
}
}
}
}
I am relatively new to SwiftUI and I'm trying to work on my first app. I am trying to use a segmented picker to give the user an option of changing between a DayView and a Week View. In each on of those Views, there would be specific user data that whould be shown as a graph. The issue I am having is loading the data. I posted the code below, but from what I can see, the issue comes down to the following:
When the view loads in, it starts with loading the dayView, since the selectedTimeInterval = 0. Which is fine, but then when the users presses on the "Week" in the segmented Picker, the data does not display. This due to the rest of the View loading prior to the .onChange() function from the segmented picker running. Since the .onChange is what puts the call into the viewModel to load the new data, there is no data. You can see this in the print statements if you run the code below.
I would have thought that the view load order would have been
load segmented picker
run the .onChange if the value changed
load the rest of the view
but the order actual is
load segmented picker,
load the rest of the view (graph loads with no data here!!!!!)
run the .onChange if the value has changed.
I am pretty lost so any help would be great! Thank you so much!
import SwiftUI
import OrderedCollections
class ViewModel: ObservableObject{
#Published var testDictionary: OrderedDictionary<String, Int> = ["":0]
public func daySelected() {
testDictionary = ["Day View Data": 100]
}
public func weekSelected() {
testDictionary = ["Week View Data": 200]
}
}
struct ContentView: View {
#State private var selectedTimeInterval = 0
#StateObject private var vm = ViewModel()
var body: some View {
VStack {
Picker("Selected Date", selection: $selectedTimeInterval) {
Text("Day").tag(0)
Text("Week").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.onChange(of: selectedTimeInterval) { _ in
let _ = print("In on change")
//Logic to handle different presses of the selector
switch selectedTimeInterval {
case 0:
vm.daySelected()
case 1:
vm.weekSelected()
default:
print("Unknown Selected Case")
}
}
switch selectedTimeInterval {
case 0:
let _ = print("In view change")
Day_View()
case 1:
let _ = print("In view change")
Week_View(inputDictionary: vm.testDictionary)
default:
Text("Whoops")
}
}
}
}
struct Day_View: View {
var body: some View {
Text("Day View!")
}
}
struct Week_View: View {
#State private var inputDictionary: OrderedDictionary<String,Int>
init(inputDictionary: OrderedDictionary<String,Int>) {
self.inputDictionary = inputDictionary
}
var body: some View {
let keys = Array(inputDictionary.keys)
let values = Array(inputDictionary.values)
VStack {
Text(keys[0])
Text(String(values[0]))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
In your WeekView, change
#State private var inputDictionary: OrderedDictionary<String,Int>
to
private let inputDictionary: OrderedDictionary<String,Int>
#State is for the local state of the view. The idea is that you are initing it with initial state and from then on the view itself will change it and cause re-renders. When the WeekView is re-rendered, SwiftUI is ignoring the parameter you pass into the init and copying it from the previous WeekView to maintain state.
But, you want to keep passing in the dictionary from ContentView and cause re-renders from the parent view.
The main issue is that the initialization of the #State property wrapper is wrong.
You must use this syntax
#State private var inputDictionary: OrderedDictionary<String,Int>
init(inputDictionary: OrderedDictionary<String,Int>) {
_inputDictionary = State(wrappedValue: inputDictionary)
}
Or – if inputDictionary is not going to be modified – just declare it as (non-private) constant and remove the init method
let inputDictionary: OrderedDictionary<String,Int>
I was playing with Swift Playground with the following code.
What I was doing is to modify a #State variable in a Button action, and then show its current value in a full-screen sheet.
The problem is that, notice the code line I commented, without this line, the value displayed in the full-screen sheet will be still 1, with this line, the value will be 2 instead, which is what I expected it to be.
I want to know why should this happen. Why the Text matters.
import SwiftUI
struct ContentView: View {
#State private var n = 1
#State private var show = false
var body: some View {
VStack {
// if I comment out this line, the value displayed in
// full-screen cover view will be still 1.
Text("n = \(n)")
Button("Set n = 2") {
n = 2
show = true
}
}
.fullScreenCover(isPresented: $show) {
VStack {
Text("n = \(n)")
Button("Close") {
show = false
// UPDATE(1)
print("n in fullScreenCover is", n)
}
}
}
}
}
Playground Version: Version 4.1 (1676.15)
Update 1:
As Asperi answered, if n in fullScreenCover isn't captured because it's in different contexts (closure?), then why does the print line prints n in fullScreenCover is 2 when not put Text in body?
The fullScreenCover (and similarly sheet) context is different context, created once and capturing current states. It is not visible for view's dynamic properties.
With put Text dependable on state into body just makes all body re-evaluated once state changed, thus re-creating fullScreenCover with current snapshot of context.
A possible solutions:
to capture dependency explicitly, like
.fullScreenCover(isPresented: $show) { [n] in // << here !!
VStack {
Text("n = \(n)")
to make separated view and pass everything there as binding, because binding is actually a reference, so being captured it still remains bound to the same source of truth:
.fullScreenCover(isPresented: $show) {
FullScreenView(n: $n, show: $show)
}
and view
struct FullScreenView: View {
#Binding var n: Int
#Binding var show: Bool
var body: some View {
VStack {
Text("n = \(n)")
Button("Close") {
show = false
}
}
}
}
both give:
Tested with Xcode 13.4 / iOS 15.5
For passing data into fullScreenCover we use a different version which is fullScreenCover(item:onDismiss:content:). There is an example at that link but in your case it would be:
struct FullscreenCoverTest: View {
#State private var n = 1
#State private var coverData: CoverData?
var body: some View {
VStack {
Button("Set n = 2") {
n = 2
coverData = CoverData(n: n)
}
}
.fullScreenCover(item: $coverData) { item in
VStack {
Text("n = \(item.n)")
Button("Close") {
coverData = nil
}
}
}
}
}
struct CoverData: Identifiable {
let id = UUID()
var n: Int
}
Note when it's done this way, if the sheet is open when coverData is changed to one with a different ID, the sheet will animate away and appear again showing the new data.
I have a question related to the next behavior.
I have ContentView with a list of views where the corresponding view models are passed. The user can click by some view. At the moment full-screen modal dialog will be shown according to the passed type. It's fine.
At some time my view models are being updated and the whole ContentView will be reloaded. The problem is: fullScreenCover is called and ChildEventView is recreated. How to prevent recreating ChildEventView?
struct ContentView: View {
#State private var fullScreenType: FullScreenType?
// some stuff
var body: some View {
ScrollView {
LazyVStack {
ForEach(eventListViewModel.cardStates.indices, id: \.self) { index in
let eventVM = eventListViewModel.eventVMs[index]
EventCardView(eventViewModel: eventVM, eventId: $selectedEvent.eventId) {
self.fullScreenType = .type1
}
// some other views
}
}
}
.fullScreenCover(item: $fullScreenType, onDismiss: {
self.fullScreenType = nil
}, content: { fullScreenType in
switch fullScreenType {
case .type1:
return ChildEventView(selectedEvent.eventId).eraseToAnyView()
// some other cases
}
})
}
}
I'm downloading data from FireStore. Data is retrieved perfectly. I have the data and can print information. The issue is, when I tap on a text/label to push to the intended view, I perform the function using the .onAppear function. My variables, from my ObservableClass are #Published. I have the data and can even set elements based on the data retrieved. I'm using the MVVM approach and have done this a plethora of times throughout my project. However, this is the first time I have this particular issue. I've even used functions that are working in other views completely fine, yet in this particular view this problem persists. When I load/push this view, the data is shown for a split second and then the view/canvas is blank. Unless the elements are static i.e. Text("Hello World") the elements will disappear. I can't understand why the data just decides to disappear.
This is my code:
struct ProfileFollowingView: View {
#ObservedObject var profileViewModel = ProfileViewModel()
var user: UserModel
func loadFollowing() {
self.profileViewModel.loadCurrentUserFollowing(userID: self.user.uid)
}
var body: some View {
ZStack {
Color(SYSTEM_BACKGROUND_COLOUR)
.edgesIgnoringSafeArea(.all)
VStack(alignment: .leading) {
if !self.profileViewModel.isLoadingFollowing {
ForEach(self.profileViewModel.following, id: \.uid) { user in
VStack {
Text(user.username).foregroundColor(.red)
}
}
}
}
} .onAppear(perform: {
self.profileViewModel.loadCurrentUserFollowing(userID: self.user.uid)
})
}
}
This is my loadFollowers function:
func loadCurrentUserFollowing(userID: String) {
isLoadingFollowing = true
API.User.loadUserFollowing(userID: userID) { (user) in
self.following = user
self.isLoadingFollowing = false
}
}
I've looked at my code that retrieves the data, and it's exactly like other features/functions I already have. It's just happens on this view.
Change #ObservedObject to #StateObject
Update:
ObservedObject easily gets destroyed/recreated whenever a view is re-created, while StateObject stays/exists even when a view is re-created.
For more info watch this video:
https://www.youtube.com/watch?v=VLUhZbz4arg
It looks like API.User.loadUserFollowing(userID:) may be asynchronous - may run in the background. You need to update all #Published variables on the main thread:
func loadCurrentUserFollowing(userID: String) {
isLoadingFollowing = true
API.User.loadUserFollowing(userID: userID) { (user) in
DispatchQueue.main.async {
self.following = user
self.isLoadingFollowing = false
}
}
}
You might need to add like this: "DispatchQueue.main.asyncAfter"
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.profileViewModel.loadCurrentUserFollowing(userID: self.user.uid)
}
}