My ForEach in a Scrollview does not get updated when CommentViewModel comments get updated. It gets successfully updated, but for some reason, CommentView does not get updated. I have tried everything, but can't seem to find a solution.
Maybe Comment should become a Hashable or Codable. But I can't quite make this work.
I also tried removing the chance of Scrollview being empty, by adding an if statement or empty Text. But this was not the problem.
Any help would be helpfull.
//These are the updated View
struct CommentView: View {
#StateObject var commentViewModel = CommentViewModel()
static let emptyScrollToString = "emptyScrollToString"
#State var commentCommentUser = ""
#State var showCommentComment = false
#State var post: Post
init(_ post: Post) {
self.post = post
}
var body: some View {
VStack {
commentView
Divider()
if showCommentComment {
HStack {
Text("Svarer \(commentCommentUser)")
.foregroundColor(.black)
.font(.system(size: 16))
.opacity(0.3)
Spacer()
Button {
withAnimation(Animation.spring().speed(2)) {
showCommentComment.toggle()
}
} label: {
Text("x")
.font(.system(size: 16))
.foregroundColor(.black)
}
}
.padding()
.background(Color(r: 237, g: 237, b: 237))
}
BottomBar(post: post)
.frame(minHeight: 50,maxHeight: 180)
.fixedSize(horizontal: false, vertical: true)
.shadow(radius: 60)
.navigationBarTitle("Kommentar", displayMode: .inline)
}
.onAppear() {
UINavigationBar.appearance().tintColor = UIColor(red: 20/255, green: 147/255, blue: 2/255, alpha: 1)
commentViewModel.fetchComments(post: post)
}
}
private var commentView: some View {
ScrollView {
ScrollViewReader { scrollViewProxy in
VStack {
HStack{ Spacer() }
.id(Self.emptyScrollToString)
ForEach(commentViewModel.comments, id: \.id) { comment in
CommentCell(post: post, comment: comment, commentCommentUser: $commentCommentUser, showCommentComment: $showCommentComment)
}
}
.onReceive(Just(commentViewModel.comments.count)) { _ in // <-- here
withAnimation(.easeOut(duration: 0.5)) {
print("Scroll to top")
scrollViewProxy.scrollTo(Self.emptyScrollToString, anchor: .bottom)
}
}
}
}
}
public func uploadData(commentText: String) {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {return}
guard let id = post.id else {return}
let data = ["fromId":uid, "commentText":commentText, "likes":0, "timestamp": Timestamp()] as [String : Any]
FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
.document().setData(data) { error in
if error != nil {
print("failed to post comment", error ?? "")
return
}
print("Update")
commentViewModel.fetchComments(post: post) //Gets error here
}
}
}
struct BottomBar: View {
var commentView: CommentView
init(post: Post) {
self.commentView = CommentView(post)
}
var body: some View {
bottomBar
}
private var bottomBar: some View {
HStack{
TextEditorView(string: $commentText)
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(lineWidth: 1)
.opacity(0.5))
VStack {
Spacer()
Button {
commentView.uploadData() // This also reset all #State variables in Commentview, for some reason
commentText = ""
} label: {
Text("Slå op")
.font(.system(size: 20, weight: .semibold))
.opacity(commentText.isEmpty ? 0.5 : 1)
.foregroundColor(Color(r: 20, g: 147, b: 2))
}
.padding(.bottom, 10)
}
}
.padding()
}
}
struct Comment: Identifiable, Decodable {
#DocumentID var id: String?
let commentText: String
let fromId: String
var likes: Int
let timestamp: Timestamp
var user: PostUser?
var didLike: Bool? = false
}
class CommentViewModel: ObservableObject {
#Published var comments = [Comment]()
#Published var count = 0
let service: CommentService
let userService = UserService()
init(post: Post) {
self.service = CommentService(post: post)
fetchComments()
}
func fetchComments() {
service.fetchComments { comments in
self.comments = comments
self.count = self.comments.count
for i in 0 ..< comments.count {
let uid = comments[i].fromId
self.userService.fetchUser(withUid: uid) { user in
self.comments[i].user = user
}
}
}
}
}
struct CommentService {
let post: Post
func fetchComments(completion: #escaping([Comment]) -> Void) {
guard let id = post.id else {return}
FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
.order(by: "timestamp", descending: true)
.getDocuments { snapshot, error in
if error != nil {
print("failed fetching comments", error ?? "")
return
}
guard let docs = snapshot?.documents else {return}
do {
let comments = try docs.compactMap({ try $0.data(as: Comment.self) })
print("COmplete")
completion(comments)
}
catch {
print("failed")
}
}
}
}
This is the old views
struct Comment: Identifiable, Decodable {
#DocumentID var id: String?
let commentText: String
let fromId: String
var likes: Int
let timestamp: Timestamp
var user: PostUser?
var didLike: Bool? = false
}
class CommentViewModel: ObservableObject {
#Published var comments = [Comment]()
#Published var count = 0
let service: CommentService
let userService = UserService()
init(post: Post) {
self.service = CommentService(post: post)
fetchComments()
}
func fetchComments() {
service.fetchComments { comments in
self.comments = comments
self.count = self.comments.count
for i in 0 ..< comments.count {
let uid = comments[i].fromId
self.userService.fetchUser(withUid: uid) { user in
self.comments[i].user = user
}
}
}
}
}
struct CommentService {
let post: Post
func fetchComments(completion: #escaping([Comment]) -> Void) {
guard let id = post.id else {return}
FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
.order(by: "timestamp", descending: true)
.getDocuments { snapshot, error in
if error != nil {
print("failed fetching comments", error ?? "")
return
}
guard let docs = snapshot?.documents else {return}
do {
let comments = try docs.compactMap({ try $0.data(as: Comment.self) })
print("COmplete")
completion(comments)
}
catch {
print("failed")
}
}
}
}
struct CommentView: View {
#ObservedObject var commentViewModel: CommentViewModel
static let emptyScrollToString = "emptyScrollToString"
init(post: Post) {
commentViewModel = CommentViewModel(post: post)
}
var body: some View {
VStack {
commentView
Divider()
BottomBar(post: commentViewModel.service.post)
.frame(minHeight: 50,maxHeight: 180)
.fixedSize(horizontal: false, vertical: true)
.shadow(radius: 60)
.navigationBarTitle("Kommentar", displayMode: .inline)
}
.onAppear() {
UINavigationBar.appearance().tintColor = UIColor(red: 20/255, green: 147/255, blue: 2/255, alpha: 1)
}
}
private var commentView: some View {
ScrollView {
ScrollViewReader { scrollViewProxy in
VStack {
HStack{ Spacer() }
.id(Self.emptyScrollToString)
ForEach(commentViewModel.comments, id: \.id) { comment in // Here should it update
let _ = print("Reload")
CommentCell(post: commentViewModel.service.post, comment: comment)
}
}
.onReceive(commentViewModel.$count) { _ in // It doesn't update here either
withAnimation(.easeOut(duration: 0.5)) {
print("Scroll to top")
scrollViewProxy.scrollTo(Self.emptyScrollToString, anchor: .bottom)
}
}
}
}
}
}
#StateObject property wrapper own the object you created, so it will keep alive once View updated by any changes.
#ObservedObject property wrapper doesn't own the object you created, so it will recreated on View update by any changes, in this way property observer get lost and will not be able to receive changes.
So, changing your ViewModel from #ObservedObject to #StateObject will fix the issue.
EDIT-1: Taking your new code into consideration.
Note, it is important to have a good grip on the SwiftUI basics, especially how to use and pass ObservableObject and how to use Views. I suggest you do the tutorial again.
I have attempted to modify your code to give you an idea on how you could re-structure it. Pay atttention to the details, hope it helps.
Note, I have commented a number of lines, because I do not have Firebase and your other code, such as UserService etc...
Adjust my code to suit your needs, and uncomment the relevant lines.
import Foundation
import SwiftUI
import Combine
struct BottomBar: View {
#ObservedObject var viewModel: CommentViewModel // <-- here
#State var post: Post
#State var commentText = ""
var body: some View {
HStack {
// TextEditorView(string: $commentText)
TextEditor(text: $commentText) // <-- for testing
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(lineWidth: 1)
.opacity(0.5))
VStack {
Spacer()
Button {
// -- here
viewModel.uploadData(post: post, commentText: commentText)
commentText = ""
} label: {
Text("Slå op")
.font(.system(size: 20, weight: .semibold))
.opacity(commentText.isEmpty ? 0.5 : 1)
}
.padding(.bottom, 10)
}
}
}
}
struct CommentView: View {
#StateObject var commentViewModel = CommentViewModel()
static let emptyScrollToString = "emptyScrollToString"
#State var commentCommentUser = ""
#State var showCommentComment = false
#State var post: Post
var body: some View {
VStack {
commentView
Divider()
if showCommentComment {
HStack {
Text("Svarer \(commentCommentUser)")
.foregroundColor(.black)
.font(.system(size: 16))
.opacity(0.3)
Spacer()
Button {
withAnimation(Animation.spring().speed(2)) {
showCommentComment.toggle()
}
} label: {
Text("x")
.font(.system(size: 16))
.foregroundColor(.black)
}
}.padding()
}
BottomBar(viewModel: commentViewModel, post: post)
.frame(minHeight: 50,maxHeight: 180)
.fixedSize(horizontal: false, vertical: true)
.shadow(radius: 60)
.navigationBarTitle("Kommentar", displayMode: .inline)
}
.onAppear() {
UINavigationBar.appearance().tintColor = UIColor(red: 20/255, green: 147/255, blue: 2/255, alpha: 1)
commentViewModel.fetchComments(post: post)
}
}
private var commentView: some View {
ScrollView {
ScrollViewReader { scrollViewProxy in
VStack {
Spacer()
ForEach(commentViewModel.comments, id: \.id) { comment in
CommentCell(post: post, comment: comment, commentCommentUser: $commentCommentUser, showCommentComment: $showCommentComment)
}
}
.onReceive(Just(commentViewModel.comments.count)) { _ in // <-- here
withAnimation(.easeOut(duration: 0.5)) {
print("Scroll to top")
scrollViewProxy.scrollTo(Self.emptyScrollToString, anchor: .bottom)
}
}
}
}
}
}
// for testing
struct CommentCell: View {
#State var post: Post
#State var comment: Comment
#Binding var commentCommentUser: String
#Binding var showCommentComment: Bool
var body: some View {
Text(comment.commentText) // for testing
}
}
struct Comment: Identifiable, Decodable {
// #DocumentID
var id: String? // for testing
let commentText: String
let fromId: String
var likes: Int
// let timestamp: Timestamp // for testing
var user: PostUser?
var didLike: Bool? = false
}
class CommentViewModel: ObservableObject {
#Published var comments = [Comment]()
let service = CommentService() // <-- here
// let userService = UserService() // for testing
init() { } // <-- here
func fetchComments(post: Post) { // <-- here
service.fetchComments(post: post) { comments in
self.comments = comments
for i in 0 ..< comments.count {
let uid = comments[i].fromId
// self.userService.fetchUser(withUid: uid) { user in
// self.comments[i].user = user
// }
}
}
}
func uploadData(post: Post, commentText: String) {
service.uploadData(post: post, commentText: commentText) { isGood in
if isGood {
self.fetchComments(post: post)
}
}
}
}
struct CommentService {
func fetchComments(post: Post, completion: #escaping([Comment]) -> Void) {
guard let id = post.id else {completion([]); return} // <-- here
// FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
// .order(by: "timestamp", descending: true)
// .getDocuments { snapshot, error in
// if error != nil {
// print("failed fetching comments", error ?? "")
// return
// }
// guard let docs = snapshot?.documents else {return}
// do {
// let comments = try docs.compactMap({ try $0.data(as: Comment.self) })
// print("COmplete")
// completion(comments)
// }
// catch {
// print("failed")
// completion([])
// }
// }
}
func uploadData(post: Post, commentText: String, completion: #escaping(Bool) -> Void) {
completion(true) // for testing, to be removed
// guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {completion(false); return} // <--- here
// guard let id = post.id else {completion(false); return} // <--- here
//
// let data = ["fromId":uid, "commentText":commentText, "likes":0, "timestamp": Timestamp()] as [String : Any]
// FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
// .document().setData(data) { error in
// if error != nil {
// print("failed to post comment", error ?? "")
// completion(false) // <--- here
// return
// }
// print("Update")
// completion(true) // <--- here
// }
}
}
// for testing
struct PostUser: Identifiable, Decodable {
var id: String?
}
// for testing
struct Post: Identifiable, Decodable {
var id: String?
var name = "something"
}
EDIT-2: typo fix.
in BottomBar changed viewModel.service.uploadData(post: post, commentText: commentText) {_ in}
to viewModel.uploadData(post: post, commentText: commentText)
Related
well so the problem: when i open the EditViewScreen i need to get the title(-text) and body(-text) of that post. But I always get the title and body of the first cell.
then I didn’t understand the meaning of this post either: 2022-06-03 06:52:57.858609+0500 SwiftuiMVVM[4334:105229] [Presentation] Attempt to present <TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView: 0x127e734d0> on <TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier_: 0x127e0ac70> (from <TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentGS1_VVS_22_VariadicView_Children7ElementVS_24NavigationColumnModifier_GVS_18StyleContextWriterVS_19SidebarStyleContext__: 0x127e12bc0>) which is already presenting <TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView: 0x12a604820>.
HomeViewScreen()
ZStack {
List {
ForEach(viewModel.posts, id: \.self) { post in
PostCell(post: post).onLongPressGesture {
showingEdit.toggle()
}.sheet(isPresented: $showingEdit) {
EditViewScreen(post: post)
}
}
.onDelete(perform: delete)
}.listStyle(.plain)
if viewModel.isLoading {
ProgressView()
}
}
EditViewScreen()
import SwiftUI
struct EditViewScreen: View {
#ObservedObject var viewModel = EditViewModel()
#Environment(\.presentationMode) var presentation
var post: Post
#State var p_title = ""
#State var p_body = ""
func updatePost() {
let post = Post(id: post.id!, title: p_title, body: p_body)
viewModel.apiPostUpdate(post: post, handler: { isUpdated in
if isUpdated {
presentation.wrappedValue.dismiss()
}
})
}
var body: some View {
NavigationView {
ZStack {
VStack(spacing: 10) {
TextField("Edit title", text: $p_title)
.frame(height: 60)
.padding(.leading)
.background(.gray.opacity(0.1))
.cornerRadius(10)
.font(.system(size: 18))
TextField("Edit body", text: $p_body)
.frame(height: 60)
.padding(.leading)
.background(.gray.opacity(0.1))
.cornerRadius(10)
.font(.system(size: 18))
Button(action: {
updatePost()
}, label: {
Spacer()
Text("Update")
Spacer()
})
.padding()
.frame(height: 60)
.background(.black.opacity(0.7))
.foregroundColor(.white)
.cornerRadius(10)
.font(.system(size: 18))
Spacer()
}
.padding(.leading)
.padding(.trailing)
.padding(.top, -35)
if viewModel.isLoading {
ProgressView()
}
}
.navigationBarItems(leading: Button(action: {
presentation.wrappedValue.dismiss()
}, label: {
Text("Cancel")
.foregroundColor(.black)
.font(.system(size: 18, weight: .light))
}))
}.onAppear {
p_title = post.title!
p_body = post.body!
}
}
}
Well, some shortcomings have passed. I've made changes to the HomeViewModel().
class HomeViewModel: ObservableObject {
#Published var isLoading = false
#Published var posts = [Post]()
#Published var post = Post() // <-- here
func apiPostList() {
isLoading = true
AFHttp.get(url: AFHttp.API_POST_LIST, params: AFHttp.paramsEmpty(), handler: { response in
self.isLoading = false
switch response.result {
case .success:
let posts = try! JSONDecoder().decode([Post].self, from: response.data!)
self.posts = posts
case let .failure(error):
print(error)
}
})
}
}
HomeViewScreen()
struct HomeViewScreen: View {
#ObservedObject var viewModel = HomeViewModel()
#State private var showingEdit = false
var body: some View {
NavigationView {
ZStack {
List {
ForEach(viewModel.posts, id: \.self) { post in
PostCell(post: post)
.onLongPressGesture {
showingEdit.toggle()
viewModel.post = post // <-- here
}.sheet(isPresented: $showingEdit) {
EditViewScreen(post: viewModel.post)
// 'll be viewmodel.post, not just the post itself
}
}
.onDelete(perform: delete)
}
.listStyle(.plain)
if viewModel.isLoading {
ProgressView()
}
}
Thank you for your attention(awa #workingdog)
try this approach using #StateObject var viewModel in your HomeViewScreen,
and passing that to EditViewScreen. In addition use .sheet(item: ...)
outside the ForEach loop, as in the example code. Importantly never
use forced unwrapping, that is, do not use ! in your code at all, use if let ... or guard ....
struct HomeViewScreen: View {
#StateObject var viewModel = EditViewModel() // <-- here
#State var selectedPost: Post? // <-- here
var body: some View {
ZStack {
List {
ForEach(viewModel.posts, id: \.self) { post in
PostCell(post: post).onLongPressGesture {
selectedPost = post // <-- here
}
}
.onDelete(perform: delete)
}.listStyle(.plain)
// -- here
.sheet(item: $selectedPost) { post in
EditViewScreen(post: post, viewModel: viewModel) // <-- here
}
if viewModel.isLoading {
ProgressView()
}
}
}
}
struct EditViewScreen: View {
#ObservedObject var viewModel: EditViewModel // <-- here
#Environment(\.presentationMode) var presentation
var post: Post
#State var p_title = ""
#State var p_body = ""
//....
Struggling to get a simple example up and running in swiftui:
Load default list view (working)
click button that launches picker/filtering options (working)
select options, then click button to dismiss and call function with selected options (call is working)
display new list of objects returned from call (not working)
I'm stuck on #4 where the returned query isn't making it to the view. I suspect I'm creating a different instance when making the call in step #3 but it's not making sense to me where/how/why that matters.
I tried to simplify the code some, but it's still a bit, sorry for that.
Appreciate any help!
Main View with HStack and button to filter with:
import SwiftUI
import FirebaseFirestore
struct TestView: View {
#ObservedObject var query = Query()
#State var showMonPicker = false
#State var monFilter = "filter"
var body: some View {
VStack {
HStack(alignment: .center) {
Text("Monday")
Spacer()
Button(action: {
self.showMonPicker.toggle()
}, label: {
Text("\(monFilter)")
})
}
.padding()
ScrollView(.horizontal) {
LazyHStack(spacing: 35) {
ForEach(query.queriedList) { menuItems in
MenuItemView(menuItem: menuItems)
}
}
}
}
.sheet(isPresented: $showMonPicker, onDismiss: {
//optional function when picker dismissed
}, content: {
CuisineTypePicker(selectedCuisineType: $monFilter)
})
}
}
The Query() file that calls a base query with all results, and optional function to return specific results:
import Foundation
import FirebaseFirestore
class Query: ObservableObject {
#Published var queriedList: [MenuItem] = []
init() {
baseQuery()
}
func baseQuery() {
let queryRef = Firestore.firestore().collection("menuItems").limit(to: 50)
queryRef
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
self.queriedList = querySnapshot?.documents.compactMap { document in
try? document.data(as: MenuItem.self)
} ?? []
}
}
}
func filteredQuery(category: String?, glutenFree: Bool?) {
var filtered = Firestore.firestore().collection("menuItems").limit(to: 50)
// Sorting and Filtering Data
if let category = category, !category.isEmpty {
filtered = filtered.whereField("cuisineType", isEqualTo: category)
}
if let glutenFree = glutenFree, !glutenFree {
filtered = filtered.whereField("glutenFree", isEqualTo: true)
}
filtered
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
self.queriedList = querySnapshot?.documents.compactMap { document in
try? document.data(as: MenuItem.self);
} ?? []
print(self.queriedList.count)
}
}
}
}
Picker view where I'm calling the filtered query:
import SwiftUI
struct CuisineTypePicker: View {
#State private var cuisineTypes = ["filter", "American", "Chinese", "French"]
#Environment(\.presentationMode) var presentationMode
#Binding var selectedCuisineType: String
#State var gfSelected = false
let query = Query()
var body: some View {
VStack(alignment: .center) {
//Buttons and formatting code removed to simplify..
}
.padding(.top)
Picker("", selection: $selectedCuisineType) {
ForEach(cuisineTypes, id: \.self) {
Text($0)
}
}
Spacer()
Button(action: {
self.query.filteredQuery(category: selectedCuisineType, glutenFree: gfSelected)
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text( "apply filters")
})
}
.padding()
}
}
I suspect that the issue stems from the fact that you aren't sharing the same instance of Query between your TestView and your CuisineTypePicker. So, when you start a new Firebase query on the instance contained in CuisineTypePicker, the results are never reflected in the main view.
Here's an example of how to solve that (with the Firebase code replaced with some non-asynchronous sample code for now):
struct MenuItem : Identifiable {
var id = UUID()
var cuisineType : String
var title : String
var glutenFree : Bool
}
struct ContentView: View {
#ObservedObject var query = Query()
#State var showMonPicker = false
#State var monFilter = "filter"
var body: some View {
VStack {
HStack(alignment: .center) {
Text("Monday")
Spacer()
Button(action: {
self.showMonPicker.toggle()
}, label: {
Text("\(monFilter)")
})
}
.padding()
ScrollView(.horizontal) {
LazyHStack(spacing: 35) {
ForEach(query.queriedList) { menuItem in
Text("\(menuItem.title) - \(menuItem.cuisineType)")
}
}
}
}
.sheet(isPresented: $showMonPicker, onDismiss: {
//optional function when picker dismissed
}, content: {
CuisineTypePicker(query: query, selectedCuisineType: $monFilter)
})
}
}
class Query: ObservableObject {
#Published var queriedList: [MenuItem] = []
private let allItems: [MenuItem] = [.init(cuisineType: "American", title: "Hamburger", glutenFree: false),.init(cuisineType: "Chinese", title: "Fried Rice", glutenFree: true)]
init() {
baseQuery()
}
func baseQuery() {
self.queriedList = allItems
}
func filteredQuery(category: String?, glutenFree: Bool?) {
queriedList = allItems.filter({ item in
if let category = category {
return item.cuisineType == category
} else {
return true
}
}).filter({item in
if let glutenFree = glutenFree {
return item.glutenFree == glutenFree
} else {
return true
}
})
}
}
struct CuisineTypePicker: View {
#ObservedObject var query : Query
#Binding var selectedCuisineType: String
#State private var gfSelected = false
private let cuisineTypes = ["filter", "American", "Chinese", "French"]
#Environment(\.presentationMode) private var presentationMode
var body: some View {
VStack(alignment: .center) {
//Buttons and formatting code removed to simplify..
}
.padding(.top)
Picker("", selection: $selectedCuisineType) {
ForEach(cuisineTypes, id: \.self) {
Text($0)
}
}
Spacer()
Button(action: {
self.query.filteredQuery(category: selectedCuisineType, glutenFree: gfSelected)
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text( "apply filters")
})
}
}
I have successfully displayed the data to the UI, but I want the user to be able to update my data again when tapping the "Save" button . Hope you can help me!
Profile
I have successfully displayed the data to the UI, but I want the user to be able to update my data again when tapping the "Save" button . Hope you can help me!
There are many ways to achieve what you want. This is just one approach, by
passing the profileViewModel to EditProfile:
class ProfileViewModel: ObservableObject {
#Published var user = Profile(id: "", image: "", birthDay: "", role: [], gender: "", name: "")
private var ref: DatabaseReference = Database.database().reference()
func fetchData(userId: String? = nil) {
// 8hOqqnFlfGZTj1u5tCkTdxAED2I3
ref.child("users").child(userId ?? "default").observe(.value) { [weak self] (snapshot) in
guard let self = self,
let value = snapshot.value else { return }
do {
print("user: \(value)")
self.user = try FirebaseDecoder().decode(Profile.self, from: value)
} catch let error {
print(error)
}
}
}
func saveUser() {
// save the user using your ref DatabaseReference
// using setValue, or updateChildValues
// see https://firebase.google.com/docs/database/ios/read-and-write
}
}
struct EditProfile: View {
#ObservedObject var profileViewModel: ProfileViewModel // <--- here
var body: some View {
VStack {
Text(profileViewModel.user.name) // <--- you probably meant TextField
.font(.custom("Poppins-Regular", size: 15))
.foregroundColor(Color.black)
Text("\(profileViewModel.user.birthDay)!")
.font(.custom("Poppins-Regular", size: 22))
.fontWeight(.bold)
.foregroundColor(Color.black)
Text("\(profileViewModel.user.gender)")
.font(.custom("Poppins-Regular", size: 22))
.fontWeight(.bold)
.foregroundColor(Color.black)
Text(profileViewModel.user.role.first ?? "")
.font(.custom("Poppins-Regular", size: 22))
.fontWeight(.bold)
.foregroundColor(Color.black)
Button(action: {
// save the profileViewModel.user to database
profileViewModel.saveUser() // <--- here
}) {
Text("Save")
}
}
.padding()
}
}
struct CategoriesView: View {
#ObservedObject var viewModel = SectionViewModel()
#EnvironmentObject var loginViewModel : LoginViewModel
#StateObject var profileViewModel = ProfileViewModel()
var body: some View {
ZStack {
VStack (alignment: .leading, spacing:0) {
EditProfile(profileViewModel: profileViewModel) // <--- here
.padding()
.padding(.bottom,-10)
}
}
.onAppear() {
self.viewModel.fetchData()
profileViewModel.fetchData(userId: loginViewModel.session?.uid)
}
}
}
EDIT1: regarding the updated code.
In your new code, in ProfileHost you are not passing ProfileViewModel.
Use:
NavigationLink(destination: ProfileEditor(profileViewModel: viewModel)) {
ProfileRow(profileSetting: profile)
}
And in ProfileEditor replace profile with profileViewModel.user
You will probably need to adjust profileItem and put it in a .onAppear {...} . Something like this:
struct ProfileEditor: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#ObservedObject var profileViewModel: ProfileViewModel
#EnvironmentObject var loginViewModel: LoginViewModel
let profileLabel: [String] = ["Name", "Account", "Gender", "Role", "Email"]
#State var profileItem: [String] = []
#State var profileEditorRow: [ProfileEditorItem] = []
var body: some View {
VStack {
ForEach(profileEditorRow) { editor in
if editor.id == 5 {
ProfileEditorRow(editor: editor, showLastLine: true)
} else {
ProfileEditorRow(editor: editor, showLastLine: false)
}
}
Button("Save") {
profileViewModel.updateData(userId: loginViewModel.session?.uid)
}
}
.onAppear {
profileItem = [profileViewModel.user.name,
profileViewModel.user.birthDay,
profileViewModel.user.gender,
profileViewModel.user.role.first ?? "",
profileViewModel.user.birthDay]
for n in 1...5 {
profileEditorRow.append(ProfileEditorItem(id: n, label: profileLabel[n-1], item: profileItem[n-1]))
}
}
}
}
EDIT2: update func
func updateData() {
ref.("users").child(user.id).updateChildValues([
"name": user.name,
"birthDay": user.birthDay,
"gender": user.gender,
"role": user.role.first ?? ""])
}
and use this in ProfileEditor :
Button("Save") {
profileViewModel.updateData()
}
Using Swift5.3.2, iOS14.4.1, Xcode12.4,
I am trying to make two simultaneous gestures work in SwiftUI.
The child-View at question is a VideoPlayer.
The parent-View is a ZStack.
I am using the .simultaneousGesture modifier, hoping that this will allow all child-View gestures to still go through.
But they don't !
The following code works but does not allow the VideoPlayer's control gestures to be recognised.
In fact, the restartScannerTapGesture TapGesture in my example does always kick in - and the VideoPlayer control gestures (like pause, play, stop etc.) are unfortunately ignored.
Any idea on how to get child-View gestures work when added a gesture to a parent-View in Swift UI ??
struct ParentView: View {
#EnvironmentObject var myURLList
var body: some View {
let restartScannerTapGesture = TapGesture(count: 1)
.onEnded {
actionClickID = UUID()
}
ZStack {
if let url = URL(fileURLWithPath: myURLList[0].path){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
.onAppear() {
isVideo = false
}
} else if url.containsVideo {
CustomPlayerView(url: url, isVideo: $isVideo)
.onAppear() {
isVideo = true
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
.onAppear() {
isVideo = false
}
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
.onAppear() {
isVideo = false
}
}
}
}
.simultaneousGesture(restartScannerTapGesture)
}
And here is the CustomPlayerView that shows the Video controls in the first place.
It clearly is the child-View at question. Its gestures should also work, but they don't. Why ??
import SwiftUI
import AVKit
class PlayerViewModel: ObservableObject {
#Published var avPlayer: AVPlayer?
func loadFromUrl(url: URL) {
avPlayer = AVPlayer(url: url)
}
func playVideo() {
avPlayer?.play()
}
func stopVideo() {
avPlayer?.pause()
avPlayer?.replaceCurrentItem(with: nil)
}
}
struct CustomPlayerView: View {
var url : URL
#Binding var isVideo: Bool
#StateObject private var playerViewModel = PlayerViewModel()
var body: some View {
ZStack {
if let url = URL(fileURLWithPath: list.paths[index]){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
.onAppear() {
isVideo = false
}
} else if url.containsVideo {
CustomPlayerView(url: url, isVideo: $isVideo)
.onAppear() {
isVideo = true
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
.onAppear() {
isVideo = false
}
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
.onAppear() {
isVideo = false
}
}
}
}
And the needed extension:
extension URL {
func mimeType() -> String {
let pathExtension = self.pathExtension
if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as NSString, nil)?.takeRetainedValue() {
if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
return mimetype as String
}
}
return "application/octet-stream"
}
var containsImage: Bool {
let mimeType = self.mimeType()
guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)?.takeRetainedValue() else {
return false
}
return UTTypeConformsTo(uti, kUTTypeImage)
}
var containsAudio: Bool {
let mimeType = self.mimeType()
guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)?.takeRetainedValue() else {
return false
}
return UTTypeConformsTo(uti, kUTTypeAudio)
}
var containsVideo: Bool {
let mimeType = self.mimeType()
guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)?.takeRetainedValue() else {
return false
}
return UTTypeConformsTo(uti, kUTTypeMovie)
}
}
I have a problem with Array using ObservableObject in my view. I have an empty array. I call a function at page onAppear. When the data is returned, the view does not update with the new data in array:
class NewsState: ObservableObject {
private let base: String = "api"
let objectWillChange = ObservableObjectPublisher()
#Published var wagsList: Array<UserSlider> = [] {
willSet {
objectWillChange.send()
}
}
func getList() {
let url = NSURL(string: "\(base)/UserApi/getList")
var mutableURLRequest = URLRequest(url: url! as URL)
mutableURLRequest.httpMethod = "GET"
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
AF.request(mutableURLRequest).responseData { response in
guard let data = response.data else { return }
let resp = try! JSONDecoder().decode(Array<UserSlider>.self, from: data)
for i in resp {
let userSlider = UserSlider(id: i.id, uid: i.uid, image: i.image)
self.wagsList.append(userSlider)
}
}
}
}
In my view I have this:
HStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
if(self.newsState.wagsList.count != 0) {
ForEach(self.newsState.wagsList, id: \.self) { wags in
VStack {
HStack {
URLImage(URL(string: "\(wags.image)")!, expireAfter: Date(timeIntervalSinceNow: 10)) { proxy in
proxy.image
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
.clipShape(Circle())
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 2)
)
.contentShape(Circle())
}.frame(width: 62, height: 62)
}
HStack {
Text("10K")
.foregroundColor(Color.white)
.font(Font.custom("Metropolis-Bold", size: 15))
}
HStack {
Text("followers")
.foregroundColor(Color.white)
.font(Font.custom("Metropolis-Normal", size: 15))
}
}
}
} else {
//loader
}
}.onAppear(perform: initPage)
}
}
What am I doing wrong? I see that the problem is caused by ScrollView.
Try this one
class NewsState: ObservableObject {
private let base: String = "api"
#Published var wagsList: Array<UserSlider> = []
func getList() {
let url = NSURL(string: "\(base)/UserApi/getList")
var mutableURLRequest = URLRequest(url: url! as URL)
mutableURLRequest.httpMethod = "GET"
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
AF.request(mutableURLRequest).responseData { response in
guard let data = response.data else { return }
let resp = try! JSONDecoder().decode(Array<UserSlider>.self, from: data)
let results = resp.map { UserSlider(id: $0.id, uid: $0.uid, image: $0.image) }
DispatchQueue.main.async {
self.wagsList = results
}
}
}
}
As it is not clear to me where the error might lay. It could be either in getList or in your View.
This is an easy example of how it works with a Published and ObserverdObject:
Note: your getList function is not in this solution as the error could be with your API, JSON ect.
import SwiftUI
struct ContentView: View {
#ObservedObject var state = NewsState()
var body: some View {
Group { //needed for the IF Statement below
if state.stringList.count > 0 {
ForEach(self.state.stringList, id: \.self){ s in
Text(String(s))
}
}
}.onTapGesture {
self.state.getNewList()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class NewsState: ObservableObject {
#Published var stringList: Array<String> = []
init() {
self.getList()
}
func getList() {
self.stringList.append("New")
}
func getNewList() {
self.stringList = []
self.stringList.append("New new")
}
}