How to initialize a class using that is using #Binding for an #AppStorage variable - swift

// I am trying to pass in the value of the #AppStorage onto the class using #Binding.
// Declaration of #AppStorge, this the the single source of truth.
// I get the following error on the SigninViewModel = Class 'SigninViewModel' has no initializers
import SwiftUI
struct MainView: View {
#AppStorage("userSignIn") var userSignIn = false
var body: some View {
ZStack {
TabView {
AppointmentsView()
.tabItem {
Image(systemName: "calendar")
Text("Appointments")
}
AccountView(userSignIn: .constant(true))
.tabItem {
Image(systemName: "person")
Text("Profile")
}
StatsView()
.tabItem {
Image(systemName: "chart.bar")
Text("Stats")
}
}
}
}
}
// This the class with the #Binding is being declared and where I am trying to have access to the #AppStorae value.
class SigninViewModel: ObservableObject {
#Published var nonce = ""
#Binding var userSignIn: Bool
func authenticate(credential: ASAuthorizationAppleIDCredential) {
// getting Token...
guard let token = credential.identityToken else {
print("Error with Firebase")
return
}
//Token String...
guard let tokenString = String(data: token, encoding: .utf8) else {
print("Error with Token")
return
}
let firebaseCredential = OAuthProvider.credential(withProviderID: "apple.com",
idToken: tokenString,
rawNonce: nonce)
Auth.auth().signIn(with: firebaseCredential) { (result, err) in
if let error = err {
print(error.localizedDescription)
return
}
// User succesfully logged into Firebase...
print("Logged in Success")
// Directing user to Main page...
withAnimation(.easeInOut) {
self.userSignIn = true
}
}
}
}

You can put the #AppStorage("userSignIn") inside the ObservableObject, but your code quickly becomes a mess.
I think a more simple structure would be to put all of your authentication state inside a single AuthenticationModel: ObservableObject and use it as a #StateObject in the MainView. Additionally, I would recommend pushing it up the view tree.
class AuthenticationModel: ObservableObject {
#Published var nonce = ""
#Published var userSignIn: Bool
init() {
let defaults = UserDefaults.standard
userSignIn = defaults.bool(forKey: "userSignIn")
}
func authenticate(credential: ASAuthorizationAppleIDCredential)
...
// Successful Login
let defaults = UserDefaults.standard
defaults.set(true, forKey: "userSignIn")
// Update the views
userSignIn = true
}
}
struct MainView: View {
#StateObject var authModel: AuthenticationModel = AuthenticationModel()
var body: some View {
if authModel.userSignIn {
Home(authModel: authModel)
} else {
SignIn(authModel: authModel)
}
}
}
struct SignIn: View {
#ObservedObject var authModel: AuthenticationModel
var body: some View {
Button("Sign In") {
authModel.authenticate(...)
}
}
}

Related

Cannot convert value of type 'Notifications.Type' to expected argument type 'Notifications'

I trying to integreat firebase with my program to make true or false but I keep get the error
Cannot convert value of type 'Notifications.Type' to expected argument type 'Notifications'
Here is my code
import SwiftUI
struct ProfileHost: View {
#Environment(\.editMode) var editMode
#EnvironmentObject var modelData: ModelData
#State private var draftProfile = Profile.default
#State private var notifications = Notifications(id: "", prefersNotifications: true)
var body: some View {
VStack(alignment: .leading, spacing: 20) {
HStack {
if editMode?.wrappedValue == .active {
Button("Cancel", role: .cancel) {
draftProfile = modelData.profile
notifications = modelData.notifications
editMode?.animation().wrappedValue = .inactive
}
}
Spacer()
EditButton()
}
if editMode?.wrappedValue == .inactive {
ProfileSummary(profile: modelData.profile, notifications: modelData.notifications)
} else {
ProfileEditor(profile: $draftProfile, notifications: $notifications)
.onAppear {
draftProfile = modelData.profile
}
.onDisappear {
modelData.profile = draftProfile
}
}
}
.padding()
}
}
struct ProfileHost_Previews: PreviewProvider {
static var previews: some View {
ProfileHost()
.environmentObject(ModelData())
}
}
Modeldata is the file I made the notifications var
This the code of modeldata
import Foundation
import Combine
final class ModelData: ObservableObject {
#Published var landmarks: [Landmark] = load("landmarkData.json")
var hikes: [Hike] = load("hikeData.json")
#Published var profile = Profile.default
#Published var notifications = Notifications.self
var features: [Landmark] {
landmarks.filter { $0.isFeatured }
}
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarks,
by: { $0.category.rawValue }
)
}
}
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
at
if editMode?.wrappedValue == .inactive {
ProfileSummary(profile: modelData.profile, notifications: modelData.notifications)
I get an error that says.
Cannot convert value of type 'Notifications.Type' to expected argument
type 'Notifications'
I tried to look at other wedsites like github and reddit

Can't change #State var

I have SwiftUI view which I want to change after checking user log-pass. I'm trying to change isAuth var like you can see below.
import SwiftUI
struct Auth : View, AuthProtocol {
#State private var isAuth = false
init() {
userManager.notifier = self
}
var body : some View {
if isAuth {
WelcomeView()
} else {
VStack {
Divider()
Text("Please, wait a minute...")
Divider()
}
.frame(width: 450, height: 350)
}
}
func passAuth() {
if userManager.validateUser() {
self.isAuth.toggle()
print("isAuth: \(isAuth)")
}
}
}
And I got output isAuth: false.
I call passAuth func from this code
class <classname> {
var notifier : AuthProtocol!
func fetchUsers() {
db.collection("users").getDocuments { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
var newUser = UserData()
newUser.login = document["login"] as! String
newUser.name = document["name"] as! String
newUser.password = document["password"] as! String
newUser.accessLevel = document["access_level"] as! Int
self.usersList.append(newUser)
}
self.notifier.passAuth()
}
}
}
}
I have no idea why value of isAuth isn't changing...
We use MVVM in swiftui. so we need a viewModel. now when the network request resutl will come we can update our view model and the change will be reflected by the View, simple right? i advice you to check out swiftui tutorials.
struct Auth : View {
#StateObject var viewModel = ViewModel()
// #Binding var isAuth: Bool // should use binding is isAuth will be passed by a parent view
var body : some View {
if viewModel.isAuth {
Text("WelcomeView()")
} else {
VStack {
Divider()
Text("Please, wait a minute...")
Divider()
}
.frame(width: 450, height: 350)
}
}
}
class ViewModel : ObservableObject {
#Published var isAuth = false
func fetchUsers() {
db.collection("users").getDocuments { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
var newUser = UserData()
newUser.login = document["login"] as! String
newUser.name = document["name"] as! String
newUser.password = document["password"] as! String
newUser.accessLevel = document["access_level"] as! Int
self.usersList.append(newUser)
}
//
if validateUser() {
self.isAuth = true
}
//
}
}
}
}

Swift load user data for dashboard after login

I am trying to retrieve user data once the user gets to the dashboard of my app
I have essentially this to get data:
class UserController: ObservableObject {
#Published var firstName: String = ""
func fetchUser(token: String) {
/* Do url settings */
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else { return }
let rData = try! JSONDecoder().decode(User.self, from: data)
let userData = [
"id": rData.id,
"firstName": rData.firstName,
"lastName": rData.lastName,
"department": rData.department,
]
UserDefaults.standard.set(userData, forKey: "user")
DispatchQueue.main.async {
self.firstName = rData.firstName
}
}.resume()
}
}
And then my view looks like this
struct HomeViewCollection: View {
#Binding var isAuthenticated: Bool
#ObservedObject var userController: UserController = UserController()
var body: some View {
VStack {
Text("Hello \(userController.firstName)!")
}
}
}
I'm just not sure how can I activate fetchUser from the View.
I have tried this in the controller
init() {
guard let tokenData = KeyChain.load(key: "token") else { return }
var token = String(data: tokenData, encoding: .utf8)
if(token != nil) {
print("Token: \(token)")
fetchUser(token: token!)
}
}
That didn't work, and then I tried userController.fetchUser(token: KeyChainTokenHere) and that didn't work because it doesn't conform to the struct.
Try passing the token to HomeViewCollection and initiating the call in onAppear completion block.
struct HomeViewCollection: View {
var token: String
#Binding var isAuthenticated: Bool
#ObservedObject var userController = UserController()
var body: some View {
VStack {
Text("Hello \(userController.firstName)!")
}
.onAppear {
self.userController.fetchUser(token: self.token)
}
}
}
Also, make sure the firstName property is getting set.
#Published var firstName: String = "" {
didSet {
print("firstName is set as \(firstName)")
}
}

ListView in child view is not refreshed correctly

There is a ListView. I make a transaction in Cloud Firestore by changing the field of an element when I click on it in the list. Data in the database changes as it should, but after this action all the elements in the list disappear (although there is .onAppear {fetchData}). An important point: this is a child view, there is no such problem in the parent view.
I also added a button at the bottom of the list to execute fetchData (), when I click on it, the data returns to the list
What could be the problem? Thanks
import SwiftUI
struct SecondView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
VStack {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Button("Update data"){
let updBook = book
self.viewModel.myTransaction(book: updBook)
}
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.navigationBarTitle("Books")
.onAppear() {
self.viewModel.fetchData()
}
Button("update list"){
self.viewModel.fetchData()
}
}
}
}
ViewModel:
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
class BooksViewModel: ObservableObject {
#Published var books = [Book]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.books = documents.compactMap { queryDocumentSnapshot -> Book? in
return try? queryDocumentSnapshot.data(as: Book.self)
}
}
}
func deleteBook(book: Book){
if let bookID = book.id{
db.collection("books").document(bookID).delete()
}
}
func updateBook(book: Book) {
if let bookID = book.id{
do {
try db.collection("books").document(bookID).setData(from: book) }
catch {
print(error)
}
}
}
func addBook(book: Book) {
do {
let _ = try db.collection("books").addDocument(from: book)
}
catch {
print(error)
}
}
func myTransaction(book: Book){
let bookID = book.id
let targetReference = db.collection("books").document(bookID!)
db.runTransaction({ (transaction, errorPointer) -> Any? in
let targetDocument: DocumentSnapshot
do {
try targetDocument = transaction.getDocument(targetReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let oldValue = targetDocument.data()?["pages"] as? Int else {
let error = NSError(
domain: "AppErrorDomain",
code: -1,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot \(targetDocument)"
]
)
errorPointer?.pointee = error
return nil
}
// Note: this could be done without a transaction
// by updating the population using FieldValue.increment()
transaction.updateData(["pages": oldValue + 1], forDocument: targetReference)
return nil
}) { (object, error) in
if let error = error {
print("Transaction failed: \(error)")
} else {
print("Transaction successfully committed!")
}
}
}
}
Parent view:
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
NavigationView {
VStack {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Button("Update"){
let delBook = book
self.viewModel.myTransaction(book: delBook)
}
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.navigationBarTitle("Books")
.onAppear() {
self.viewModel.fetchData()
}
NavigationLink(destination: SecondView()){
Text("Second View")
}
}
}
}
}
A possible solution might be that your Views and its ViewModels interfere with each other. It looks like you create two instances of the same BookViewModel:
struct ContentView: View {
#ObservedObject var viewModel = BooksViewModel()
struct SecondView: View {
#ObservedObject var viewModel = BooksViewModel()
Try creating one BooksViewModel and pass it between views (you can use an #EnvironmentObject).

Entries from text field are not adding into list

import SwiftUI
import Firebase
import FirebaseFirestore
struct ContentView: View {
var body: some View {
customView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct customView : View{
#State var msg = ""
#ObservedObject var datas = observer()
var body : some View{
VStack{
List{
ForEach(datas.data){i in
Text(i.msg)
}
.onDelete { (index) in
let id = self.datas.data[index.first!].id
let db = Firestore.firestore().collection("msgs")
db.document(id).delete{(err) in
if err != nil{
print((err!.localizedDescription))
return
}
print("deleted Successfully !!!")
self.datas.data.remove(atOffsets: index)
}
}
}
HStack{
TextField("msg", text: $msg).textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
print(self.msg)
self.addData(msg1: self.msg)
}) {
Text("Add")
}.padding()
}.padding()
}
}
func addData(msg1:String){ 'Here is the code for additon'
let db = Firestore.firestore()
let msg = db.collection("msgs").document()
msg.setData(["id":msg.documentID,"msg": msg1]) { (err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
print("Success")
self.msg = ""
}
}
}
class observer : ObservableObject{
#Published var data = [datatype]()
init() {
let db = Firestore.firestore().collection("msg")
db.addSnapshotListener{(snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in snap!.documentChanges{
if i.type == .added{
let msgData = datatype(id: i.document.documentID, msg: i.document.get("msg")
as! String)
self.data.append(msgData)
}
}
}
}
}
struct datatype : Identifiable {
var id : String
var msg : String
}
Here is the code for the CRUD(creation, reading, update, delete) using the firebase cloud service. When i enter the text in the text field and then hit button, it adds to firebase database, but doesn't shows up in the interface in the list of the app. Can anybody tell me where i am going wrong?
In this i am trying to add data to firebase, delete it ,read it and modify/update it. But the data entered doesn't shows up in interface.
Try to update data container explicitly on main thread, like below
if i.type == .added{
let msgData = datatype(id: i.document.documentID, msg: i.document.get("msg")
as! String)
DispatchQueue.main.async {
self.data.append(msgData)
}
}
as alternate try assignment instead of modification (however this should not be the case)
DispatchQueue.main.async {
self.data = self.data + [msgData]
}