I'm currently building a simple app with a SignIn/SignUp View in order to better understand how to integrate SwiftUI with Firebase.
I've run into a bit of an issue when I try to update User Data. I noticed that after Signing Out as one User and then Signing In as a different User, there is a split second delay before the User information is updated on the FirstView after the Sign In button is tapped. I'm trying to figure out how to avoid this delay from happening.
As a side, I'm also having trouble figuring out how to smoothly navigate from my LoginView() to my FirstView(). I have my FirstView() inside of a Navigation Stack, but the transition between the views is very abrupt and devoid of the NavigationLink animation. How can I correct this issue?
Much appreciated!
Here is the relevant code...
#main
struct AWSupportLoggerApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#StateObject var viewModel = AppViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
class AppDelegate:NSObject,UIApplicationDelegate{
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
}
struct ContentView: View {
#EnvironmentObject var viewModel: AppViewModel
var body: some View {
NavigationView {
if viewModel.signedIn {
FirstView()
} else {
//.onAppear method is used for keyboard management (See Misc Functions...)
SignInView()
.onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
.navigationBarHidden(true)
}
}
.onAppear {
viewModel.listen()
}
}
}
class AppViewModel: ObservableObject {
private var db = Firestore.firestore()
#Published var userInfo: User?
#Published var signedIn: Bool = false
var handle: AuthStateDidChangeListenerHandle?
let authRef = Auth.auth()
var authHandle : AuthStateDidChangeListenerHandle?
var rootInfoCollection : CollectionReference!
var userIdRef = ""
func fetchUserData(){
db.collection("Users").document("\(userIdRef)").getDocument { document, error in
// Check for error
if error == nil {
// Check that this document exists
if document != nil && document!.exists {
self.userInfo = document.map { (documentSnapshot) -> User in
let data = documentSnapshot.data()
let uid = data?["uid"] as? UUID ?? UUID()
let company = data?["company"] as? String ?? ""
let name = data?["name"] as? String ?? ""
let admin = data?["admin"] as? Bool ?? false
let photo = data?["photo"] as? String ?? ""
return User(uid: uid, company: company, name: name, admin: admin, photo: photo)
}
}
}
}
}
func listen(){
handle = authRef.addStateDidChangeListener({ auth, user in
print(user?.email ?? "No User Found")
if let user = auth.currentUser {
self.userIdRef = user.uid
self.rootInfoCollection = Firestore.firestore().collection("/Users/")
DispatchQueue.main.async {
self.fetchUserData()
}
self.signedIn = true
} else {
self.signedIn = false
}
})
}
func signIn(email: String, password: String){
authRef.signIn(withEmail: email, password: password) { result, error in
guard result != nil, error == nil else {
return
}
}
}
func signOut(){
do {
try authRef.signOut()
} catch {
print(error)
}
}
func signUp(email: String, password: String, company: String, name: String, admin: Bool, photo: String){
authRef.createUser(withEmail: email, password: password) { result, error in
guard result != nil, error == nil else {
return
}
let db = Firestore.firestore()
//Success
db.collection("Users").document("\(result!.user.uid)").setData(["company" : "\(company)", "name" : "\(name)", "admin" : admin, "photo" : "\(photo)", "uid":result!.user.uid]) { error in
if error != nil {
print(error!)
}
}
}
}
func unbind() {
if let handle = handle {
authRef.removeStateDidChangeListener(handle)
}
}
}
struct FirstView: View {
#EnvironmentObject private var appViewModel: AppViewModel
var body: some View {
VStack{
Spacer()
VStack(spacing: 50){
NavigationLink(destination: Text("Test")){
awButton(content: "Request Support", backColor: Color(#colorLiteral(red: 0, green: 0.723585546, blue: 0.9907287955, alpha: 1)))
.shadow(color: Color.primary.opacity(0.5), radius: 20, x: 0, y: 20)
.rotation3DEffect(Angle(degrees:10), axis: (x: 10.0, y: 0, z: 0))
}
NavigationLink(destination: Text("Test")){
awButton(content: "Request Quote", backColor: Color(#colorLiteral(red: 0.9372549057, green: 0.3490196168, blue: 0.1921568662, alpha: 1)))
.shadow(color: Color.primary.opacity(0.5), radius: 20, x: 0, y: 20)
.rotation3DEffect(Angle(degrees:10), axis: (x: 10.0, y: 0, z: 0))
}
NavigationLink(destination: Text("Test")){
awButton(content: "Ticket Status", backColor: Color(#colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1)))
.shadow(color: Color.primary.opacity(0.5), radius: 20, x: 0, y: 20)
.rotation3DEffect(Angle(degrees:10), axis: (x: 10.0, y: 0, z: 0))
}
}
Spacer()
}
.navigationBarBackButtonHidden(true)
.navigationTitle(appViewModel.userInfo?.company ?? "Test")
.navigationBarItems(leading: Button(action: {
appViewModel.signOut()
}) {
HStack {
Text("Sign Out")
}
},trailing: HStack{
Image(systemName: "bell")
.font(.system(size: 30))
// selectedImageArray.first!
// .resizable()
// .scaledToFit()
// .clipShape(Circle())
// .frame(width: 50, height: 50)
Text(appViewModel.userInfo?.name ?? "Tester")
.font(.system(size: 20))
})
}
}
Your navigation state depends on signedIn. In your auth listener, you do this:
DispatchQueue.main.async {
self.fetchUserData()
}
self.signedIn = true
This will set signedIn to true, which will change your navigation state and then at an indeterminate time in the future, fetchUserData will finish and update the user data (once the network call has completed).
To avoid this delay (or, really, to just avoid seeing the information not update on the screen -- the delay is an inevitability of the network call), don't set signedIn until fetchUserData completes. So, remove the line inside the listener and instead, set it after your self.userInfo = line.
Related
Despite the simplicity of the question I have not been able to find a satisfactory answer yet. I want to update toggle switches based on value in Firebase. I have added listeners but run into problems converting a Bool to Binding Bool, any help is appreciated.
struct oneSeqeuncer : Identifiable{
var id: String
var status: Bool
}
struct Sequencers: View {
#ObservedObject var seqModel = SequencerModel()
#State private var novaseq404A :Bool = true
#State private var novaseq404B :Bool = true
#State private var novaseq297A :Bool = true
#State private var novaseq297B :Bool = true
var body: some View {
ZStack{
VStack{
Text("Sequencers")
.foregroundColor(.white)
.font(.title)
.fontWeight(.bold)
.padding()
List{
HStack{
Text("404")
.font(.title)
.padding()
Toggle("", isOn: $novaseq404A)
.onChange(of: novaseq404A) { newValue in
updateStatus(name: "404A", status: novaseq404A)
}
Toggle("", isOn: $novaseq404B)
.padding()
.onChange(of: novaseq404B) { newValue in
updateStatus(name: "404B", status: novaseq404B)
}
}
HStack{
Text("297")
.font(.title)
.padding()
Toggle("", isOn: $novaseq297A)
.onChange(of: novaseq297A) { newValue in
updateStatus(name: "297A", status: novaseq297A)
}
Toggle("", isOn: $novaseq297B)
.padding()
.onChange(of: novaseq297B) { newValue in
updateStatus(name: "297B", status: novaseq297B)
}
}
}
}
}.onAppear(){
self.seqModel.fetchData()
for seq in seqModel.seqs{
if seq.id == "404A"{
novaseq404A = seq.status
}
if seq.id == "404B"{
novaseq404A = seq.status
}
if seq.id == "297A"{
novaseq297A = seq.status
}
if seq.id == "297B"{
novaseq297B = seq.status
}
}
func updateStatus(name: String, status: Bool){
let timeInterval = NSDate().timeIntervalSince1970
let myInt = Int(timeInterval)
let db = Firestore.firestore()
if status == false{
db.collection("Sequencers").document(name).updateData([
"status": false,
"lastChange" : myInt
]){ error in
if error != nil{
print(error!)
}
}
}
else{
let docRef = db.collection("Sequencers").document(name)
docRef.getDocument {(document, error) in
if error != nil {
print(error!)
}
if let document = document, document.exists{
let data = document.data()
if let lastChange = data!["lastChange"]! as? Int{
let timeOff = myInt - lastChange
if let timeOffTotal = data!["timeOff"]! as? Int{
let newTimeOff = timeOffTotal + timeOff
db.collection("Sequencers").document(name).updateData([
"timeOff" : newTimeOff
])
}
}
db.collection("Sequencers").document(name).updateData([
"previousChange": data!["lastChange"]!,
"status": true ,
"lastChange" : myInt
])
}
}
}
}
}
struct Sequencers_Previews: PreviewProvider {
static var previews: some View {
Sequencers()
}
}
Below is my model for storing 'sequencers'
import Foundation
import FirebaseFirestore
import Firebase
class SequencerModel : ObservableObject {
#Published var seqs = [oneSeqeuncer]()
private var db = Firestore.firestore()
func fetchData(){
db.collection("Sequencers").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.seqs = documents.map { queryDocumentSnapshot -> oneSeqeuncer in
let data = queryDocumentSnapshot.data()
let id = queryDocumentSnapshot.documentID
let status = data["status"] as? Bool
print(id)
print(status as Any)
return oneSeqeuncer(id: id, status: status!)
}
}
}
}
My solution was not ideal but solved, I realized the function .fetchData() that I was calling to was taking too long to respond. Ideally I should use some completion handler... However I simply changed my TabView on ContentView to display another page first, to allow time for my call to Firebase to finish, which allowed my for loop in .onAppear to have a non empty iterable. Again not sure this really belongs as an "Answer" but just wanted to share my temp solution as an option.
I'm having trouble figuring out how to smoothly navigate from my SignInView() to my FirstView(). I have my FirstView() inside of a Navigation Stack, but the transition between the views is very abrupt and devoid of the transition that you normally get with the use of a NavigationLink. How can I get the transition to work?
Much appreciated!
Here is the relevant code...
struct ContentView: View {
#EnvironmentObject var viewModel: AppViewModel
var body: some View {
VStack{
NavigationView {
if viewModel.signedIn {
FirstView()
.transition(.slide)
} else {
//.onAppear method is used for keyboard management (See Misc Functions...)
SignInView()
.onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
.navigationBarHidden(true)
}
}
.onAppear {
viewModel.listen()
}
}
}
}
class AppViewModel: ObservableObject {
private var db = Firestore.firestore()
#Published var userInfo: User?
#Published var signedIn: Bool = false
var handle: AuthStateDidChangeListenerHandle?
let authRef = Auth.auth()
var authHandle : AuthStateDidChangeListenerHandle?
var rootInfoCollection : CollectionReference!
var userIdRef = ""
func fetchUserData(){
db.collection("Users").document("\(userIdRef)").getDocument { document, error in
// Check for error
if error == nil {
// Check that this document exists
if document != nil && document!.exists {
self.userInfo = document.map { (documentSnapshot) -> User in
let data = documentSnapshot.data()
let uid = data?["uid"] as? UUID ?? UUID()
let company = data?["company"] as? String ?? ""
let name = data?["name"] as? String ?? ""
let admin = data?["admin"] as? Bool ?? false
let photo = data?["photo"] as? String ?? ""
return User(uid: uid, company: company, name: name, admin: admin, photo: photo)
}
withAnimation {
self.signedIn = true
}
}
}
}
}
func listen(){
handle = authRef.addStateDidChangeListener({ auth, user in
print(user?.email ?? "No User Found")
if let user = auth.currentUser {
self.userIdRef = user.uid
self.rootInfoCollection = Firestore.firestore().collection("/Users/")
DispatchQueue.main.async {
self.fetchUserData()
}
} else {
self.signedIn = false
}
})
}
func signIn(email: String, password: String){
authRef.signIn(withEmail: email, password: password) { result, error in
guard result != nil, error == nil else {
return
}
}
}
}
struct SignInView: View {
#EnvironmentObject var viewModel: AppViewModel
#State private var username : String = ""
#State private var password : String = ""
#State private var shouldShowLoginAlert: Bool = false
#State var selectedImageArray : [Image] = []
var disableLoginButton : Bool {
return self.username.isEmpty || self.password.isEmpty
}
var body: some View {
VStack{
Image(uiImage: #imageLiteral(resourceName: "awText"))
.resizable()
.frame(width: 180, height: 100)
.padding(.bottom, 50)
TextField("Email", text: $username)
.padding(.leading)
.disableAutocorrection(true)
.autocapitalization(.none)
Rectangle().fill(Color.gray.opacity(0.25)).frame(height: 1, alignment: .center).padding(.bottom)
.padding(.bottom)
.onChange(of: self.username, perform: { value in
if value.count > 10 {
self.username = String(value.prefix(20)) //Max 10 Characters for Username.
}
})
SecureField("Password", text: $password)
.padding(.leading)
.disableAutocorrection(true)
.autocapitalization(.none)
Rectangle().fill(Color.gray.opacity(0.25)).frame(height: 1, alignment: .center)
.onChange(of: self.username, perform: { value in
if value.count > 10 {
self.username = String(value.prefix(10)) //Max 10 Characters for Password.
}
})
//SignIn Button
Button(action: {
viewModel.signIn(email: username, password: password)
}, label: {
Text("Sign In")
.disabled(disableLoginButton)
.frame(width: 300, height: 50)
.background(Color.green)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.padding()
})
}
Replacing the default NavigationView behavior with your own animations isn't necessarily totally straightforward. I'll lay out one possibility, but another would be to use a real NavigationView transition, but just hide the back button once you're on FirstView.
To do the transition yourself, you'll need one root element to NavigationView, an if clause, a transition(.slide) and withAnimation. Here's a simplified version of your code showing just these elements:
class AppViewModel: ObservableObject {
#Published var signedIn = false
}
struct FirstView : View {
var body: some View {
Text("Signed in")
}
}
struct ContentView: View {
#StateObject var viewModel = AppViewModel()
var body: some View {
NavigationView {
VStack {
if viewModel.signedIn {
FirstView()
.transition(.slide)
} else {
Button("Sign me in") {
withAnimation {
viewModel.signedIn = true
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationBarHidden(true)
}
}
}
I'm working on a very simple app to try to wrap my mind around SwiftUI and Firebase. I have a Login View, a SignUp View, and the FirstView that gets presented once the user is logged in.
Once a User is logged in and the SignOut button is tapped, the Navigation Bar that was only visible in the FirstView, all of a sudden appears superimposed on the LoginView as well. That's not supposed to happen and I'm trying to understand why that happens. Perhaps I'm not dismissing the FirstView correctly?
As a side question, I'm also running into an issue where my User Data is presented/Updated on the FirstView after Signing In, but with a noticeable delay between the Text change for the fields within the view.
Any help would be greatly appreciated.
Here is the relevant code...
#main
struct AWSupportLoggerApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#StateObject var viewModel = AppViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
class AppDelegate:NSObject,UIApplicationDelegate{
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
}
class AppViewModel: ObservableObject {
private var db = Firestore.firestore()
#Published var userInfo: User?
#Published var signedIn: Bool = false
var handle: AuthStateDidChangeListenerHandle?
let authRef = Auth.auth()
var authHandle : AuthStateDidChangeListenerHandle?
var rootInfoCollection : CollectionReference!
var userIdRef = ""
func fetchUserData(){
db.collection("Users").document("\(userIdRef)").getDocument { document, error in
// Check for error
if error == nil {
// Check that this document exists
if document != nil && document!.exists {
self.userInfo = document.map { (documentSnapshot) -> User in
let data = documentSnapshot.data()
let uid = data?["uid"] as? UUID ?? UUID()
let company = data?["company"] as? String ?? ""
let name = data?["name"] as? String ?? ""
let admin = data?["admin"] as? Bool ?? false
let photo = data?["photo"] as? String ?? ""
return User(uid: uid, company: company, name: name, admin: admin, photo: photo)
}
}
}
}
}
func listen(){
handle = authRef.addStateDidChangeListener({ auth, user in
print(user?.email ?? "No User Found")
if let user = auth.currentUser {
self.userIdRef = user.uid
self.rootInfoCollection = Firestore.firestore().collection("/Users/")
DispatchQueue.main.async {
self.fetchUserData()
}
self.signedIn = true
} else {
self.signedIn = false
}
})
}
func signIn(email: String, password: String){
authRef.signIn(withEmail: email, password: password) { result, error in
guard result != nil, error == nil else {
return
}
}
}
func signOut(){
do {
try authRef.signOut()
} catch {
print(error)
}
}
func signUp(email: String, password: String, company: String, name: String, admin: Bool, photo: String){
authRef.createUser(withEmail: email, password: password) { result, error in
guard result != nil, error == nil else {
return
}
let db = Firestore.firestore()
//Success
db.collection("Users").document("\(result!.user.uid)").setData(["company" : "\(company)", "name" : "\(name)", "admin" : admin, "photo" : "\(photo)", "uid":result!.user.uid]) { error in
if error != nil {
print(error!)
}
}
}
}
func unbind() {
if let handle = handle {
authRef.removeStateDidChangeListener(handle)
}
}
}
struct ContentView: View {
#EnvironmentObject var viewModel: AppViewModel
var body: some View {
NavigationView {
if viewModel.signedIn {
FirstView()
} else {
//.onAppear method is used for keyboard management (See Misc Functions...)
SignInView()
.onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
}
}
.onAppear {
viewModel.listen()
}
}
}
struct FirstView: View {
#EnvironmentObject private var appViewModel: AppViewModel
var body: some View {
VStack{
Spacer()
VStack(spacing: 50){
NavigationLink(destination: Text("Test")){
awButton(content: "Request Support", backColor: Color(#colorLiteral(red: 0, green: 0.723585546, blue: 0.9907287955, alpha: 1)))
.shadow(color: Color.primary.opacity(0.5), radius: 20, x: 0, y: 20)
.rotation3DEffect(Angle(degrees:10), axis: (x: 10.0, y: 0, z: 0))
}
NavigationLink(destination: Text("Test")){
awButton(content: "Request Quote", backColor: Color(#colorLiteral(red: 0.9372549057, green: 0.3490196168, blue: 0.1921568662, alpha: 1)))
.shadow(color: Color.primary.opacity(0.5), radius: 20, x: 0, y: 20)
.rotation3DEffect(Angle(degrees:10), axis: (x: 10.0, y: 0, z: 0))
}
NavigationLink(destination: Text("Test")){
awButton(content: "Ticket Status", backColor: Color(#colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1)))
.shadow(color: Color.primary.opacity(0.5), radius: 20, x: 0, y: 20)
.rotation3DEffect(Angle(degrees:10), axis: (x: 10.0, y: 0, z: 0))
}
}
Spacer()
}
.navigationBarBackButtonHidden(true)
.navigationTitle(appViewModel.userInfo?.company ?? "Test")
.navigationBarItems(leading: Button(action: {
appViewModel.signOut()
}) {
HStack {
Text("Sign Out")
}
},trailing: HStack{
Image(systemName: "bell")
Text(appViewModel.userInfo?.name ?? "Tester")
.font(.system(size: 20))
})
}
}
Try to hide it explicitly, like
if viewModel.signedIn {
FirstView()
.navigationBarHidden(true) // << here !!
} else {
Hiding the navigationBar will cause lots of problems if you need it in other views .the better answer in my opinion is to
1 - create a splash view and set it as your starting view in #main file
2 - add a StateObject for tracking user loginState :
class LoginState: ObservableObject {
#Published var isLogin : Bool = false
}
3 - add navigationView to splash view
4 - pass loginState , StateObject as .environmentObject(loginState) to navigationView
5- now you can change login state when ever you want from anywhere without need of hiding the navigation bar
There is a button in the TabBar, this button should open the camera
"Button(action: {...}, label: { ..."
The camera code is written in "CameraView" and "CameraModel" :
struct CameraView: View {
#StateObject var camera = CameraModel()
var body: some View {
ZStack{
// Camera preview...
CameraPreview(camera: camera)
.ignoresSafeArea(.all, edges: .all)
VStack{
if camera.isTaken{
HStack {
Spacer()
Button(action: {}, label: {
Image(systemName: "arrow.triangle.2.circlepath.camera")
.foregroundColor(.black)
.padding()
.background(Color.white)
.clipShape(Circle())
})
.padding(.trailing,10)
}
}
Spacer()
HStack{
if camera.isTaken{
Button(action: {}, label: {
Text("Save")
.foregroundColor(.black)
.fontWeight(.semibold)
.padding(.vertical,10)
.padding(.horizontal,20)
.background(Color.white)
.clipShape(Capsule())
})
.padding(.leading)
Spacer()
}
else{
Button(action: {camera.isTaken.toggle()}, label: {
ZStack{
Circle()
.fill(Color.white)
.frame(width: 65, height: 65)
Circle()
.stroke(Color.white,lineWidth: 2)
.frame(width: 75, height: 75)
}
})
}
}
.frame(height: 75)
}
}
.onAppear(perform: {
camera.Check()
})
}
}
class CameraModel: ObservableObject{
#Published var isTaken = false
#Published var session = AVCaptureSession()
#Published var alert = false
// since were going to read pic data....
#Published var output = AVCapturePhotoOutput()
// preview....
#Published var preview : AVCaptureVideoPreviewLayer!
func Check(){
// first checking camerahas got permission...
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
setUp()
return
// Setting Up Session
case .notDetermined:
// retusting for permission....
AVCaptureDevice.requestAccess(for: .video) { (status) in
if status{
self.setUp()
}
}
case .denied:
self.alert.toggle()
return
default:
return
}
}
func setUp(){
// setting up camera...
do{
// setting configs...
self.session.beginConfiguration()
// change for your own...
let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
let input = try AVCaptureDeviceInput(device: device!)
// checking and adding to session...
if self.session.canAddInput(input){
self.session.addInput(input)
}
// same for output....
if self.session.canAddOutput(self.output){
self.session.addOutput(self.output)
}
self.session.commitConfiguration()
}
catch{
print(error.localizedDescription)
}
}
}
// setting view for preview...
struct CameraPreview: UIViewRepresentable {
#ObservedObject var camera : CameraModel
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: UIScreen.main.bounds)
camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
camera.preview.frame = view.frame
// Your Own Properties...
camera.preview.videoGravity = .resizeAspectFill
view.layer.addSublayer(camera.preview)
// starting session
camera.session.startRunning()
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
How to call the camera on click Button in TabBar "Button(action: {}, label: {
)"?
This is just an example of how I did it in the past (after all permissions etc... have been done).
Use your code to do the same.
struct ContentView: View {
#State var image: UIImage?
#State private var showCamera = false
var body: some View {
VStack {
Button(action: { self.showCamera.toggle() }) {
Image(systemName: "camera.circle").resizable().frame(width: 100, height: 100)
}
if image != nil {
Image(uiImage: image!).resizable().frame(width: 200, height: 200)
}
} // this is where it happens
.sheet(isPresented: $showCamera, onDismiss: {self.showCamera = false}) {
CameraViewController(photo: $image)
}
}
}
struct CameraViewController: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var photo: UIImage?
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
}
func makeUIViewController(context: UIViewControllerRepresentableContext<CameraViewController>) -> UIImagePickerController {
let vc = UIImagePickerController()
vc.sourceType = .camera
vc.delegate = context.coordinator
return vc
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate, AVCapturePhotoCaptureDelegate {
var parent: CameraViewController
var captureSession: AVCaptureSession!
var capturePhotoOutput: AVCapturePhotoOutput!
var theCamera: AVCaptureDevice!
var videoPreviewLayer: AVCaptureVideoPreviewLayer?
let photoQualityPrioritizationMode = AVCapturePhotoOutput.QualityPrioritization.speed
init(_ imagePickerController: CameraViewController) {
self.parent = imagePickerController
}
// called when a picture has been taken
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info:[UIImagePickerController.InfoKey : Any]) {
guard let image = info[.originalImage] as? UIImage else {
print("No image found")
return
}
parent.photo = image // <--- the photo image
parent.presentationMode.wrappedValue.dismiss()
}
}
}
Unwrapping values the conventional way in SwiftUI views is not allowed, but you can do so within the body of, say, an HStack or a VStack. Below I am trying to unwrap a user I was certain was not nil. My LoginView lets a user create an account on Firebase Firestore, logs you in if a user enters the correct values, and shows an alert view if not.
In my HomeView.swift, the first screen after launch, I crash when trying to present the user image. However, when I unwrap the user, the user is still nil in the console. I think this is because Firebase doesn't have time to load the image before the view initializes. But I could be wrong. Help.
struct HomeProfileView: View {
#EnvironmentObject var session: SessionStore
#State var showDashboard = false
var user: User?
var body: some View {
if user != nil {
URLImage(URL(string: user!.profileImageUrl)!, content: {
$0.image
.resizable()
.aspectRatio(contentMode: .fill)
.clipShape(Circle())
})
.frame(width: 50, height: 50)
.background(Color(#colorLiteral(red: 0.9490196078, green: 0.9490196078, blue: 0.9490196078, alpha: 1)))
.clipShape(Circle())
.shadow(color: Color.black.opacity(0.1), radius: 1, x: 0, y: 1)
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 10)
.sheet(isPresented: $showDashboard) {
DashboardView(showDashboard: $showDashboard)
}
}
}
}
I don't think I need an optional user, here, but I do not know how to safely unwrap the user otherwise. Another version is using the session EnvironmentObject to access the currently logged user.
struct HomeProfileView: View {
#EnvironmentObject var session: SessionStore
#State var showDashboard = false
var body: some View {
if session.isLoggedIn {
URLImage(URL(string: session.userSession!.profileImageUrl)!, content: {
$0.image
.resizable()
.aspectRatio(contentMode: .fill)
.clipShape(Circle())
})
.frame(width: 50, height: 50)
.background(Color(#colorLiteral(red: 0.9490196078, green: 0.9490196078, blue: 0.9490196078, alpha: 1)))
.clipShape(Circle())
.shadow(color: Color.black.opacity(0.1), radius: 1, x: 0, y: 1)
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 10)
.sheet(isPresented: $showDashboard) {
DashboardView(showDashboard: $showDashboard)
}
}
}
}
The session property belongs to a SessionStore class and is essentially an optional User.
import Foundation
import Combine
import Firebase
class SessionStore: ObservableObject {
#Published var isLoggedIn: Bool = UserDefaults.standard.bool(forKey: "isLoggedIn") {
didSet {
UserDefaults.standard.set(self.isLoggedIn, forKey: "isLoggedIn")
}
}
var userSession: User?
var handle: AuthStateDidChangeListenerHandle?
func listenAuthenticationState() {
handle = Auth.auth().addStateDidChangeListener({ (auth, user) in
if let user = user {
print(user.email as Any)
let firestoreUserId = Ref.FIRESTORE_DOCUMENT_USERID(userId: user.uid)
firestoreUserId.getDocument { (document, error) in
if let dict = document?.data() {
guard let decodeUser = try? User.init(fromDictionary: dict) else { return }
self.userSession = decodeUser
}
}
self.isLoggedIn = true
} else {
print("User is Logged Out")
self.isLoggedIn = false
self.userSession = nil
}
})
}
}
The logged in user is indeed logged in but I cannot access any properties.
Set isLoggedIn exactly at the place you set userSession, because it is done in async callback, and do this on main queue (to update UI properly)
firestoreUserId.getDocument { (document, error) in
if let dict = document?.data() {
guard let decodeUser = try? User.init(fromDictionary: dict) else { return }
DispatchQueue.main.async {
self.userSession = decodeUser
self.isLoggedIn = true
}
}
}
I have this working. Not sure how scalable it is but I am doing a not equals nil. I also put it in a button to toggle it versus an onTapGesture.
struct HomeProfileView: View {
#EnvironmentObject var session: SessionStore
#State var showDashboard = false
var body: some View {
if session.isLoggedIn {
if (session.userSession?.profileImageUrl) != nil {
Button(action: { self.showDashboard.toggle() } ) {
URLImage(URL(string: session.userSession!.profileImageUrl)!, content: {
$0.image
.resizable()
.aspectRatio(contentMode: .fill)
.clipShape(Circle())
})
.frame(width: 50, height: 50)
.background(Color(#colorLiteral(red: 0.9490196078, green: 0.9490196078, blue: 0.9490196078, alpha: 1)))
.clipShape(Circle())
.shadow(color: Color.black.opacity(0.1), radius: 1, x: 0, y: 1)
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 10)
.sheet(isPresented: $showDashboard) {
DashboardView(showDashboard: $showDashboard)
}
}
}
}
}
}