Can someone please help me why the NavigationLink is not working as intended?
As shown down below (in the code) I use the MarkdownWebView(url: <url>) with 3 different URL’s.
But when I want to switch between them, the view doesn’t update.
If I open another view in between it’s working.
And on the iPhone (NavigationStack) it also works.
The Problem
My Code:
Section("Legal") {
NavigationLink {
MarkdownWebView(url: "https://<url>/privacy.md", scrollbar: false)
.navigationTitle("Privacy Policy")
} label: {
Text("")
.font(.custom(CustomFonts.FADuotone, size: 20))
.frame(width: 30)
.foregroundColor(.gray)
Text(String(localized: "Privacy Policy", comment: "/"))
}
NavigationLink {
MarkdownWebView(url: "https://<url>/tos.md", scrollbar: false)
.navigationTitle("Terms of use")
} label: {
Text("")
.font(.custom(CustomFonts.FADuotone, size: 20))
.frame(width: 30)
.foregroundColor(.gray)
Text(String(localized: "Terms of Service", comment: "/"))
}
NavigationLink {
MarkdownWebView(url: "https://<url>/licenses.md", scrollbar: false)
.navigationTitle("Licenses")
} label: {
Text("")
.font(.custom(CustomFonts.FADuotone, size: 20))
.frame(width: 30)
.foregroundColor(.gray)
Text(String(localized: "Licenses", comment: "/"))
}
}
NavigagationSplitView
This is what the NavigationSplitView looks like:
var body: some View {
NavigationSplitView(columnVisibility: $navigationVM.selectedColumnVisibility) {
column1Form
.navigationTitle(String(localized: "Dashboard", comment: "/"))
.navigationBarTitleDisplayMode(.large)
} content: {
secondForm
}detail: {
detailForm
}
.navigationSplitViewStyle(.balanced)
}
#ViewBuilder
var secondForm: some View {
switch navigationVM.selectedCategory {
case .findWineries: findWineries()
case .profile: ProfileView()
case .stats: StatisticsView()
case .favWines: FavWineView()
case .favWineries: FavWineriesView()
case .cellar: CellarView()
case .orders: OrderListView()
-> case .settings: SettingsView()
case .none: Text("")
}
}
#ViewBuilder
var detailForm: some View {
switch navigationVM.selectedDetail {
case .map: MapView()
case .order: Text("orderTest")
case .orderDetail: OrderDetailView(Status: .delivered)
case .none: Text("")
}
}
On the second column of the SplitView I navigate to the SettingsView() (marked in the code with an arrow).
From there (SettingsView) I want to push the third row with the NavigationLink.
This works fine if I push separate Views. But it doesn’t work with the same View and different parameters (as shown in the post above).
MarkdownWebView()
import SwiftUI
import MarkdownUI
struct MarkdownWebView: View {
#State var url: String
#State var scrollbar: Bool
#State var error: Bool = false
#State private var fileContent: String? = nil
var body: some View {
VStack {
if let content = fileContent {
ScrollView(showsIndicators: scrollbar) {
Markdown(content)
}
} else {
if (error) {
VStack(spacing: 20) {
Text("")
.font(.custom(CustomFonts.FADuotone, size: 100, relativeTo: .body))
.foregroundColor(.red)
Text("Document not found")
.font(.title)
}
} else {
VStack(spacing: 20) {
ProgressView()
Text("loading")
}
}
}
}
.onAppear {
loadMarkdownFile(url: url)
}
.padding()
}
private func loadMarkdownFile(url: String) {
DispatchQueue.global().async {
guard let fileUrl = URL(string: url) else {
print("File not found")
self.error = true
return
}
do {
let content = try String(contentsOf: fileUrl)
DispatchQueue.main.async {
self.fileContent = content
}
} catch {
self.error = true
print("Error reading file: \(error)")
}
}
}
}
In the way you use NavigationLink the .onAppear in MarkdownWebView is only called once for the first view. So the content doesn't refresh on other selections, because the view is already visible and .onAppear isn't called again.
I can suggest two options:
1. quick and dirty
Give each call of MarkdownWebView a different .id which forces a redraw:
struct SettingsView: View {
var body: some View {
List {
Section("Legal") {
NavigationLink {
MarkdownWebView(url: "https://www.lipsum.com", scrollbar: false)
.navigationTitle("Privacy Policy")
.id(1) // here
} label: {
Text("")
.font(.system(size: 20))
.frame(width: 30)
.foregroundColor(.gray)
Text(String(localized: "Privacy Policy", comment: "/"))
}
NavigationLink {
MarkdownWebView(url: "https://www.apple.com", scrollbar: false)
.navigationTitle("Terms of use")
.id(2) // here
} label: {
Text("")
.font(.system(size: 20))
.frame(width: 30)
.foregroundColor(.gray)
Text(String(localized: "Terms of Service", comment: "/"))
}
NavigationLink {
MarkdownWebView(url: "https://www.google.com", scrollbar: false)
.navigationTitle("Licenses")
.id(3) // here
} label: {
Text("")
.font(.system(size: 20))
.frame(width: 30)
.foregroundColor(.gray)
Text(String(localized: "Licenses", comment: "/"))
}
}
}
}
}
2. new SwiftUI navigation logic
Use the new init of NavigationLink with value and provide a .navigationDestination.
I used Int values here (1,2,3) but you can (and should) also use enum values.
struct SettingsView2: View {
var body: some View {
List {
Section("Legal") {
NavigationLink(value: 1) { // value 1
Text("")
.font(.system(size: 20))
.frame(width: 30)
.foregroundColor(.gray)
Text(String(localized: "Privacy Policy", comment: "/"))
}
NavigationLink(value: 2) { // value 2
Text("")
.font(.system(size: 20))
.frame(width: 30)
.foregroundColor(.gray)
Text(String(localized: "Terms of Service", comment: "/"))
}
NavigationLink(value: 3) { // value 3
Text("")
.font(.system(size: 20))
.frame(width: 30)
.foregroundColor(.gray)
Text(String(localized: "Licenses", comment: "/"))
}
}
}
.navigationDestination(for: Int.self) { value in // define destinations based on value
switch value {
case 1:
MarkdownWebView(url: "https://www.lipsum.com", scrollbar: false)
.navigationTitle("Privacy Policy")
case 2:
MarkdownWebView(url: "https://www.apple.com", scrollbar: false)
.navigationTitle("Terms of use")
case 3:
MarkdownWebView(url: "https://www.google.com", scrollbar: false)
.navigationTitle("Licenses")
default: Text("nothing")
}
}
}
}
Related
I am new to swiftUI. I have one application in which i am making api call, in this application i want to display error if API does not return the response. When i navigate first time to the screen, it displays alert message, but when i go back to previous screen and again navigate back to same screen. alert is not displayed.
struct DashboardView: View {
#ObservedObject var viewModel = DashboardViewModel()
#State private var showingAlert = true
init() {
UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.black]
self.showingAlert = true
}
var body: some View {
ZStack {
Color.white
if case .LOADING = viewModel.currentState {
loaderView()
.onAppear(perform: viewModel.getDashboardData)
} else if case .SUCCESS(let dashboard) = viewModel.currentState {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("Date:")
.foregroundColor(Color.black)
Text(dashboard.date)
.foregroundColor(Color.black)
}
VStack(alignment: .leading) {
Text("HDURL:")
.foregroundColor(Color.black)
Text(dashboard.hdurl)
.foregroundColor(Color.black)
}
HStack {
Text("Media Type:")
.foregroundColor(Color.black)
Text(dashboard.mediaType)
.foregroundColor(Color.black)
}
HStack {
Text("Service Version:")
.foregroundColor(Color.black)
Text(dashboard.serviceVersion)
.foregroundColor(Color.black)
}
VStack(alignment: .leading) {
Text("Title:")
.foregroundColor(Color.black)
Text(dashboard.title)
.foregroundColor(Color.black)
}
VStack(alignment: .leading) {
Text("url:")
.foregroundColor(Color.black)
Text(dashboard.url)
.foregroundColor(Color.black)
}
VStack(alignment: .leading) {
Text("Explanation:")
.foregroundColor(Color.black)
Text(dashboard.explanation)
.foregroundColor(Color.black)
}
}
.padding(.horizontal, 10)
.padding(.top, UIApplication.shared.keyWindow!.safeAreaInsets.top )
}
.padding(.top, UIApplication.shared.keyWindow!.safeAreaInsets.top )
} else if case .FAILURE(let error) = viewModel.currentState {
VStack {
Text("No Data")
.foregroundColor(Color.black)
}
}
}
.navigationBarTitle("Dashboard", displayMode: .inline)
.alert(item: $viewModel.appError) { appAlert in
Alert(title: Text("Error"),
message: Text("""
\(appAlert.errorString)
Please try again later!
"""
)
)
}
.ignoresSafeArea(.all)
}
}
enum ViewStateDashboard {
case START
case LOADING
case SUCCESS(dashboardModel: Dashboard)
case FAILURE(error: String)
}
protocol FetchDashboardDataFromServer {
func getDashboardData()
var currentState: ViewStateDashboard {
get set
}
}
class DashboardViewModel: ObservableObject, FetchDashboardDataFromServer {
struct AppError: Identifiable {
let id = UUID().uuidString
let errorString: String
}
#Published var appError: AppError?
let monitor = NWPathMonitor()
let monitorPostUser = NWPathMonitor()
let queue = DispatchQueue(label: "InternetConnectionMonitor")
var cancelable: Set<AnyCancellable> = []
#Published var currentState: ViewStateDashboard = .START
init() {
self.currentState = .LOADING
}
// GET Method
func getDashboardData() {
print("fetch dashboard data")
self.currentState = .LOADING
monitor.pathUpdateHandler = { pathUpdateHandler in
if pathUpdateHandler.status == .satisfied {
APIClient.dispatch(
APIRouter.GetDashboardData(queryParams:
APIParameters.GetDasbhboardParams(apikey: "API_KEY")))
.sink { completion in
switch completion {
case .finished:
print("Execution Finihsed dashboard.")
case .failure(let error):
DispatchQueue.main.async {
print("dashboard error", error)
self.appError = AppError(errorString: error.localizedDescription)
self.currentState = .FAILURE(error: error.localizedDescription)
}
}
}
receiveValue: { dashboardData in
print("received dashboard data", dashboardData)
self.currentState = .SUCCESS(dashboardModel: dashboardData)
}.store(in: &self.cancelable)
} else {
DispatchQueue.main.async {
self.currentState = .FAILURE(error: StringConstants.NoInterNet)
self.appError = AppError(errorString: StringConstants.NoInterNet)
print("no internet get users")
}
}
}
monitor.start(queue: queue)
}
}
What could be the issue here? Any help would be appreciated.
When you close the alert, viewModel.appError is set to nil. If you want it to appear again, you must set it to a value again.
Good Evening,
I have a question, I need to show an error when a user enters a wrong email or enters a wrong password. What do I need to add in my code?
I am new to software development and Sam not so familiar with swift ui could someone help me?
#State var email = ""
#State var password = ""
var body: some View {
Image("ISD-Logo")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 185, height: 140)
.clipped()
.padding(.bottom, 75)
VStack {
TextField("Email", text: $email)
.padding()
.background(Color(UIColor.lightGray))
.cornerRadius(5.0)
.padding(.bottom, 20)
SecureField("password", text: $password)
.padding()
.background(Color(UIColor.lightGray))
.cornerRadius(5.0)
.padding(.bottom, 20)
Button(action: { login() }) {
Text("Sign in")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.black)
.cornerRadius(35.0)
}
}
.padding()
}
// Login and error message
func login() {
Auth.auth().signIn(withEmail: email, password: password) { (result, error) in
if error != nil {
print(error?.localizedDescription ?? "")
} else {
print("success")
}
}
}
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}
What kind of alert do you want?
You can do that:
struct ContentView: View {
#State private var showingAlert = false
var body: some View {
Button("Show Alert") {
showingAlert = true
}
.alert("Important message", isPresented: $showingAlert) {
Button("OK", role: .cancel) { }
}
}
}
You can try like this. I would suggest you go to SWIFT UI basics first
#State var email = ""
#State var password = ""
var body: some View {
Image("ISD-Logo")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 185, height: 140)
.clipped()
.padding(.bottom, 75)
VStack {
TextField("Email", text: $email)
.padding()
.background(Color(UIColor.lightGray))
.cornerRadius(5.0)
.padding(.bottom, 20)
SecureField("password", text: $password)
.padding()
.background(Color(UIColor.lightGray))
.cornerRadius(5.0)
.padding(.bottom, 20)
Button(action: { login() }) {
Text("Sign in")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.black)
.cornerRadius(35.0)
}
}
.padding()
}
// Login and error message
func login() {
Auth.auth().signIn(withEmail: email, password: password) { (result, error) in
if error != nil {
self.isAlert = true
print(error?.localizedDescription ?? "")
} else {
print("success")
}
}
}
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}
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))
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()
}
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)
}
}