Have 0th item in SwiftUI auto selected - ios

I have data loaded into an HStack that is in a Scroll View in SwiftUI. Right now I have it coded where a user can tap on one of those items and have it selected. I'd like for the 0th item to already be selected upon load.
import SwiftUI
import Combine
import Contentful
struct moviesView : View {
#ObservedObject var fetcher = MovieFetcher()
#State var selectMovie: Movie? = nil
#Binding var show: Bool
var body: some View {
HStack(alignment: .bottom) {
if show {
ScrollView(.horizontal) {
Spacer()
HStack(alignment: .bottom, spacing: 30) {
ForEach(fetcher.movies, id: \.self) { item in
selectableRow(movie: item, selectMovie: self.$selectMovie)
}
.onAppear() {
self.selectMovie = self.movies.count > 0 ? self.movies.first! : nil
}
}
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding(.leading, 46)
.padding(.bottom, 26)
}
}
}
}
struct selectableRow : View {
var movie: Movie
#Binding var selectedMovie: Movie?
#State var initialImage = UIImage()
var body: some View {
ZStack(alignment: .center) {
if movie == selectedMovie {
Image("")
.resizable()
.frame(width: 187, height: 254)
.overlay(
RoundedRectangle(cornerRadius: 13)
Image(uiImage: initialImage)
.resizable()
.cornerRadius(13.0)
.frame(width: 182, height: 249)
.onAppear {
let urlString = "\(urlBase)\(self.movie.movieId).png?"
guard let url = URL(string: self.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
} else {
Image(uiImage: initialImage)
.resizable()
.cornerRadius(13.0)
.frame(width: 135, height: 179)
.onAppear {
let urlString = "\(urlBase)\(self.movie.movieId).png?"
guard let url = URL(string: self.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
}
}
.onTapGesture {
self.selectedMovie = self.movie
}
}
}
EDIT:
I've added the below suggestion but it's still now working properly. Maybe it's where I added the .onAppear?
So when I launch my app I see the 0th item is selected but when I tap on any item the view just reloads but the 0th item always stays selected.
Additional issue:
Also, my #ObservedObject var fetcher = MovieFetcher() in moviesView is called repeatedly.

Since you haven't given the full working code, I wasn't able to reproduce the issue you've mentioned. However, I'd suggest you move the .onAppear from ForEach to the HStack (see code below).
I couldn't reproduce the issue you specified.
var body: some View {
HStack(alignment: .bottom) {
if show {
ScrollView(.horizontal) {
Spacer()
HStack(alignment: .bottom, spacing: 30) {
ForEach(fetcher.movies, id: \.self) { item in
selectableRow(movie: item, selectedMovie: self.$selectMovie)
}
}
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding(.leading, 46)
.padding(.bottom, 26)
.onAppear() {
self.selectMovie = self.fetcher.movies.count > 0 ? self.fetcher.movies.first! : nil
}
}
}
}
In the struct moviesView, use the below code to auto select the first movie.
.onAppear() {
self.selectMovie = self.movies.count > 0 ? self.movies.first! : nil
}
Let me know if you have any other questions.

Related

I have a problem with displaying the data inside the sheet

I am trying to display a link from the variable
#StateObject var modelNew = Api()
But I want to take a value in the sheet
Inside the view, I can view the entire content without a problem
struct detiles2: View {
var model : model
#State var isPlayer = false
#StateObject var modelNew = Api()
var body: some View {
VStack {
let link = URL(string: APIgetURL.PathImage + (model.image_path ?? ""))
URLImage(link!) { image in
image
.resizable()
.shadow(color: .black.opacity(0.2), radius: 10, x: 20, y: 20)
.frame(maxWidth: .infinity, maxHeight: 200)
}
Text("Hello").padding()
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(modelNew.models) { item in
HStack {
ZStack {
Circle()
.frame(maxWidth: 30, maxHeight: 30)
.shadow(color: .black.opacity(0.2), radius: 10, x: 20, y: 20)
Image(systemName: "play.fill")
.foregroundColor(Color(UIColor.systemBackground))
.onTapGesture {
isPlayer.toggle()
}
}.padding(.leading, 15)
Spacer()
VStack(alignment: .trailing, spacing: 15) {
Text(item.title)
Text(item.algiment).font(.footnote).foregroundColor(Color.brown)
}.padding(.trailing, 5)
let link = URL(string: APIgetURL.PathImage + (model.image_path ?? ""))
URLImage(link!) { image in
image
.resizable()
.cornerRadius(20)
.frame(maxWidth: 80, maxHeight: 80)
}
}.padding(.trailing, 5)
}
}
}
Spacer()
}
.onAppear() {
modelNew.getData(url: model.url!)
}
.sheet(isPresented: $isPlayer) {
player(url: <String>) //Here ----- I want to pull it out like right away ForEach(modelNew.models) { item in} To get item.url
}
.ignoresSafeArea()
}
}
Here I am fetching new data and it was displayed correctly in the view, but inside the sheet I was not able to do so
class Api : ObservableObject{
#Published var models : [model] = []
func getData (url : String) {
guard let url = URL(string: url) else { return }
var request = URLRequest(url: url)
let token = "38|xxxxx"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, responce, err in
guard let data = data else { return }
print(data)
do {
let dataModel = try JSONDecoder().decode([model].self, from: data)
DispatchQueue.main.async {
self.models = dataModel
}
} catch {
print("error: ", error)
}
}
.resume()
}
}
you could try a different approach using .sheet(item:...) as in
this sample code, to show your sheet with the video player. You should be able to adapt this code to suit your purpose. Works for me.
import Foundation
import AVFoundation
import AVKit
import SwiftUI
struct SiteURL: Identifiable {
let id = UUID()
var urlString: String
}
struct ContentView: View {
#State var sheetUrl: SiteURL?
var body: some View {
Button("show player sheet", action: {
sheetUrl = SiteURL(urlString: "https://jplayer.org/video/m4v/Finding_Nemo_Teaser.m4v")
})
.sheet(item: $sheetUrl) { site in
if let url = URL(string: site.urlString) {
VideoPlayer(player: AVPlayer(url: url))
}
}
}
}
So, in your .onTapGesture {...} set the sheetUrl with your item info, like in the sample code Button,
instead of isPlayer.toggle().
EDIT-1: here is another example code to show that my answer with .sheet(item:...) works.
struct Model: Identifiable, Codable {
let id = UUID()
var image_path: String
var title: String
var algiment: String
var url: String
}
class Api: ObservableObject {
// for testing
#Published var models: [Model] = [
Model(image_path: "image_path-1", title: "Finding_Nemo_Teaser", algiment: "model algiment-1", url: "https://jplayer.org/video/m4v/Finding_Nemo_Teaser.m4v"),
Model(image_path: "image_path-2", title: "Incredibles_Teaser", algiment: "model algiment-2", url: "https://jplayer.org/video/m4v/Incredibles_Teaser.m4v")]
func getData(url: String) {
guard let url = URL(string: url) else { return }
var request = URLRequest(url: url)
let token = "38|xxxxx"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, responce, err in
guard let data = data else { return }
print(data)
do {
let dataModel = try JSONDecoder().decode([Model].self, from: data)
DispatchQueue.main.async {
self.models = dataModel
}
} catch {
print("error: ", error)
}
}
.resume()
}
}
struct TestView: View {
#State var videoUrl: SiteURL? // <-- here
// var model : model
#StateObject var modelNew = Api()
var body: some View {
VStack {
// let link = URL(string: "https://ccc/" + (model.image_path ?? ""))
// URLImage(link!) { image in
// image
// .resizable()
// .shadow(color: .black.opacity(0.2), radius: 10, x: 20, y: 20)
// .frame(maxWidth: .infinity, maxHeight: 200)
// }
Text("Hello").padding()
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(modelNew.models) { item in
HStack {
ZStack {
Circle()
.frame(maxWidth: 30, maxHeight: 30)
.shadow(color: .black.opacity(0.2), radius: 10, x: 20, y: 20)
Image(systemName: "play.fill")
.foregroundColor(Color(UIColor.systemBackground))
.onTapGesture {
videoUrl = SiteURL(urlString: item.url) // <-- here
}
}.padding(.leading, 15)
Spacer()
VStack(alignment: .trailing, spacing: 15) {
Text(item.title)
Text(item.algiment).font(.footnote).foregroundColor(Color.brown)
}.padding(.trailing, 5)
// let link = URL(string: APIgetURL.PathImage + (model.image_path ?? ""))
// URLImage(link!) { image in
// image
// .resizable()
// .cornerRadius(20)
// .frame(maxWidth: 80, maxHeight: 80)
// }
}.padding(.trailing, 5)
}
}
}
Spacer()
}
.onAppear() {
// modelNew.getData(url: model.url!)
}
.sheet(item: $videoUrl) { site in // <-- here
if let url = URL(string: site.urlString) {
VideoPlayer(player: AVPlayer(url: url))
}
}
.ignoresSafeArea()
}
}
struct ContentView: View {
var body: some View {
TestView()
}
}

How do I let text retrieved from Firestore load upon the view being shown?

Intro
Hi there. I recently asked a different question asking how to implement user-triggered FCMs. I quickly realised that in order to implement FCMs I needed to add another feature to my app, which is why I am here now. I'm trying, trust me.
My question / problem
In the picture I attached, you can see the text that is supposed to show when the user clicks on the friends tab. But it doesn't do that. It does when I refresh the list, because I put the function in the refreshable attribute. I did also put the function in the view initialisation, but it doesn't do what I want it to do. So my question would be, a) why it doesn't load? and b) what would be an approach to solve my problem?
Code reference
This function is called in the init{} of the view and .refreshable{} of the list inside the view. (I also tried adding it via .onAppear{} in the NavigationLink of the parent view.)
#State var bestfriend: String = ""
func getBestie() {
let db = Firestore.firestore()
let docRef = db.collection("users").document(email)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let bestie = document.get("bestie") as? String ?? "error: bestie"
bestfriend = String(bestie)
} else {
print("Document does not exist")
}
}
}
Image for reference
Thanks and annotation
Thank you very much in advance, I'm amazed every day by how amazing this community is and how so many people are willing to help. If you need me to add anything else, of course I will do that. I hope I'll be wise enough one day to help other people with their problems as well.
Edit
The view
import Firebase
import FirebaseAuth
import SDWebImage
import SDWebImageSwiftUI
import SwiftUI
struct View_Friend_Tab: View {
#ObservedObject var friends_model = Model_User()
#State var friendWho = ""
init() {
friends_model.getFriendlist()
//friends_model.getBestie()
getBestie()
}
//VARS
let gifurl = URL(string: "https://c.tenor.com/BTCEb08QgBgAAAAC/osita-iheme-aki-and-pawpaw.gif")
let avatarURL = URL(
string:
"https://firebasestorage.googleapis.com/v0/b/universerp-72af2.appspot.com/o/avatars%2Fanime-girl-white-hair-1-cropped.jpg?alt=media&token=efba4215-850d-41c8-8c90-385f7a572e94"
)
#State var showingAlert = false
#State var showFriendRequests = false
#State var bestfriend: String = ""
func getBestie() {
let db = Firestore.firestore()
let docRef = db.collection("users").document(email)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let bestie = document.get("bestie") as? String ?? "error: bestie"
bestfriend = String(bestie)
} else {
print("Document does not exist")
}
}
}
var body: some View {
if !showFriendRequests {
VStack {
NavigationView {
/*
List (friends_model.friend_list) { item in
HStack {
Text(item.email)
Spacer()
}
}
.refreshable{
friends_model.getFriendlist()
}
.listStyle(.grouped)
*/
List {
Section(header: Text("management")) {
NavigationLink(destination: View_Friend_Best_Tab()) {
Label("Select your bestie", systemImage: "star.fill")
}
NavigationLink(destination: View_Friend_Requests_Tab()) {
Label("Manage friend requests", systemImage: "person.fill.questionmark")
}
NavigationLink(destination: View_Friend_Add_Tab()) {
Label("Add friend", systemImage: "person.fill.badge.plus")
}
}
ForEach(friends_model.friend_list) { item in
let avURL = URL(string: item.avatarURL)
Section(header: Text(item.username)) {
HStack {
VStack(alignment: .leading) {
if bestfriend == item.email {
Text("Is your Shin'yū")
.foregroundColor(Color("lightRed"))
.fontWeight(.bold)
.font(.footnote)
}
Text(item.username)
.fontWeight(.bold)
.frame(alignment: .leading)
Text(item.email)
.font(.footnote)
.multilineTextAlignment(.leading)
}
Spacer()
WebImage(url: avURL)
.resizable(resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.clipShape(Circle())
.shadow(radius: 5)
.overlay(Circle().stroke(Color.black, lineWidth: 1))
}
Button("Remove", role: .destructive) {
showingAlert = true
}
.alert("Do you really want to remove this friend?", isPresented: $showingAlert) {
HStack {
Button("Cancel", role: .cancel) {}
Button("Remove", role: .destructive) {
friendWho = item.email
removeFriend()
withAnimation {
friends_model.getFriendlist()
}
}
}
}
}
}
}
.navigationTitle("Your Friends")
.navigationViewStyle(.automatic)
.refreshable {
friends_model.getFriendlist()
getBestie()
}
.listStyle(.insetGrouped)
Spacer()
}
}
} else {
View_Friend_Requests_Tab()
}
}
}
struct View_Friend_Tab_Previews: PreviewProvider {
static var previews: some View {
View_Friend_Tab()
}
}
As previously stated, the function is being called in the init block and when the list is refreshed.
As a general recommendation, use view models to keep your view code clean.
In SwiftUI, a view's initialiser should not perform any expensive / long-running computations. Keep in mind that in SwiftUI, a view is only a description of your UI, not the UI itself. Any state management should be handled outside of the initialiser.
In your case, use .onAppear or .task:
import Firebase
import FirebaseAuth
import SDWebImage
import SDWebImageSwiftUI
import SwiftUI
class FriendsViewModel: ObservableObject {
#Published var bestfriend: String = ""
func getBestie() {
let db = Firestore.firestore()
let docRef = db.collection("users").document(email)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
// consider using Codable for mapping - see https://peterfriese.dev/posts/firestore-codable-the-comprehensive-guide/
let bestie = document.get("bestie") as? String ?? "error: bestie"
self.bestfriend = String(bestie)
} else {
print("Document does not exist")
}
}
}
// add other functions and properties here.
}
struct FriendsView: View {
#ObservedObject var viewModel = FriendsViewModel()
//VARS
let gifurl = URL(string: "https://c.tenor.com/BTCEb08QgBgAAAAC/osita-iheme-aki-and-pawpaw.gif")
let avatarURL = URL(
string:
"https://firebasestorage.googleapis.com/v0/b/universerp-72af2.appspot.com/o/avatars%2Fanime-girl-white-hair-1-cropped.jpg?alt=media&token=efba4215-850d-41c8-8c90-385f7a572e94"
)
#State var showingAlert = false
#State var showFriendRequests = false
#State var bestfriend: String = ""
var body: some View {
if !showFriendRequests {
VStack {
NavigationView {
List {
Section(header: Text("management")) {
NavigationLink(destination: SelectBestieView()) {
Label("Select your bestie", systemImage: "star.fill")
}
NavigationLink(destination: ManageFriendRequestsView()) {
Label("Manage friend requests", systemImage: "person.fill.questionmark")
}
NavigationLink(destination: AddFriendsView()) {
Label("Add friend", systemImage: "person.fill.badge.plus")
}
}
ForEach(viewModel.friends) { friend in
let avURL = URL(string: friend.avatarURL)
Section(header: Text(friend.username)) {
HStack {
VStack(alignment: .leading) {
if bestfriend == friend.email {
Text("Is your Shin'yū")
.foregroundColor(Color("lightRed"))
.fontWeight(.bold)
.font(.footnote)
}
Text(friend.username)
.fontWeight(.bold)
.frame(alignment: .leading)
Text(friend.email)
.font(.footnote)
.multilineTextAlignment(.leading)
}
Spacer()
WebImage(url: avURL)
.resizable(resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.clipShape(Circle())
.shadow(radius: 5)
.overlay(Circle().stroke(Color.black, lineWidth: 1))
}
Button("Remove", role: .destructive) {
showingAlert = true
}
.alert("Do you really want to remove this friend?", isPresented: $showingAlert) {
HStack {
Button("Cancel", role: .cancel) {}
Button("Remove", role: .destructive) {
friendWho = friend.email
viewModel.removeFriend()
withAnimation {
viewModel.getFriendlist()
}
}
}
}
}
}
}
.navigationTitle("Your Friends")
.navigationViewStyle(.automatic)
.onAppear {
viewModel.getFriendlist()
viewModelgetBestie()
}
.refreshable {
viewModel.getFriendlist()
viewModelgetBestie()
}
.listStyle(.insetGrouped)
Spacer()
}
}
} else {
FriendRequestsView()
}
}
}
struct FriendsViewPreviews: PreviewProvider {
static var previews: some View {
FriendsView()
}
}

Issues with assigning URL to an optional URL State variable

I am trying to assign variable "furl" to #State var fileurl: URL? in saveImage() of my MainScreenView(). In saveImage Print(furl) shows the URL, but after assigning fileURL = furl, print(fileurl) returns nil, that's the issue. Removing the optional doesn't allow it to build as:
if let furl = fileurl {
let data = try Data(contentsOf: furl)
if let img = UIImage(data: data) {
return img
}.
of loadImage()Throws an error of "Initializer for conditional binding must have Optional type, not 'URL'". I'm unsure what to do in this case.
Here's MainScreenView
import SwiftUI
import SafariServices
struct MainScreenView: View {
#State var isfound = true
#State var failed = false
#State var ShowInstruction = false
#State var fileurl: URL? //I'm guessing since this is an optional, it's not allow it to do something correctly?
#State var ShowBadge = false
#State var ShowPortal = false
#State var ShowDetails = false
#State var alert = false
#State var listalert = false
#State var truebadge: Bool = false
var body: some View {
NavigationView {
Color("BackgroundMain")
.edgesIgnoringSafeArea(.all)
.overlay(
VStack{
Button {
ShowBadge = true
} label: {
if truebadge != true{
VStack{
Image(systemName: "person")
.font(.system(size: 120))
.foregroundColor(Color.white)
.padding()
Text("expired compliance")
.font(.system(size: 30))
.foregroundColor(Color.white)
}.offset(y: -30)
}
else {
VStack{
Image(systemName: "person")
.font(.system(size: 120))
.foregroundColor(Color("MainBadgeScreen"))
.padding()
Text("Show Complaince")
.font(.system(size: 30))
.foregroundColor(Color("MainBadgeScreen"))
}.offset(y: -30)
}
}
Button {
} label: {
HStack {
Image(systemName: "lanyardcard")
Text("Student ID")
}
.font(.system(size: 23))
.foregroundColor(.white)
.frame(width: 200, height: 65, alignment: .center)
.background(Color.blue)
.cornerRadius(15)
.offset(y: -10)
}
Button {
listalert.toggle()
} label: {
HStack{
Image(systemName: "gear.circle")
Text("Settings")
}
.font(.system(size: 23))
.foregroundColor(.white)
.frame(width: 200, height: 65, alignment: .center)
.background(Color.gray)
.cornerRadius(15)
}
}
).sheet(isPresented: $listalert, content: {
NavigationView{
List {
Button(action: {
ShowPortal = true
}) {
HStack{
Text("Student Health Portal")
Spacer()
Image(systemName: "heart.fill")
}.foregroundColor(Color.red)
.font(.system(size: 20))}
Button(action: {
ShowInstruction = true
}) {
HStack{
Text("Instructions")
Spacer()
Image(systemName: "questionmark.circle")
}.font(.system(size: 20))}
Button(action: {
ShowDetails = true
}) {
HStack{
Text("About")
Spacer()
Image(systemName: "info.circle")
}.font(.system(size: 20))}
}
.navigationBarTitle(Text("Settings"), displayMode: .inline)
}
.sheet(isPresented: $ShowPortal, content: {
safari()
})
.sheet(isPresented: $ShowInstruction, content: {
Instruction()
})
.sheet(isPresented: $ShowDetails, content: {
Details()
})
})
.sheet(isPresented: $ShowBadge, content: {
BadgeScreenView(complianceview: loadImage(), truebadge: truebadge)
})
.navigationTitle("Home")
.navigationBarTitleDisplayMode(.large)
}
}
func saveImage(binding: Binding<Bool>, image: UIImage){
//Need to set #State var truebadge to 'true'
let finalcompliance = image.self.pngData()
do {
let furl = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("Compliance")
.appendingPathExtension("png")
print(furl) // shows url from furl
fileurl = furl // assigning furl to fileurl allows it to be assigned. But ends up becoming nil
print(fileurl) //nil value; doesn't allow the image to load in loadimage() even though assigned to furl
try finalcompliance?.write(to: furl)
} catch{
print("could not create imageFile")
}
}
func loadImage() -> UIImage {
do {
print("showing the image!")
if let furl = fileurl {
let data = try Data(contentsOf: furl)
if let img = UIImage(data: data) {
return img
}
}
} catch {
print("error: \(error)") // todo
}
return UIImage()
}
}
struct safari : UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<safari>) -> SFSafariViewController{
let controller = SFSafariViewController(url: URL(string: "https://patientportal.bowiestate.edu/login_directory.aspx")!)
return controller
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<safari>) {
}
}

Update each button item inside a list instead of all buttons at once in Swift / SwiftUI

I'm currently trying to change a button from one view to another depending on what the user presses. They would get a list/scrollview of pending requests and if the user clicks on accept, it should change that button to an accepted button and if the user clicks on reject, it should change that button to a rejected button. I'm running into a problem where if a user clicks on accept, all the pending request buttons gets changed to an accepted button and similarly for the reject case.
import SwiftUI
import Firebase
import SDWebImageSwiftUI
import Foundation
import Combine
struct BottomSheet: View {
// #ObservedObject var userData : UserViewModel
var edges = UIApplication.shared.windows.first?.safeAreaInsets
#StateObject var userData = UserViewModel()
#State var declinedRequest: Bool = false
#State var acceptedRequest: Bool = false
var body: some View {
VStack{
Spacer()
VStack(spacing: 12){
Divider()
// here are the buttons inside the scrollview
ScrollView{
ForEach(userData.pendingFriendUsers){ person in
HStack{
if person.pic != ""{
WebImage(url: URL(string: person.pic)!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
.clipShape(Circle())
.padding(.leading, 30)
.padding(.trailing, 10)
}else{
Circle()
.stroke(Color.black.opacity(0.8), lineWidth: 2)
.frame(width: 60, height: 60)
.padding(.leading, 20)
.padding(.trailing, 10)
}
VStack(alignment: .leading){
Text("\(person.name)")
.font(.custom("Helvetica Neue", size: 16))
.foregroundColor(Color.white).bold()
Text("#\(person.username)")
.font(.custom("Helvetica Neue", size: 16))
.foregroundColor(Color.white)
.opacity(0.8)
}
Spacer()
if person.isFriends == 2 {
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color("Dark-Grey"))
.frame(width: 150, height: 40)
.padding(.trailing, 25)
.overlay(
Text("Request removed")
.font(.custom("Helvetica Neue", size: 14))
.foregroundColor(Color.white)
.padding(.trailing, 25)
)
}else if person.isFriends == 1 {
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color.white)
.frame(width: 150, height: 40)
.padding(.trailing, 25)
.overlay(
Text("Request accepted")
.font(.custom("Helvetica Neue", size: 14))
.foregroundColor(Color.black)
.padding(.trailing, 25)
)
}else {
Button(action: {
print("declined friend request for \(person.uid)")
withAnimation(){declinedRequest = true}
userData.declineFriendRequest(otherUserUID: person.uid)
}){
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color("Dark-Grey"))
.frame(width: 55, height: 26.5)
.padding(.trailing, 13)
.overlay(
Image("x")
.renderingMode(.template)
.resizable()
.foregroundColor(Color.white)
.opacity(0.8)
.frame(width: 12, height: 12)
.padding(.trailing, 12)
)
}
Button(action: {
print("accepted friend request for \(person.uid)")
withAnimation(){acceptedRequest = true}
userData.acceptFriendRequest(otherUserUID: person.uid)
}){
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color.white)
.frame(width: 50, height: 25)
.padding(.trailing, 35)
.overlay(
Image("check")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 20, height: 20)
.padding(.trailing, 33)
)
}
}
}.padding(.top, 12.5)
}
}
Spacer()
.contentShape(Rectangle())
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height / 1.15)
.padding(.top)
.background(Color("gray")
.clipShape(CustomCorner(corners: [.topLeft,.topRight])))
.offset(y: offset)
// bottom sheet remove swipe gesture....
.gesture(DragGesture().onChanged(onChanged(value:)).onEnded(onEnded(value:)))
.offset(y: showSheet ? 0 : UIScreen.main.bounds.height)
}
.ignoresSafeArea()
.background(
Color.black.opacity(showSheet ? 0.3 : 0).ignoresSafeArea()
.onTapGesture(perform: {
withAnimation{showSheet.toggle()}
})
)
}
}
When declinedRequest or acceptedRequest get modified, its not mapped to the button so everything in the for view gets changed instead of the individual button. I've added my basic code below but some things I tried are making a published variable in my user data model class but it doesn't get updated in the for each as it moves on by then and I also tried making a published variable here. It looks like I have something to do with indices and mapping but I'm unsure what the best approach is. Thanks for your help
Edit:
Here's relevant sections from my user view model:
import SwiftUI
import Firebase
import Combine
import Foundation
struct pendingFriendUser: Identifiable {
var id: Int
var uid: String
var name: String
var username: String
var pic: String
var isFriends: Int
init(id: Int, uid: String, name: String, username: String, pic: String, isFriends: Int){
self.id = id
self.uid = uid
self.name = name
self.username = username
self.pic = pic
self.isFriends = isFriends
}
}
class UserViewModel : ObservableObject{
#Published var userInfo = UserModel(username: "", pic: "", name: "", age: 1, uid: "", phoneNumber: "")
let ref = Firestore.firestore()
let uid = Auth.auth().currentUser!.uid
#Published var pendingFriendUsers: [pendingFriendUser]
//show add friends sheet
#AppStorage("showSheet") var showSheet = false
//check friendship variabe
#Published var isFriend = 0
init() {
self.searchedUsers = []
self.pendingFriendUsers = []
fetchUser(uid: uid) { (user) in
self.userInfo = user
}
}
func getPendingRequests(){
//check if friends has any false memberships
var pendingFriendRequests: [String: Bool] = [:]
self.ref.collection("Users").document(uid).getDocument(){
(document, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
if document!.data()!["friends"] != nil {
pendingFriendRequests = document!.data()!["friends"] as! [String : Bool]
}
//filter based on false pending friend requests
self.pendingFriendUsers.removeAll()
var friendUserID = 0
for key in pendingFriendRequests.keys {
if pendingFriendRequests[key] == false {
self.ref.collection("Users").document(key).getDocument(){
(friendDocument, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
let pendingFriendUsername = (friendDocument?.data()?["username"]) as! String
let pendingFriendUID = (friendDocument?.data()?["uid"]) as! String
let pendingFriendName = (friendDocument?.data()?["name"]) as! String
let pendingFriendPic = (friendDocument?.data()?["imageurl"]) as! String
self.pendingFriendUsers.append(pendingFriendUser(id: friendUserID, uid: pendingFriendUID , name: pendingFriendName , username: pendingFriendUsername, pic: pendingFriendPic, isFriends: 0))
friendUserID += 1
}
}
}
}
}
}
}
...
func acceptFriendRequest(otherUserUID: String){
for var pendingFriend in pendingFriendUsers {
if pendingFriend.uid == otherUserUID {
pendingFriend.isFriends = 1
}
}
self.ref.collection("Users").document(self.uid).setData(
[ "friends": [
otherUserUID: true
] ]
, merge: true)
self.ref.collection("Users").document(otherUserUID).setData(
[ "friends": [
self.uid: true
] ]
, merge: true)
}
func declineFriendRequest(otherUserUID: String){
for var pendingFriend in pendingFriendUsers {
if pendingFriend.uid == otherUserUID {
pendingFriend.isFriends = 2
}
}
self.ref.collection("Users").document(self.uid).updateData([
"friends.\(otherUserUID)": FieldValue.delete(),
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
}
func checkFriendRequest(otherUserUID: String){
//0 if not found in friend list or friend request
//1 means theyre friends
//2 means that user sent self/me a friend request
var pendingFriendRequests: [String: Bool] = [:]
self.ref.collection("Users").document(self.uid).getDocument(){
(document, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
pendingFriendRequests = document!.data()!["friends"] as! [String : Bool]
for key in pendingFriendRequests.keys {
if key == otherUserUID{
if pendingFriendRequests[key] == true {
self.isFriend = 1
}else if pendingFriendRequests[key] == false {
self.isFriend = 2
}
}
}
}
}
}
}
Edited with isFriends variable
Right now, you have this:
for var pendingFriend in pendingFriendUsers {
if pendingFriend.uid == otherUserUID {
pendingFriend.isFriends = 1
}
}
This doesn't actually modify anything in pendingFriendUsers, because pendingFriendUser is a struct, which gets passed by value in Swift, not by reference. So var pendingFriend is a copy of the version in pendingFriendUsers.
Instead, you could do something like this:
self.pendingFriendUsers = self.pendingFriendUsers.map { pendingFriend in
guard pendingFriend.uid == otherUserUID else { return pendingFriend } //return the original if the UID doesn't match
var modified = pendingFriend //make a mutable copy
modified.isFriends = 1
return modified //return the modified version
}
or:
guard let index = self.pendingFriendUsers.firstIndex(where: { $0.uid == otherUserUID }) else { return } //get the index of the matching UID
self.pendingFriendUsers[index].isFriends = 1 //modify the item at that index

View doesn't get updated when using ObservableObject

I'm trying to build an Instagram clone app using SwiftUI.
I'm fetching the data through Firebase and trying to achieve a UI update every time the data in the server changes.
For some reason, when I first open the app and fetch the data, the body of my view gets called, but the UI doesn't change. I even put a breakpoint and saw the body gets called and contains the correct information, it's just the UI which doesn't get updated.
I have a few tabs in my app, and when I switch to another tab (which doesn't contain anything but a Text yet), suddenly the UI does gets updated.
Please see the gif below:
Here is my code:
HomeView:
struct HomeView: View {
#ObservedObject private var fbData = firebaseData
var body: some View {
TabView {
//Home Tab
NavigationView {
ScrollView(showsIndicators: false) {
ForEach(self.fbData.posts.indices, id: \.self) { postIndex in
PostView(post: self.$fbData.posts[postIndex])
.listRowInsets(EdgeInsets())
.padding(.vertical, 5)
}
}
.navigationBarTitle("Instagram", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
print("Camera btn pressed")
}, label: {
Image(systemName: "camera")
.font(.title)
})
, trailing:
Button(action: {
print("Messages btn pressed")
}, label: {
Image(systemName: "paperplane")
.font(.title)
})
)
} . tabItem({
Image(systemName: "house")
.font(.title)
})
Text("Search").tabItem {
Image(systemName: "magnifyingglass")
.font(.title)
}
Text("Upload").tabItem {
Image(systemName: "plus.app")
.font(.title)
}
Text("Activity").tabItem {
Image(systemName: "heart")
.font(.title)
}
Text("Profile").tabItem {
Image(systemName: "person")
.font(.title)
}
}
.accentColor(.black)
.edgesIgnoringSafeArea(.top)
}
}
FirebaseData:
let firebaseData = FirebaseData()
class FirebaseData : ObservableObject {
#Published var posts = [Post]()
let postsCollection = Firestore.firestore().collection("Posts")
init() {
self.fetchPosts()
}
//MARK: Fetch Data
private func fetchPosts() {
self.postsCollection.addSnapshotListener { (documentSnapshot, err) in
if err != nil {
print("Error fetching posts: \(err!.localizedDescription)")
return
} else {
documentSnapshot!.documentChanges.forEach { diff in
if diff.type == .added {
let post = self.createPostFromDocument(document: diff.document)
self.posts.append(post)
} else if diff.type == .modified {
self.posts = self.posts.map { (post) -> Post in
if post.id == diff.document.documentID {
return self.createPostFromDocument(document: diff.document)
} else {
return post
}
}
} else if diff.type == .removed {
for index in self.posts.indices {
if self.posts[index].id == diff.document.documentID {
self.posts.remove(at: index)
}
}
}
}
}
}
}
private func createPostFromDocument(document: QueryDocumentSnapshot) -> Post {
let data = document.data()
let id = document.documentID
let imageUrl = data["imageUrl"] as! String
let authorUsername = data["authorUsername"] as! String
let authorProfilePictureUrl = data["authorProfilePictureUrl"] as! String
let postLocation = data["postLocation"] as! String
let postDescription = data["postDescription"] as! String
let numberOfLikes = data["numberOfLikes"] as! Int
let numberOfComments = data["numberOfComments"] as! Int
let datePosted = (data["datePosted"] as! Timestamp).dateValue()
let isLiked = data["isLiked"] as! Bool
return Post(id: id, imageUrl: imageUrl, authorUsername: authorUsername, authorProfilePictureUrl: authorProfilePictureUrl, postLocation: postLocation, postDescription: postDescription, numberOfLikes: numberOfLikes, numberOfComments: numberOfComments, datePosted: datePosted, isLiked: isLiked)
}
}
If you need me to post more code please let me know.
Update:
PostView:
struct PostView: View {
#Binding var post: Post
var body: some View {
VStack(alignment: .leading) {
//Info bar
HStack {
WebImage(url: URL(string: post.authorProfilePictureUrl))
.resizable()
.frame(width: 40, height: 40)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 2) {
Text(post.authorUsername).font(.headline)
Text(post.postLocation)
}
Spacer()
Button(action: {
print("More options pressed")
}, label: {
Image(systemName: "ellipsis")
.font(.title)
.foregroundColor(.black)
}).buttonStyle(BorderlessButtonStyle())
}
.padding(.horizontal)
//Main Image
WebImage(url: URL(string: post.imageUrl))
.resizable()
.aspectRatio(contentMode: .fit)
//Tools bar
HStack(spacing: 15) {
Button(action: {
self.post.isLiked.toggle()
print("Like btn pressed")
}, label: {
Image(systemName: post.isLiked ? "heart.fill" : "heart")
.font(.title)
.foregroundColor(.black)
}).buttonStyle(BorderlessButtonStyle())
Button(action: {
print("Comments btn pressed")
}, label: {
Image(systemName: "message")
.font(.title)
.foregroundColor(.black)
}).buttonStyle(BorderlessButtonStyle())
Button(action: {
print("Share btn pressed")
}, label: {
Image(systemName: "paperplane")
.font(.title)
.foregroundColor(.black)
}).buttonStyle(BorderlessButtonStyle())
Spacer()
Button(action: {
print("Bookmark btn pressed")
}, label: {
Image(systemName: "bookmark")
.font(.title)
.foregroundColor(.black)
}).buttonStyle(BorderlessButtonStyle())
}.padding(8)
Text("Liked by \(post.numberOfLikes) users")
.font(.headline)
.padding(.horizontal, 8)
Text(post.postDescription)
.font(.body)
.padding(.horizontal, 8)
.padding(.vertical, 5)
Button(action: {
print("Show comments btn pressed")
}, label: {
Text("See all \(post.numberOfComments) comments")
.foregroundColor(.gray)
.padding(.horizontal, 8)
}).buttonStyle(BorderlessButtonStyle())
Text(post.datePostedString)
.font(.caption)
.foregroundColor(.gray)
.padding(.horizontal, 8)
.padding(.vertical, 5)
}
}
}
Post:
struct Post : Identifiable, Hashable {
var id: String
var imageUrl: String
var authorUsername: String
var authorProfilePictureUrl: String
var postLocation: String
var postDescription: String
var numberOfLikes: Int
var numberOfComments: Int
var datePostedString: String
var isLiked: Bool
init(id: String, imageUrl: String, authorUsername: String, authorProfilePictureUrl: String, postLocation: String, postDescription : String, numberOfLikes: Int, numberOfComments: Int, datePosted: Date, isLiked: Bool) {
self.id = id
self.imageUrl = imageUrl
self.authorUsername = authorUsername
self.authorProfilePictureUrl = authorProfilePictureUrl
self.postLocation = postLocation
self.postDescription = postDescription
self.numberOfLikes = numberOfLikes
self.numberOfComments = numberOfComments
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMMM dd, yyyy"
self.datePostedString = dateFormatter.string(from: datePosted)
self.isLiked = isLiked
}
}
Thank you!
The problem is that when the app starts your array is empty, and the ScrollView stops updating, you can replace it for a VStack and it will work (just for testing).
The solution is to wrap the ForEach(or the ScrollView) with a condition, like this:
if (fbData.posts.count > 0) {
ForEach(self.fbData.posts.indices, id: \.self) { postIndex in
PostView(post: self.$fbData.posts[postIndex])
.listRowInsets(EdgeInsets())
.padding(.vertical, 5)
}
}

Resources