SwiftUI - how to get newly created object to navigation link - swiftui

I have a form that saves a new customer when a trailing navigation bar button labeled "Save" is pressed. I would then like to load a new view based on the new customer object. One possible way might be to activate a navigation link with an empty view like this:
NavigationLink(destination: CustomerDetailView(customer: currentCustomer), isActive: self.$showCustomerDetail) { EmptyView() }
Setting showCustomerDetail to true would make trigger the link. But to get a value for currentCustomer I'd need a dynamic fetch request, probably using the new UUID as a predicate. And that's where things fall apart. I can't figure out how to get the result of the fetch request to the currentCustomer variable.
Here are the useful parts of the code:
Navigation Bar Buttons
.navigationBarItems(leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
}), trailing:
Button(action: {
let newCustomer = Customer(context: self.moc)
newCustomer.id = UUID()
newCustomer.custName = self.name
}
self.appDelegate.saveContext()
self.showCustomerDetail = true
})
}, label: {
Text("Save")
})
Fetch Request
currentCustomer = FetchRequest<Customer>(entity: Customer.entity(), sortDescriptors: [], predicate: NSPredicate(format: "id == %#", custID as CVarArg))

Here is appropriate approach (as newCustomer is already saved into database it can be just used):
.navigationBarItems(leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
}), trailing:
Button(action: {
let newCustomer = Customer(context: self.moc)
newCustomer.id = UUID()
newCustomer.custName = self.name
}
self.appDelegate.saveContext()
self.currentCustomer = newCustomer // << here !!
self.showCustomerDetail = true
})
}, label: {
Text("Save")
})

Related

Picker not selecting the desired option

I am trying to setup a picker, simple. I am successfully fetching an array of projects from firebase and populating the picker with the names of the projects. The problem that I am having is that I need to get the project id when I click the list but it's not doing anything after I click the option that I want. I tried to run it in a simulator and also on my iPhone and nothing happens after I make the selection. I am pretty sure I am not updating the picker and thus I am not updating the variable with the selected project id. I tried using the .onChange on the picker but nothing happens.
import SwiftUI
struct NewProjectView: View {
#ObservedObject var viewModel = ProjectViewModel()
#ObservedObject var clientViewModel = ClientFeedViewModel()
#Environment (\.dismiss) var dismiss
#State var projectName: String = "s"
var clientNameIsEmpty: Bool {
if projectName.count < 3 {
return true
} else {
return false
}
}
var clients: [Client] {
return clientViewModel.clients
}
#State var selectedClient: String = ""
var body: some View {
NavigationView {
VStack {
Picker("", selection: $selectedClient) {
ForEach(clients, id:\.self) {
Text($0.clientName)
//I need to exctract the project id so I can pass it on
}
}
.pickerStyle(.menu)
CustomTextField(text: $projectName, placeholder: Text("Client Name"), imageName: "person.text.rectangle")
.padding()
.background(Color("JUMP_COLOR")
.opacity(0.75)
)
.cornerRadius(10)
.padding(.horizontal, 40)
Text("Name must contain more than 3 characters")
.font(.system(.subheadline))
.foregroundColor(.gray.opacity(0.3))
.padding(.top, 30)
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: {
Button(action: {
dismiss()
}, label: {
Text("Cancel")
})
})
ToolbarItem(placement: .navigationBarTrailing , content: {
Button(action: {
viewModel.newProject(name: projectName)
dismiss()
}, label: {
Text("Save")
})
.disabled(clientNameIsEmpty)
})
}
}
}
.presentationDetents([.height(400)])
//.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
Here is the picker populated with the foo data: picker
Your selection variable $selectedClient needs to have a type that matches the tagged value of each item in the picker.
As you're not specifying an explicit .tag for your text, the ForEach creates an implicit one using what it's using for tracking its loop, which in this case looks like it's a Client.
You can either change selectedClient to be a type of Client, or tag your displayed subview with the string value to populate selectedClient with, e.g.:
ForEach(clients, id: \.self) { client in
Text(client.clientName)
.tag(client.clientID)
}
Also, if each client has a unique ID, you're better off using that as ForEach's identifier than \.self. You can either specify id: \.clientID, etc., to use a single attribute – or you can add Identifiable conformance to Client and make sure that it has an id value that is guaranteed to be unique.
import SwiftUI
import Firebase
struct NewProjectView: View {
#ObservedObject var viewModel = ProjectViewModel()
#ObservedObject var clientViewModel = ClientFeedViewModel()
#Environment (\.dismiss) var dismiss
#State var projectName: String = "s"
var clientNameIsEmpty: Bool {
if projectName.count < 3 {
return true
} else {
return false
}
}
var clients: [Client] {
return clientViewModel.clients
}
#State var selectedClient: Client = Client(id: "", clientName: "Blank", timestamp: Timestamp(), ownerId: "", ownerUsername: "")
var body: some View {
NavigationView {
VStack {
Picker("d", selection: $selectedClient) {
ForEach(clients, id:\.id) { client in
Text(client.clientName)
.tag(client)
//I need to exctract the project id so I can pass it on
}
}
.pickerStyle(.menu)
Text(selectedClient.id ?? "")
CustomTextField(text: $projectName, placeholder: Text("Client Name"), imageName: "person.text.rectangle")
.padding()
.background(Color("JUMP_COLOR")
.opacity(0.75)
)
.cornerRadius(10)
.padding(.horizontal, 40)
Text("Name must contain more than 3 characters")
.font(.system(.subheadline))
.foregroundColor(.gray.opacity(0.3))
.padding(.top, 30)
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: {
Button(action: {
dismiss()
}, label: {
Text("Cancel")
})
})
ToolbarItem(placement: .navigationBarTrailing , content: {
Button(action: {
viewModel.newProject(name: projectName)
dismiss()
}, label: {
Text("Save")
})
.disabled(clientNameIsEmpty)
})
}
}
}
.presentationDetents([.height(400)])
//.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}

SwiftUI: Navigation view pops back when variable inside a filtered array is updated

I have a navigation view with a list in my content view. In my list there is a ForEach that is passed an array. If I pass the ForEach just the array, everything works fine. If I filter that array, it will pop back anytime a change is made to a variable inside the array. I have a similar question that I already posted which has an answer (to use .stack navigation style). The navigation view is utilizing the .stack style. You can see in the code below that the array is filtered to show books that haven't been finished. In BookView there is a toggle that will set if the book is finished or not. My question, is there a way to stop it from popping back when using a filtered list?
NavigationView {
List {
ForEach(user.books.filter({$0.finished == false})) { book in
NavigationLink(destination: {
BookView(book: $user.books.first(where: {$0.id == book.id})!)
}, label: {
BookListItem(book: book)
})
}
.listStyle(.plain)
.searchable(text: $search, placement: .automatic, prompt: "Search")
.navigationTitle("Books")
.toolbar(content: {
ToolbarItemGroup(placement: .bottomBar, content: {
HStack {
Button(action: {
settings.toggle()
}, label: {
Text("Settings")
})
Spacer()
Button(action: {
addBook.toggle()
}, label: {
Text("Add Book")
})
}
})
})
.sheet(isPresented: $addBook, onDismiss: {}, content: {
AddBookView(user: $user)
})
.sheet(isPresented: $settings, onDismiss: {}, content: {
SettingsView(user: $user)
})
.sheet(isPresented: $editBook, onDismiss: {
//Clear editing book
editingBook = nil
}, content: {
EditBookView(user: $user,
book: $user.books.first(where: {$0.id == editingBook!.id})!,
sheet: $editBook,
name: editingBook!.name,
kind: editingBook!.kind,
author: editingBook!.author ?? "",
iconColor: editingBook!.iconColor)
})
}
.navigationViewStyle(.stack)
This is the Book model to help replicate the issue.
struct Book: Identifiable, Codable, Hashable {
enum Kind: String, Hashable, CaseIterable, Codable {
case fiction = "Fiction"
case nonfiction = "Nonfiction"
}
//Needs at creation
var name: String //Book name
var kind: Kind //Book kind
var finished: Bool //If book is finished
}
Toggle in the BookView
Toggle("Finished Reading", isOn: $book.finished)
Weirdly enough, when I used a computed property to search through the array, it will work okay. You can type something in the search bar, click on an item from the now filtered list, set the book to finished, and it won't pop back. It only seems to be an issue when I'm trying to filter by if the books are finished or not.
var searchedBooks: [Book] {
if search.isEmpty {
return user.books
} else {
//Lowercase the words when filtering
return filteredBooks.filter({
$0.name.lowercased().contains(search.lowercased().trimmed()) || //See if any of the book names contain what's in the search
$0.kind.rawValue.lowercased().contains(search.lowercased().trimmed()) //Check for fiction or nonfiction
})
}
}

How can I stop a SwiftUI TextField from losing focus when binding within a ForEach loop?

In a form, I'd like a user to be able to dynamically maintain a list of phone numbers, including adding/removing numbers as they wish.
I'm currently maintaining the list of numbers in a published array property of an ObservableObject class, such that when a new number is added to the array, the SwiftUI form will rebuild the list through its ForEach loop. (Each phone number is represented as a PhoneDetails struct, with properties for the number itself and the type of phone [work, cell, etc].)
Adding/removing works perfectly fine, but when I attempt to edit a phone number within a TextField, as soon as I type a character, the TextField loses focus.
My instinct is that, since the TextField is bound to the phoneNumber property of one of the array items, as soon as I modify it, the entire array within the class publishes the fact that it's been changed, hence SwiftUI dutifully rebuilds the ForEach loop, thus losing focus. This behavior is not ideal when trying to enter a new phone number!
I've also tried looping over an array of the PhoneDetails objects directly, without using an ObservedObject class as an in-between repository, and the same behavior persists.
Below is the minimum reproducible example code; as mentioned, adding/removing items works great, but attempting to type into any TextField immediately loses focus.
Can someone please help point me in the right direction as to what I'm doing wrong?
class PhoneDetailsStore: ObservableObject {
#Published var allPhones: [PhoneDetails]
init(phones: [PhoneDetails]) {
allPhones = phones
}
func addNewPhoneNumber() {
allPhones.append(PhoneDetails(phoneNumber: "", phoneType: "cell"))
}
func deletePhoneNumber(at index: Int) {
if allPhones.indices.contains(index) {
allPhones.remove(at: index)
}
}
}
struct PhoneDetails: Equatable, Hashable {
var phoneNumber: String
var phoneType: String
}
struct ContentView: View {
#ObservedObject var userPhonesManager: PhoneDetailsStore = PhoneDetailsStore(
phones: [
PhoneDetails(phoneNumber: "800–692–7753", phoneType: "cell"),
PhoneDetails(phoneNumber: "867-5309", phoneType: "home"),
PhoneDetails(phoneNumber: "1-900-649-2568", phoneType: "office")
]
)
var body: some View {
List {
ForEach(userPhonesManager.allPhones, id: \.self) { phoneDetails in
let index = userPhonesManager.allPhones.firstIndex(of: phoneDetails)!
HStack {
Button(action: { userPhonesManager.deletePhoneNumber(at: index) }) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $userPhonesManager.allPhones[index].phoneNumber)
}
}
Button(action: { userPhonesManager.addNewPhoneNumber() }) {
Label {
Text("Add Phone Number")
} icon: {
Image(systemName: "plus.circle.fill")
}
}.buttonStyle(BorderlessButtonStyle())
}
}
}
try this:
ForEach(userPhonesManager.allPhones.indices, id: \.self) { index in
HStack {
Button(action: {
userPhonesManager.deletePhoneNumber(at: index)
}) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $userPhonesManager.allPhones[index].phoneNumber)
}
}
EDIT-1:
Reviewing my comment and in light of renewed interest, here is a version without using indices.
It uses the ForEach with binding feature of SwiftUI 3 for ios 15+:
class PhoneDetailsStore: ObservableObject {
#Published var allPhones: [PhoneDetails]
init(phones: [PhoneDetails]) {
allPhones = phones
}
func addNewPhoneNumber() {
allPhones.append(PhoneDetails(phoneNumber: "", phoneType: "cell"))
}
// -- here --
func deletePhoneNumber(of phone: PhoneDetails) {
allPhones.removeAll(where: { $0.id == phone.id })
}
}
struct PhoneDetails: Identifiable, Equatable, Hashable {
let id = UUID() // <--- here
var phoneNumber: String
var phoneType: String
}
struct ContentView: View {
#ObservedObject var userPhonesManager: PhoneDetailsStore = PhoneDetailsStore(
phones: [
PhoneDetails(phoneNumber: "800–692–7753", phoneType: "cell"),
PhoneDetails(phoneNumber: "867-5309", phoneType: "home"),
PhoneDetails(phoneNumber: "1-900-649-2568", phoneType: "office")
]
)
var body: some View {
List {
ForEach($userPhonesManager.allPhones) { $phone in // <--- here
HStack {
Button(action: {
userPhonesManager.deletePhoneNumber(of: phone) // <--- here
}) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $phone.phoneNumber) // <--- here
}
}
Button(action: { userPhonesManager.addNewPhoneNumber() }) {
Label {
Text("Add Phone Number")
} icon: {
Image(systemName: "plus.circle.fill")
}
}.buttonStyle(BorderlessButtonStyle())
}
}
}

SwiftUI multiple sheets: dismiss animation broken

I have button1, showing sheet1 when pressed. Then I have a toggle, showing a button2, which in turn presents sheet2 when pressed.
If I just press button1, the sheet shows and dismisses with animation. If, however, I press the toggle, which shows button2, and then press button1, the sheet dismiss animation is broken (simply doesn't animate).
struct ContentView: View {
#State private var showSheetButton2 = false
#State private var showSheet1 = false
#State private var showSheet2 = false
var body: some View {
VStack {
Toggle("Show Sheet Button 2", isOn: $showSheetButton2)
Button(action: {
showSheet1.toggle()
}, label: {
Text("Show Sheet 1")
})
.sheet(isPresented: $showSheet1, content: {
Button(action: {
showSheet1 = false
}, label: {
Text("Dismiss")
})
})
if showSheetButton2 {
Button(action: {
showSheet2.toggle()
}, label: {
Text("Show Sheet 2")
})
.sheet(isPresented: $showSheet2, content: {
Button(action: {
showSheet2 = false
}, label: {
Text("Dismiss")
})
})
}
}.padding()
}
}
EDIT:
Fixed in iOS 14.5:
You can now apply multiple sheet(isPresented:onDismiss:content:) and
fullScreenCover(item:onDismiss:content:) modifiers in the same view
hierarchy. (74246633)
Somehow this sheet inside If statement is destroying animation. Please try to add EmptyView outside this if statement and assign sheet to that view.
if showSheetButton2 {
Button(action: {
showSheet2.toggle()
}, label: {
Text("Show Sheet 2")
})
}
EmptyView()
.sheet(isPresented: $showSheet2, content: {
Button(action: {
showSheet2 = false
}, label: {
Text("Dismiss")
})
})

How to move between views in SwiftUI?

I am trying to open a view on a button click rather than on clicking a Navigation Link, but all I can find on the internet is how to do it with a Navigation Link. So how do you do it without?
Code:
#State var goToMainView = false
NavigationLink(destination: ContentView(),isActive: $goToMainView,label:{}).hidden()
Button(action: {
goToMainView = true
context.delete(goal)
do {
try context.save()
} catch {
print(error)
}
}, label: {
Text("Delete Goal")
})
You can navigate with a button by using NavigationLink
#State var goToSecondView = false
NavigationView {
VStack {
NavigationLink(destination: SecondView(),isActive: $goToSecondView,
label: { EmptyView() })
}.hidden()
Button(action : { goToSecondView = true } , label : { Text("Click On Me") })
}
}
And for more details , I wrote this article , it may help you
How to navigate with SwiftUI