View doesn't get updated when using ObservableObject - ios

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

Related

Swiftui items get duplicated in all views when added to a single custom view

I'm struggling with the following issue: I'm trying to build a very simple app that lets you add items in a dedicated view that can be collapsed. I managed to write a simple function that lets me add multiple of these custom collapsable views. It's my first app so I wanted to follow the MVVM protocol. I think I got confused along the way because now every item I add gets automatically added to all the custom collapsable views I made. Is there any way to fix this? I thought using the UUID would solve this issue.. I'm guessing that I have to customise the "saveButtonPressed" function, but I don't know how to tell it to only add the item to the view where I pressed the "plus" button..
Here are the Models for the individual items and the collapsable view:
struct ItemModel: Identifiable, Equatable {
let id: String
let title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel {
return ItemModel(id: id, title: title)
}
}
--
import Foundation
struct CollapsableItem: Equatable, Identifiable, Hashable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> CollapsableItem {
return CollapsableItem(id: id, title: title)
}
}
These are my two ViewModels:
class ListViewModel: ObservableObject {
#Published var items: [ItemModel] = []
init() {
getItems()
}
func getItems() {
let newItems = [
ItemModel(title: "List Item1"),
ItemModel(title: "List Item2"),
ItemModel(title: "List Item3"),
]
items.append(contentsOf: newItems)
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
--
class CollapsedViewModel: ObservableObject {
#Published var collapsableItems: [CollapsableItem] = []
#Published var id = UUID().uuidString
init() {
getCollapsableItems()
}
func getCollapsableItems() {
let newCollapsableItems = [
CollapsableItem(title: "Wake up")
]
collapsableItems.append(contentsOf: newCollapsableItems)
}
func addCollapsableItem(title: String) {
let newCollapsableItem = CollapsableItem(title: title)
collapsableItems.append(newCollapsableItem)
}
func updateCollapsableItem(collapsableItem: CollapsableItem) {
if let index = collapsableItems.firstIndex(where: { $0.id ==
collapsableItem.id}) {
collapsableItems[index] =
collapsableItem.updateCompletion()
}
}
}
The item view:
struct ListRowView: View {
#EnvironmentObject var listViewModel: ListViewModel
let item: ItemModel
var body: some View {
HStack() {
Text(item.title)
.font(.body)
.fontWeight(.bold)
.foregroundColor(.white)
.multilineTextAlignment(.center)
.lineLimit(1)
.frame(width: 232, height: 16)
}
.padding( )
.frame(width: 396, height: 56)
.background(.gray)
.cornerRadius(12.0)
}
}
The collapsable view:
struct CollapsedView2<Content: View>: View {
#State var collapsableItem: CollapsableItem
#EnvironmentObject var collapsedViewModel: CollapsedViewModel
#State private var collapsed: Bool = true
#EnvironmentObject var listViewModel: ListViewModel
#State var label: () -> Text
#State var content: () -> Content
#State private var show = true
var body: some View {
ZStack{
VStack {
HStack{
Button(
action: { self.collapsed.toggle() },
label: {
HStack() {
Text("Hello")
.font(.title2.bold())
Spacer()
Image(systemName: self.collapsed ? "chevron.down" :
"chevron.up")
}
.padding(.bottom, 1)
.background(Color.white.opacity(0.01))
}
)
.buttonStyle(PlainButtonStyle())
Button(action: saveButtonPressed, label: {
Image(systemName: "plus")
.font(.title2)
.foregroundColor(.white)
})
}
VStack {
self.content()
}
ForEach(listViewModel.items) { item in ListRowView (item: item)
}
.lineLimit(1)
.fixedSize(horizontal: true, vertical: true)
.frame(minWidth: 396, maxWidth: 396, minHeight: 0, maxHeight: collapsed ?
0 : .none)
.animation(.easeInOut(duration: 1.0), value: show)
.clipped()
.transition(.slide)
}
}
}
func saveButtonPressed() {
listViewModel.addItem(title: "Hello")
}
}
and finally the main view:
struct ListView: View {
#EnvironmentObject var listViewModel: ListViewModel
#EnvironmentObject var collapsedViewModel: CollapsedViewModel
var body: some View {
ZStack{
ScrollView{
VStack{
HStack{
Text("MyFirstApp")
.font(.largeTitle.bold())
Button(action: newCollapsablePressed, label: {
Image(systemName: "plus")
.font(.title2)
.foregroundColor(.white)
})
}
.padding()
.padding(.leading)
ForEach(collapsedViewModel.collapsableItems) { collapsableItem in
CollapsedView2 (collapsableItem: collapsableItem,
label: { Text("") .font(.title2.bold()) },
content: {
HStack {
Text("")
Spacer() }
.frame(maxWidth: .infinity)
})
}
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.statusBar(hidden: false)
.navigationBarHidden(true)
}
}
func newCollapsablePressed() {
collapsedViewModel.addCollapsableItem(title: "hello2")
}
}
Would love to understand how I could fix this!
There is the anwser for your comment about add item in each CollapsedView2.
Because ListViewModel is not ObservableObject (ListViewModel is difference from each CollapsableItem). You should use "#State var items: [ItemModel]".
struct CollapsedView2<Content: View>: View {
#State var collapsableItem: CollapsableItem
// #State var listViewModel = ListViewModel()
#State var collapsed: Bool = true
#State var label: () -> Text
#State var content: () -> Content
#State private var show = true
#State var items: [ItemModel] = []
#State var count = 1
var body: some View {
VStack {
HStack{
Text("Hello")
.font(.title2.bold())
Spacer()
Button( action: { self.collapsed.toggle() },
label: {
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
)
.buttonStyle(PlainButtonStyle())
Button(action: saveButtonPressed, label: {
Image(systemName: "plus")
.font(.title2)
// .foregroundColor(.white)
})
}
VStack {
self.content()
}
ForEach(items) { item in
ListRowView (item: item)
}
.lineLimit(1)
.fixedSize(horizontal: true, vertical: true)
.frame(minHeight: 0, maxHeight: collapsed ? 0 : .none)
.animation(.easeInOut(duration: 1.0), value: show)
.clipped()
.transition(.slide)
}
}
func saveButtonPressed() {
addItem(title: "Hello \(count)")
count += 1
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
There is the anwser. Ask me if you have some questions
struct ListView: View {
#StateObject var collapsedViewModel = CollapsedViewModel()
var body: some View {
ScrollView{
VStack{
HStack{
Text("MyFirstApp")
.font(.largeTitle.bold())
Button(action: newCollapsablePressed, label: {
Image(systemName: "plus")
.font(.title2)
// .foregroundColor(.white)
})
}
ForEach(collapsedViewModel.collapsableItems) { collapsableItem in
CollapsedView2 (collapsableItem: collapsableItem,
label: { Text("") .font(.title2.bold()) },
content: {
HStack {
Text("")
Spacer()
}
})
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.statusBar(hidden: false)
.navigationBarHidden(true)
}
func newCollapsablePressed() {
collapsedViewModel.addCollapsableItem(title: "hello2")
}
}
struct CollapsedView2<Content: View>: View {
#State var collapsableItem: CollapsableItem
#State var listViewModel = ListViewModel()
#State var collapsed: Bool = true
#State var label: () -> Text
#State var content: () -> Content
#State private var show = true
var body: some View {
VStack {
HStack{
Button( action: { self.collapsed.toggle() },
label: {
HStack() {
Text("Hello")
.font(.title2.bold())
Spacer()
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
.padding(.bottom, 1)
.background(Color.white.opacity(0.01))
}
)
.buttonStyle(PlainButtonStyle())
Button(action: saveButtonPressed, label: {
Image(systemName: "plus")
.font(.title2)
.foregroundColor(.white)
})
}
VStack {
self.content()
}
ForEach(listViewModel.items) { item in
ListRowView (item: item)
}
.lineLimit(1)
.fixedSize(horizontal: true, vertical: true)
.frame(minHeight: 0, maxHeight: collapsed ? 0 : .none)
.animation(.easeInOut(duration: 1.0), value: show)
.clipped()
.transition(.slide)
}
}
func saveButtonPressed() {
listViewModel.addItem(title: "Hello")
}
}
struct ListRowView: View {
let item: ItemModel
var body: some View {
HStack() {
Text(item.title)
.font(.body)
.fontWeight(.bold)
.foregroundColor(.white)
.multilineTextAlignment(.center)
.lineLimit(1)
.frame(width: 232, height: 16)
}
.padding( )
.frame(width: 396, height: 56)
.background(.gray)
.cornerRadius(12.0)
}
}
class ListViewModel {
var items: [ItemModel] = []
init() {
getItems()
}
func getItems() {
let newItems = [
ItemModel(title: "List Item1"),
ItemModel(title: "List Item2"),
ItemModel(title: "List Item3"),
]
items.append(contentsOf: newItems)
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
class CollapsedViewModel: ObservableObject {
#Published var collapsableItems: [CollapsableItem] = []
#Published var id = UUID().uuidString
init() {
getCollapsableItems()
}
func getCollapsableItems() {
let newCollapsableItems = [
CollapsableItem(title: "Wake up")
]
collapsableItems.append(contentsOf: newCollapsableItems)
}
func addCollapsableItem(title: String) {
let newCollapsableItem = CollapsableItem(title: title)
collapsableItems.append(newCollapsableItem)
}
func updateCollapsableItem(collapsableItem: CollapsableItem) {
if let index = collapsableItems.firstIndex(where: { $0.id ==
collapsableItem.id}) {
collapsableItems[index] =
collapsableItem.updateCompletion()
}
}
}
struct CollapsableItem: Equatable, Identifiable, Hashable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> CollapsableItem {
return CollapsableItem(id: id, title: title)
}
}
struct ItemModel: Identifiable, Equatable {
let id: String
let title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel {
return ItemModel(id: id, title: title)
}
}

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

Cannot convert value of type 'String' to specified type 'NWEndpoint.Host'

SwiftUI code to send commands trough UDP
What I want
I need to set this:
var hostUDP: NWEndpoint.Host = "192.168.0.205" //Line I want to fill with dispositive.ip
var portUDP: NWEndpoint.Port = 3489 //Line I want to fill with dispositive.port
to the values who come in the Dispositive structure. I know I can't do this outside the View, but I can't do it inside also.
The error
Cannot convert value of type 'String' to specified type 'NWEndpoint.Host'
At line var IP: NWEndpoint.Host = dispositive.ip
Code
This is my code:
import SwiftUI
import Foundation
import Network
var connection: NWConnection?
var hostUDP: NWEndpoint.Host = "192.168.0.205"
var portUDP: NWEndpoint.Port = 3489
struct CommunicationsView: View {
var dispositive: Dispositive
#State var confirm = false
#State var command: String = ""
var body: some View {
VStack {
HStack{
VStack{
HStack{
Button(action: {
var IP: NWEndpoint.Host = dispositive.ip
self.connectToUDP(hostUDP, portUDP, message:"<ARRIBA1>")
}) {
Text("Y+")
}
}
.padding()
.background(
Capsule()
.stroke(Color.blue, lineWidth: 1.5)
)
HStack{
Button(action: {
self.connectToUDP(hostUDP, portUDP, message:"<IZQUIERDA1>")
}) {
Text("X-")
}
.padding()
.background(
Capsule()
.stroke(Color.blue, lineWidth: 1.5)
)
Button(action: {
self.connectToUDP(hostUDP, portUDP, message:"<PARAR>")
}) {
Text("STOP")
.font(.subheadline)
.bold()
}
.padding()
.background(
Capsule()
.stroke(Color.blue, lineWidth: 1.5)
)
Button(action: {
self.connectToUDP(hostUDP, portUDP, message:"<DERECHA1>")
}) {
Text("X+")
}
.padding()
.background(
Capsule()
.stroke(Color.blue, lineWidth: 1.5)
)
}
HStack{
Button(action: {
self.connectToUDP(hostUDP, portUDP, message:"<ABAJO1>")
}) {
Text("Y-")
}
}
.padding()
.background(
Capsule()
.stroke(Color.blue, lineWidth: 1.5)
)
}
.padding()
Divider()
.padding()
.frame(height: 200)
VStack{
HStack{
Button(action: {
self.connectToUDP(hostUDP, portUDP, message:"<SUBIR>")
}) {
Text("Z+")
}
.padding()
.background(
Capsule()
.stroke(Color.blue, lineWidth: 1.5)
)
}
HStack{
Button(action: {
self.connectToUDP(hostUDP, portUDP, message:"<BAJAR>")
}) {
Text("Z-")
}
.padding()
.background(
Capsule()
.stroke(Color.blue, lineWidth: 1.5)
)
}
}
.padding()
}
VStack(alignment: .leading) {
Text("Terminal input")
.font(.callout)
.bold()
HStack{
TextField("Enter new command to send...", text: $command)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.confirm = true
}) {
Text("Enviar")
}
.actionSheet(isPresented: $confirm){
ActionSheet(
title: Text("Send custom message"),
message: Text("Are you Sure?"),
buttons: [
.cancel(Text("Cancel")),
.destructive(Text("Yes"), action: {
print("Sound the Alarm")
self.connectToUDP(hostUDP, portUDP, message:command)
})
]
)
}
}
}.padding()
}
}
func connectToUDP(_ hostUDP: NWEndpoint.Host, _ portUDP: NWEndpoint.Port, message: String) {
// Transmited message:
let messageToUDP = message
connection = NWConnection(host: hostUDP, port: portUDP, using: .udp)
connection?.stateUpdateHandler = { (newState) in
print("This is stateUpdateHandler:")
switch (newState) {
case .ready:
print("State: Ready\n")
self.sendUDP(messageToUDP)
self.receiveUDP()
case .setup:
print("State: Setup\n")
case .cancelled:
print("State: Cancelled\n")
case .preparing:
print("State: Preparing\n")
default:
print("ERROR! State not defined!\n")
}
}
connection?.start(queue: .global())
}
func sendUDP(_ content: String) {
let contentToSendUDP = content.data(using: String.Encoding.utf8)
connection?.send(content: contentToSendUDP, completion: NWConnection.SendCompletion.contentProcessed(({ (NWError) in
if (NWError == nil) {
print("Data was sent to UDP")
} else {
print("ERROR! Error when data (Type: Data) sending. NWError: \n \(NWError!)")
}
})))
}
func receiveUDP() {
connection?.receiveMessage { (data, context, isComplete, error) in
if (isComplete) {
print("Receive is complete")
if (data != nil) {
let backToString = String(decoding: data!, as: UTF8.self)
print("Received message: \(backToString)")
} else {
print("Data == nil")
}
}
}
}
}
struct CommunicationsView_Previews: PreviewProvider {
static var previews: some View {
CommunicationsView(dispositive: Dispositive(id: 1, name: "Nombre", description: "Descripción", color: .blue, banner: Image(""), ip: "192.168.0.84", port: 8888, control: 1, avatar: Image("user"), favorite: true)).previewLayout(.fixed(width: 400, height: 320))
}
}
Dispositive
Dispositive struct:
struct Dispositive {
var id: Int
var name: String
var description: String
var color: Color
var banner: Image
var ip: String
var port: Int
var control: Int
var avatar: Image
var favorite: Bool
}
Try below code to initialise Host and a Port.
Host-:
let hostUDP: NWEndpoint.Host = .init(dispositive.ip)
This is the recommended way, and you can check the same by doing command + click on NWEndpoint enum, and look at Host initialiser.
Port-:
let portUDP: NWEndpoint.Port = .init(integerLiteral: UInt16(dispositive.port))

SwiftUI automatically go to next view on success

I have a login view that does an http request . Once we get the http response we know whether the user can go to the next view . I am wondering how can I trigger the next view without click ? I am not looking to do a sheet since I want to get the full screen mode . I have been looking at this Go to a new view using SwiftUI but no luck . From my code below when I click on Press on me navigationLink I can go to the correct view, however I need that same functionality to work without clicking in the http response below where it says decode.status == 1 because the user has authenticated successfully .
struct ContentView: View {
#State var email: String = ""
#State var password: String = ""
#State var message: String = ""
#State var status: Int = -10
#State var showActive = true
var body: some View {
NavigationView {
ZStack {
Color(UIColor.systemGroupedBackground)
.edgesIgnoringSafeArea(.all)
VStack(alignment: .center) {
Spacer()
NavigationLink(destination: MainView()) {
Text("Press on me")
}.buttonStyle(PlainButtonStyle()) // This works when clicked
TextField("Email", text: $email)
.padding(10)
.textFieldStyle(RoundedBorderTextFieldStyle())
.background(Color(UIColor(hexString: ForestGreen)))
.foregroundColor(Color.black)
SecureField("Password", text: $password)
.padding(10)
.textFieldStyle(RoundedBorderTextFieldStyle())
.background(Color(UIColor(hexString: ForestGreen)))
.foregroundColor(Color.black)
Button(action: {
guard let url = URL(string:ConnectionString+"profile/login") else { return }
let parameter = "email=\(self.email)&password=\(self.password)"
let request = RequestObject(AddToken: true, Url: url, Parameter: parameter)
URLSession.shared.dataTask(with:request, completionHandler: {(data, response, error) in
if let decode = try? JSONDecoder().decode(ProfileCodable.self, from: data!)
{
self.status = decode.status ?? -10
self.message = decode.message ?? ""
if decode.status == 0 {
print("Invalid Credentials")
} else if decode.status == 1 {
// ** Go to next View here **
} else if decode.status == -1 {
print("Error")
}
} else {
print("No Response")
}
}).resume()
}) {
Text("Login")
.padding(10)
.frame(minWidth: 0, maxWidth: .infinity)
.font(.system(size: 22))
.foregroundColor(Color(UIColor(hexString: "#006622")))
.overlay(
RoundedRectangle(cornerRadius: 40)
.stroke(Color.black, lineWidth: 1))
}.padding([.top],40)
if self.status == 0 {
Text(self.message)
.foregroundColor(.red)
.font(.system(size: 20))
.padding([.top],30)
}
Spacer()
}.padding()
}
}
}
}
Try this (scratchy - not tested because not compilable due to absent dependencies), so adapt at your side:
struct ContentView: View {
#State var email: String = ""
#State var password: String = ""
#State var message: String = ""
#State var status: Int = -10
#State var showActive = false // << seems this state, so false
var body: some View {
NavigationView {
ZStack {
Color(UIColor.systemGroupedBackground)
.edgesIgnoringSafeArea(.all)
VStack(alignment: .center) {
Spacer()
TextField("Email", text: $email)
.padding(10)
.textFieldStyle(RoundedBorderTextFieldStyle())
.background(Color(UIColor(hexString: ForestGreen)))
.foregroundColor(Color.black)
SecureField("Password", text: $password)
.padding(10)
.textFieldStyle(RoundedBorderTextFieldStyle())
.background(Color(UIColor(hexString: ForestGreen)))
.foregroundColor(Color.black)
Button(action: {
guard let url = URL(string:ConnectionString+"profile/login") else { return }
let parameter = "email=\(self.email)&password=\(self.password)"
let request = RequestObject(AddToken: true, Url: url, Parameter: parameter)
URLSession.shared.dataTask(with:request, completionHandler: {(data, response, error) in
if let decode = try? JSONDecoder().decode(ProfileCodable.self, from: data!)
{
self.status = decode.status ?? -10
self.message = decode.message ?? ""
if decode.status == 0 {
print("Invalid Credentials")
} else if decode.status == 1 {
self.showActive = true // << here !!
} else if decode.status == -1 {
print("Error")
}
} else {
print("No Response")
}
}).resume()
}) {
Text("Login")
.padding(10)
.frame(minWidth: 0, maxWidth: .infinity)
.font(.system(size: 22))
.foregroundColor(Color(UIColor(hexString: "#006622")))
.overlay(
RoundedRectangle(cornerRadius: 40)
.stroke(Color.black, lineWidth: 1))
}.padding([.top],40)
.background(
// activated by state programmatically !!
NavigationLink(destination: MainView(), isActive: $self.showActive) {
EmptyView() // << just hide
}.buttonStyle(PlainButtonStyle())
)
if self.status == 0 {
Text(self.message)
.foregroundColor(.red)
.font(.system(size: 20))
.padding([.top],30)
}
Spacer()
}.padding()
}
}
}
}
Simply use the isActive property of navigation link. It would look like this:
NavigationLink(destination: MainView(), isActive: $mainViewActive) {
Text("Press on me")
}.buttonStyle(PlainButtonStyle())
and you should also declare the variable in your view:
#State var mainViewActive = false
then on successful login simply change the value to true. If you also do not want to display an actual link use EmptyView() as wrapper. So it would look like this:
NavigationLink(destination: MainView(), isActive: $mainViewActive) {
EmptyView()
}

Resources