Im trying to set displayName to users collection and display it in textfield - google-cloud-firestore

When registering a user through Apple sign in, a users collection is created in which there are userId, email and displayName fields.
When registering, the user cannot set a displayName, so the field is empty, in settings user can set displayName.
I have a test textfield and would like to keep it that way. (done and edit button next to the field, not in the bar)
#State var nameInEditMode = false
#State var name = "Example"
HStack {
if nameInEditMode {
TextField("New name", text: $name)
.padding(.leading, 5)
.onReceive(name.publisher.collect()) {
self.name = String($0.prefix(10))
}
} else {
Text(name)
}
Button(action: {
self.nameInEditMode.toggle()
}) {
Text(nameInEditMode ? "Done" : "Edit")
}
}
I tried to change displayName with:
#ObservedObject var viewModel = SetNameView()
#State var mode: Mode = .new
HStack {
if mode == .new {
TextField("New name", text: $viewModel.updatename.displayName)
.padding(.leading, 5)
.onReceive(viewModel.updatename.displayName.publisher.collect()) {
self.viewModel.updatename.displayName = String($0.prefix(10))
}
} else {
Text(viewModel.updatename.displayName)
}
Button(action: {
self.handleDoneTapped()
}) {
Text(mode == .new ? "Done" : "Edit")
}
}
func handleDoneTapped() {
self.viewModel.save()
}
}
Firestore parameters for updating data inside users collection:
class SetNameView: ObservableObject {
#Published var updatename: FBKeys
#Published var modified = false
private var cancellables = Set<AnyCancellable>()
init(updatename: FBKeys = FBKeys(displayName: "")) {
self.updatename = updatename
self.$updatename
.dropFirst()
.sink { [weak self] updatename in
self?.modified = true
}
.store(in: &self.cancellables)
}
private var db = Firestore.firestore()
private func addItem(_ updatename: FBKeys) {
do {
var addedItem = updatename
addedItem.displayName = Auth.auth().currentUser?.uid ?? ""
_ = try db.collection("users").addDocument(from: addedItem)
}
catch {
print(error)
}
}
private func updateItem(_ updatename: FBKeys) {
if let documentID = updatename.id {
do {
try db.collection("users").document(documentID).setData(from: updatename)
}
catch {
print(error)
}
}
}
public func updateOrAddItem() {
if let _ = updatename.id {
self.updateItem(self.updatename)
}
else {
addItem(updatename)
}
}
func save() {
self.updateOrAddItem()
}
}
Keys:
struct FBKeys: Codable, Hashable, Identifiable {
#DocumentID var id = UUID().uuidString
var displayName : String
var userId : String?
}
In general, when I try to substitute the viewModel, I can’t enter characters, but I can click on the edit button and already on clicking the button (with obviously impossible to enter any characters), a new document is still created in the users collection, nothing changes in the existing one user document.
Through the edit button (since it's the only one displayed) - I call the handleDoneTapped() func, which in turn calls the function inside SetNameView() -> func save() inside which has self.updateOrAddItem()
There are many problems, but the main one that haunts me is that a new document is being created. It is necessary that when entering text in the field and user saves it, the name is entered in the same user document in the displayName field.
I cleaned up the code from garbage as much as possible for better understanding.

Related

How do I show a certain amount of of users while giving the option to load more?

Part of my app, I allow users to explore other users with a ForEach line - however I have quickly realised how inefficient and laggy this will become in the future.
My question is: How do I only show a few users at first (lets say 8) then once you scroll to the bottom of the displayed users more will load in?
My code is below.
SWIFT UI VIEW:
NavigationView {
VStack {
SearchBar(text: $viewModel.searchText)
.padding()
ScrollView {
LazyVStack {
ForEach(viewModel.searchableUsers) { user in
NavigationLink {
ProfileView(user: user)
} label: {
UserRowView(user: user)
}
}
}
}
}
}
VIEW MODEL:
class ExploreViewModel: ObservableObject {
#Published var users = [User]()
#Published var searchText = ""
private let config: ExploreViewModelConfiguration
var searchableUsers: [User] {
if searchText.isEmpty {
return users
} else {
let lowercasedQuery = searchText.lowercased()
return users.filter({
$0.username.contains(lowercasedQuery) ||
$0.name.lowercased().contains(lowercasedQuery)
})
}
}
let service = UserService()
init(config: ExploreViewModelConfiguration) {
self.config = config
fetchUsers(forConfig: config)
}
func fetchUsers(forConfig config: ExploreViewModelConfiguration) {
service.fetchUsers { users in
let users = users
switch config {
case .newMessage:
self.users = users.filter({ !$0.isCurrentUser })
case .search:
self.users = users
}
}
}
}

why data are passing back but SwiftUi not updating Text

I get to pass back data via closure, so new name is passed, but my UI is not updating. The new name of the user is printed when I go back to original view, but the text above the button is not getting that new value.
In my mind, updating startingUser should be enough to update the ContentView.
my ContentView:
#State private var startingUser: UserData?
var body: some View {
VStack {
Text(startingUser?.name ?? "no name")
Text("Create start user")
.onTapGesture {
startingUser = UserData(name: "Start User")
}
}
.sheet(item: $startingUser) { userToSend in
DetailView(user: userToSend) { newOnePassedFromWhatDoneInEDitView in
startingUser = newOnePassedFromWhatDoneInEDitView
print("✅ \(startingUser?.name)")
}
}
}
my EditView:
struct DetailView: View {
#Environment(\.dismiss) var dismiss
var user: UserData
var callBackClosure: (UserData) -> Void
#State private var name: String
var body: some View {
NavigationView {
Form {
TextField("your name", text: $name)
}
.navigationTitle("edit view")
.toolbar {
Button("dismiss") {
var newData = self.user
newData.name = name
newData.id = UUID()
callBackClosure(newData)
dismiss()
}
}
}
}
init(user: UserData, callBackClosure: #escaping (UserData) -> Void ) {
self.user = user
self.callBackClosure = callBackClosure
_name = State(initialValue: user.name)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(user: UserData.example) { _ in}
}
}
my model
struct UserData: Identifiable, Codable, Equatable {
var id = UUID()
var name: String
static let example = UserData(name: "Luke")
static func == (lhs: UserData, rhs: UserData) -> Bool {
lhs.id == rhs.id
}
}
update
using these changes solves the matter, but my question remains valid, cannot understand the right reason why old code not working, on other projects, where sheet and text depends on the same #state var it is working.
adding
#State private var show = false
adding
.onTapGesture {
startingUser = UserData(name: "Start User")
show = true
}
changing
.sheet(isPresented: $show) {
DetailView(user: startingUser ?? UserData.example) { newOnePassedFromWhatDoneInEDitView in
startingUser = newOnePassedFromWhatDoneInEDitView
print("✅ \(startingUser!.name)")
}
}
The reason Text is not showing you the updated user name that you are passing in the closure is, your startingUser property will be set to nil when you dismiss the sheet because you have bind that property with sheet. Now after calling callBackClosure(newData) you are calling dismiss() to dismiss the sheet. To overcome this issue you can try something like this.
struct ContentView: View {
#State private var startingUser: UserData?
#State private var updatedUser: UserData?
var body: some View {
VStack {
Text(updatedUser?.name ?? "no name")
Text("Create start user")
.onTapGesture {
startingUser = UserData(name: "Start User")
}
}
.sheet(item: $startingUser) { userToSend in
DetailView(user: userToSend) { newUser in
updatedUser = newUser
print("✅ \(updatedUser?.name ?? "no name")")
}
}
}
}
I would suggest you to read the Apple documentation of sheet(item:onDismiss:content:) and check the example from the Discussion section to get more understanding.

Persistently store list items added by user in SwiftUI

I am very new to swift (as in I started today) and I have code that allows the user to add items to a list:
private func onAdd() {
let alert = UIAlertController(title: "Enter a name for your plant", message: "Make sure it's descriptive!", preferredStyle: .alert)
alert.addTextField { (textField) in
textField.placeholder = "Enter here"
}
alert.addAction(UIAlertAction(title: "Done", style: .default) { _ in
let textField = alert.textFields![0] as UITextField
plantName2 = textField.text ?? "Name"
appendItem()
})
showAlert(alert: alert)
}
private func appendItem() {
items.append(Item(title: plantName2))
}
and
struct HomePage: View {
#State var plantName2: String = ""
#State private var items: [Item] = []
#State private var editMode = EditMode.inactive
private static var count = 0
var body: some View {
NavigationView {
List {
Section(header: Text("My Plants")) {
ForEach(items) { item in
NavigationLink(destination: PlantView(plantName3: item.title)) {
Text(item.title)
}
}
.onDelete(perform: onDelete)
.onMove(perform: onMove)
.onInsert(of: [String(kUTTypeURL)], perform: onInsert)
}
}
.listStyle(InsetGroupedListStyle()) // or GroupedListStyle
.navigationBarTitle("Plantify")
.navigationBarTitleTextColor(CustomColor.pastelGreen)
.navigationBarItems(leading: EditButton().accentColor(CustomColor.pastelGreen), trailing: addButton)
.environment(\.editMode, $editMode)
}
}
and I want the entries the user adds to be saved in persistent storage. I've looked at the docs for persistent storage and am a bit confused. Is it even possible with the code I have?
Thanks!
Basically you can save the data as String array in UserDefaults
func save() {
UserDefaults.standard.set(items.map(\.title), forKey: "items")
}
func load() {
let savedItems = UserDefaults.standard.stringArray(forKey: "items") ?? []
items = savedItems.map(Item.init)
}
and call load in .onAppear.
However if there is a huge amount of items consider a file in the Documents folder or Core Data

How Can You Reinstantiate EnvironmentObjects is SwiftUI?

I am working on a SwiftUI project and using Firebase. Part of the app allows users to add business addresses to their account. In Firebase I add a BusinessAddress as a sub collection to the user.
User -> BusinessAddresses
In iOS I am using snapshotlistners and Combine to get the data from Firestore. My issue happens when one user logs out and a new user logs in. The business addresses from the first user continue to show in the view that is designated to show business addresses.
I've confirmed the Firebase code is working properly. What seems to be happening is that the creation of BusinessAddressViewModel maintains the original instance of BusinessAddressRepository from when the first user logs in.
My question is how do I "reset" the instance of businessAddressRepository in my BusinessAddressViewModel when a new user logs in?
BusinessAddressRepository
This file gets the data from Firebase. It gets the currently user that is logged in and pulls the business addresses for this user. For each log in, I can see that the proper user and addresses are being updated here.
class BusinessAddressRepository: ObservableObject {
let db = Firestore.firestore()
private var snapshotListener: ListenerRegistration?
#Published var businessAddresses = [BusinessAddress]()
init() {
startSnapshotListener()
}
func startSnapshotListener() {
guard let currentUserId = Auth.auth().currentUser else {
return
}
if snapshotListener == nil {
self.snapshotListener = db.collection(FirestoreCollection.users).document(currentUserId.uid).collection(FirestoreCollection.businessAddresses).addSnapshotListener { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
guard let documents = querySnapshot?.documents else {
print("No Business Addresses.")
return
}
self.businessAddresses = documents.compactMap { businessAddress in
do {
print("This is a business address *********** \(businessAddress.documentID)")
return try businessAddress.data(as: BusinessAddress.self)
} catch {
print(error)
}
return nil
}
}
}
}
}
}
BusinessAddressViewModel
This file uses Combine to monitor the #published property businessAddresses from bankAccountRepository. This seems to be where the problem is. For some reason, this is holding on to the instance that is created when the app starts up.
class BusinessAddressViewModel: ObservableObject {
var businessAddressRepository: BusinessAddressRepository
#Published var businessAddressRowViewModels = [BusinessAddressRowViewModel]()
private var cancellables = Set<AnyCancellable>()
init(businessAddressRepository: BusinessAddressRepository) {
self.businessAddressRepository = businessAddressRepository
self.startCombine()
}
func startCombine() {
businessAddressRepository
.$businessAddresses
.receive(on: RunLoop.main)
.map { businessAddress in
businessAddress
.map { businessAddress in
BusinessAddressRowViewModel(businessAddress: businessAddress)
}
}
.assign(to: \.businessAddressRowViewModels, on: self)
.store(in: &cancellables)
}
}
BusinessAddressViewModel is set up and an #EnvironmentObject in main.
Main
#StateObject private var businessAddressViewModel = BusinessAddressViewModel(businessAddressRepository: BusinessAddressRepository())
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(businessAddressViewModel)
}
I was able to confirm it's hanging on to the instance in the view BusinessAddressView. Here I use a List that looks at each BusinessAddress in BusinessAddressViewModel. I use print statements in the ForEach and an onAppear that show the BusinessAddresses from the first user.
BusinessAddressView
struct BusinessAddressView: View {
let db = Firestore.firestore()
#EnvironmentObject var businessAddressViewModel: BusinessAddressViewModel
var body: some View {
List {
ForEach(self.businessAddressViewModel.businessAddressRowViewModels, id: \.id) { businessAddressRowViewModel in
let _ = print("View Business Addresses \(businessAddressRowViewModel.businessAddress.id)")
NavigationLink(destination: BusinessAddressDetailView(businessAddressDetailViewModel: BusinessAddressDetailViewModel(businessAddress: businessAddressRowViewModel.businessAddress, businessAddressRepository: businessAddressViewModel.businessAddressRepository))
) {
BusinessAddressRowView(businessAddressRowViewModel: businessAddressRowViewModel)
}
} // ForEach
} // List
.onAppear(perform: {
for businessAddress in self.businessAddressViewModel.businessAddressRepository.businessAddresses {
let _ = print("On Appear Business Addresses \(businessAddress.id)")
}
}) // CurrentUserUid onAppear
} // View
}
So, how do I reset the instance of BusinessAddressViewModel and get it to look at the current BusinessAddressRepository?

How do I generate custom TextFields from an array of strings, each with its own edit/delete controls?

My IOS application has a form-builder for users. Users can create a form, add questions to it, and change question types, including Text Response, Multiple Choice, and Checkbox. For the Multiple Choice and Checkbox types, users must provide between 2-5 options.
In my QuestionManager (view model), I'm storing these options as an array of strings. In the view, I'm generating OptionCells using a ForEach loop. Each OptionCell has a TextField and control functions to delete, edit and confirm each option. The issue is that I'm unsure about my approach because there are a few bugs that I think are due to the way I've set this up.
For example, which is really the only issue, I have a #State var isEditing: Bool variable in the OptionCell that is not being updated, which I believe has something do with the way I've declared the #Published var options: [String] in the QuestionManager. Or it might have something to do with the #State var option: String in the OptionCell. (I tried setting this up as #Binding, but for some reason that would not work.) But none of this makes sense to me since the isEditing variable is set up to update when the View is tapped, though I've narrowed the problem down to what I'm most uncertain about and I believe it is somehow related to what I described above. Any suggestions?
struct OptionCell: View {
#ObservedObject var manager: QuestionManager
#State var option: String
#State var isEditing: Bool = false
var optIndex: Int
init(_ manager: QuestionManager, option: String) {
self.manager = manager
self.option = option
self.optIndex = manager.options!.firstIndex(of: option)!
}
var body: some View {
HStack {
MyCustomTextField(text: $option)
.keyboardType(.default)
.onTapGesture {
option = ""
isEditing = true // The main issue -> Isnt updating the view
}
// ActionIcon is just a Button with a SFLabels string input
ActionIcon(isEditing ? "checkmark" : "xmark", size: 10) { // No changes are recognized
isEditing ? saveOption() : deleteOption()
}
.padding(.horizontal)
}
}
func saveOption() {
if option == "" {
manager.error = "An option must not be empty"
} else {
manager.options![optIndex] = option
isEditing = false
}
}
func deleteOption() {
if manager.options!.count == 2 {
manager.error = "You must have at least two options"
} else {
manager.deleteOption(option: option)
}
}
}
struct QuestionView: View {
#Environment(\.presentationMode) var mode
#ObservedObject var manager: QuestionManager
init(_ manager: QuestionManager) { self.manager = manager }
var body: some View {
ZStack {
MyCustomViewWrapper {
// ^ Reskins the background, adds padding, etc.
ScrollView {
VStack(spacing: 20) {
ConstantTitledTextField("Type", manager.type.rawValue)
// ^ Reskinned TextField
.overlay {
HStack {
Spacer()
self.changeTypeMenu
}
.padding(.horizontal)
.padding(.top, 30)
}
TitledTextField("Prompt", $manager.prompt)
// ^ Another reskinned TextField
.keyboardType(.default)
.onTapGesture { manager.prompt = "" }
MyCustomDivider()
if manager.options != nil {
self.optionsSection
}
}
}
}
.alert(isPresented: $manager.error.isNotNil()) {
MyCustomAlert(manager.error!) { manager.error = nil }
}
VStack {
Spacer()
MyCustomButton("Save") { checkAndSave() }
}
.hiddenIfKeyboardActive()
}
}
var changeTypeMenu: some View {
Image(systemName: "chevron.down").icon(15)
.menuOnPress {
Button(action: { self.manager.changeType(.text) } ) { Text("Text Response") }
Button(action: { self.manager.changeType(.multiple) } ) { Text("Multiple Choice") }
Button(action: { self.manager.changeType(.checkbox) } ) { Text("Checkbox") }
}
}
var optionsSection: some View {
VStack(spacing: 15) {
HStack {
Text("Options")
.font(.title3)
.padding(.horizontal)
Spacer()
ActionIcon("plus", size: 15) { manager.addOption() }
.padding(.horizontal)
}
ForEach(manager.options!, id: \.self) { opt in
OptionCell(manager, option: opt)
}
}
}
private func checkAndSave() {
if manager.checkQuestion() {
manager.saveQuestion()
self.mode.wrappedValue.dismiss()
}
}
}
class QuestionManager: ObservableObject {
#Published var question: Question
#Published var type: Question.QuestionType
#Published var prompt: String
#Published var options: [String]?
#Published var error: String? = nil
#Published var id = ""
init(_ question: Question) {
self.question = question
self.type = question.type
self.prompt = question.prompt
self.options = question.options
self.id = question.id
}
func saveQuestion() {
self.question.prompt = prompt
self.question.type = type
self.question.options = options
}
func checkQuestion() -> Bool {
if type == .checkbox || type == .multiple {
if options!.count < 2 {
error = "You must have at least two options"
return false
}
else if options!.contains("") {
error = "Options must not be empty"
return false
}
else if !options!.elementsAreUnique() {
error = "Options must be unique"
return false
}
}
if prompt.isEmpty {
error = "You must include a prompt for your question"
return false
}
return true
}
func changeType(_ type: Question.QuestionType) {
if type == .text {
options = nil
}
else {
options = ["Option 1", "Option 2", "Option 3"]
}
self.type = type
}
func addOption() {
guard options != nil else { return }
if options!.count == 5 {
error = "No more than 5 options are permitted"
}
else {
let count = options!.count
let newOption = "Option \(count + 1)"
options!.append(newOption)
}
}
func deleteOption(option: String) {
guard options != nil else { return }
guard let index = self.options!.firstIndex(of: option) else {
self.error = "An error occured during deleting ... sorry you're screwed"
return
}
self.options!.remove(at: index)
}
}