Issues with assigning URL to an optional URL State variable - ios

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

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

SwiftUI having trouble switching #State and #Binding variables based of if statement

I'm trying to allow the FileManager to check if the matching Image Selected is saved. When saved, it needs to update the views on both the MainScreenView and BadgeScreenView. I am getting a error that "Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update." and "Result of 'BadgeScreenView' initializer is unused" after the image is selected and checked to see if it is saved at the right locations.
var ContentViewBadge = UIImage(systemName: "questionmark")!
var fileURL: URL?
func saveImage() {
do {
let furl = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("Compliance")
.appendingPathExtension("png")
fileURL = furl
try ContentViewBadge.pngData()?.write(to: furl)
print("Image \(ContentViewBadge) is saved to \(furl)")
} catch {
print("could not create imageFile")
}
let finding = fileURL
let fileExists = FileManager().fileExists(atPath: finding!.path)
if fileExists {
#State var IsTrue: Bool = true
BadgeScreenView(TrueBadge: $IsTrue)
//Change State variable "TrueBadge" here
print("Found something!")
}
}
import SwiftUI
import Foundation
var IsDone = false
struct BadgeScreenView: View {
#Binding var TrueBadge: Bool //Need help switching this Binding to true
#State private var ComplianceBadgeIsPicking = UIImage(named: "BlankComplianceBadge")!
#State private var isShwoingPhotoPicker = false
#State private var ShowInstruction = false
#State private var AlertToReplaceBade = false
var body: some View {
//The beginning
if TrueBadge {
Color("MainBadgeScreen")
.edgesIgnoringSafeArea(.all)
.overlay(
VStack{
Text("Clearance Status")
.font(.title)
.fontWeight(.semibold)
.offset(y: -15)
.foregroundColor(.white)
Text("Vaccine Compliant")
.foregroundColor(.white)
.bold()
.font(.system(size: 30))
Image(uiImage: ContentViewBadge)
.resizable()
.aspectRatio(contentMode: .fit)
.scaledToFit()
Button(action: {
AlertToReplaceBade.toggle()
}) {
Image(systemName: "trash" )
Text("Remove")
}
.foregroundColor(.white)
.padding()
.background(Color.red)
.cornerRadius(15)
.offset(y: 13)
}.alert(isPresented: $AlertToReplaceBade, content: {
Alert(title: Text("Are you sure you would like to remove your current badge?"),
message: Text("Remeber that this badge is and will be permanently removed"),
primaryButton: .default(Text("Yes"), action: {
// Somehow need to remove the image and activate the UIImagePickerController
isShwoingPhotoPicker.toggle()
}), secondaryButton: .cancel(Text("No, I do not")))
}).sheet(isPresented: $isShwoingPhotoPicker, content: {
PhotoPicker(Badge: $ComplianceBadgeIsPicking)
})
)}
else {
Color("ExpiredBadgeScreen")
.edgesIgnoringSafeArea(.all)
.overlay(
VStack{
Image(systemName: "person.crop.circle.badge.questionmark.fill")
.font(.system(size:150))
.offset(y: -10)
.foregroundColor(.black)
Text("Compliance Badge")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.black)
.offset(y: -2)
Text("You do not have a current vaccine compliant badge. Please upload one that shows you are vaccine compliant or within 'green' status")
.font(.system(size: 15))
.foregroundColor(.black)
.fontWeight(.bold)
.multilineTextAlignment(.center)
.frame(width: 270, height: 140, alignment: .center)
.offset(y: -26)
Button(action: {
ShowInstruction.toggle()
}) {
Image(systemName: "questionmark.circle")
Text("How to upload")
.bold()
.font(.system(size:20))
}
.offset(y: -40)
Button(action: {
isShwoingPhotoPicker.toggle()
}) {
Image(systemName: "square.and.arrow.up")
Text("Upload Badge")
.bold()
.font(.system(size:20))
}
.offset(y: -10)
}.sheet(isPresented: $ShowInstruction, content: {
Instruction()
})
.sheet(isPresented: $isShwoingPhotoPicker, content: {
PhotoPicker(Badge: $ComplianceBadgeIsPicking)
})
.accentColor(.black)
)
}
//The End
}
}
There's not enough for a minimal reproducible example in your code, so I had to make some guesses here, but this is the gist of what I'd expect things to look like. Note that saveImage is inside the View and thus has access to change the state.
It's not clear to me, though, where you call saveImage (you don't do it anywhere in your included code), which could effect things further.
let contentViewBadge = UIImage(systemName: "questionmark")!
struct BadgeScreenView: View {
#Binding var trueBadge: Bool
#State private var complianceBadgeIsPicking = UIImage(named: "BlankComplianceBadge")!
#State private var isShowingPhotoPicker = false
#State private var showInstruction = false
#State private var alertToReplaceBade = false
func saveImage() {
do {
let furl = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("Compliance")
.appendingPathExtension("png")
try contentViewBadge.pngData()?.write(to: furl)
print("Image \(contentViewBadge) is saved to \(furl)")
let fileExists = FileManager().fileExists(atPath: furl.path)
if fileExists {
trueBadge = true
print("Found something!")
}
} catch {
print("could not create imageFile")
}
}
var body: some View {
//The beginning
if trueBadge {
Color("MainBadgeScreen")
.edgesIgnoringSafeArea(.all)
.overlay(
VStack{
Text("Clearance Status")
.font(.title)
.fontWeight(.semibold)
.offset(y: -15)
.foregroundColor(.white)
Text("Vaccine Compliant")
.foregroundColor(.white)
.bold()
.font(.system(size: 30))
Image(uiImage: contentViewBadge)
.resizable()
.aspectRatio(contentMode: .fit)
.scaledToFit()
Button(action: {
alertToReplaceBade.toggle()
}) {
Image(systemName: "trash" )
Text("Remove")
}
.foregroundColor(.white)
.padding()
.background(Color.red)
.cornerRadius(15)
.offset(y: 13)
}.alert(isPresented: $alertToReplaceBade, content: {
Alert(title: Text("Are you sure you would like to remove your current badge?"),
message: Text("Remeber that this badge is and will be permanently removed"),
primaryButton: .default(Text("Yes"), action: {
// Somehow need to remove the image and activate the UIImagePickerController
isShowingPhotoPicker.toggle()
}), secondaryButton: .cancel(Text("No, I do not")))
}).sheet(isPresented: $isShowingPhotoPicker, content: {
PhotoPicker(Badge: $complianceBadgeIsPicking)
})
)}
else {
Color("ExpiredBadgeScreen")
.edgesIgnoringSafeArea(.all)
.overlay(
VStack{
Image(systemName: "person.crop.circle.badge.questionmark.fill")
.font(.system(size:150))
.offset(y: -10)
.foregroundColor(.black)
Text("Compliance Badge")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.black)
.offset(y: -2)
Text("You do not have a current vaccine compliant badge. Please upload one that shows you are vaccine compliant or within 'green' status")
.font(.system(size: 15))
.foregroundColor(.black)
.fontWeight(.bold)
.multilineTextAlignment(.center)
.frame(width: 270, height: 140, alignment: .center)
.offset(y: -26)
Button(action: {
showInstruction.toggle()
}) {
Image(systemName: "questionmark.circle")
Text("How to upload")
.bold()
.font(.system(size:20))
}
.offset(y: -40)
Button(action: {
isShwoingPhotoPicker.toggle()
}) {
Image(systemName: "square.and.arrow.up")
Text("Upload Badge")
.bold()
.font(.system(size:20))
}
.offset(y: -10)
}.sheet(isPresented: $showInstruction, content: {
Instruction()
})
.sheet(isPresented: $isShowingPhotoPicker, content: {
PhotoPicker(Badge: $complianceBadgeIsPicking)
})
.accentColor(.black)
)
}
//The End
}
}
An alternative (or addition) to putting savingImage inside the view would be to explicitly pass a binding to it. The function would look like this:
func saveImage(binding: Binding<Bool>) {
do {
let furl = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("Compliance")
.appendingPathExtension("png")
try contentViewBadge.pngData()?.write(to: furl)
print("Image \(contentViewBadge) is saved to \(furl)")
let fileExists = FileManager().fileExists(atPath: furl.path)
if fileExists {
binding.wrappedValue = true
print("Found something!")
}
} catch {
print("could not create imageFile")
}
}
And you would call it like this:
saveImage(binding: $trueBadge)
Note: I changed your variable names to fit the Swift conventions of using lowercase letters to start variable/property names

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

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

Have 0th item in SwiftUI auto selected

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.

Resources