How to change State variables one after another in SwiftUI? - swift

I have a Menu with a few buttons. Each button, represents an URL.
On selecting one of the buttons I want to present a webView loading the said URL using .fullScreenCover(isPresented:)
#State private var showWebPage = false
#State private var urlToLoad = ""
...
View()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
Button("FAQ", action: {
presentWebView(for: "https://example.com/faqsLink")
})
Button("Privacy Policy", action: {
presentWebView(for: "https://example.com/privacyLink")
})
Button("Terms and Conditions", action: {
presentWebView(for: "https://example.com/termsLink")
})
}
}
}
.fullScreenCover(isPresented: $showWebPage) {
WebView(url: URL(string: urlToLoad)!)
}
private func presentWebView(for url: String) {
urlToLoad = url
showWebPage.toggle()
}
Everytime I try this, urlToLoad is still empty when I toggle showWebPage
I feel it has to do with how #State works but can't figure it out, I'm still new to SwiftUI.

with the hint from Delano I managed to come up with this:
.fullScreenCover(isPresented: $showWebPage) {
WebView(url: URL(string: urlToLoad)!)
.withCloseButton(and: "FAQ")
}
.onChange(of: urlToLoad) { value in
log.info (value)
if !value.isEmpty {
showWebPage.toggle()
}
}
and in the button action I just update the value of urlToLoad
Hope this helps out somebody, I spent more time on this than I ever thought a Menu needed.

Related

SwiftUI Swipe Actions Note Working Correctly

So I’m working on this shopping list and basically I have a list of tile views that have swipe actions.
The bought and delete actions work perfectly, but I am unable to get the edit button to work. The edit button pulls up a form already filled with the item’s information so you can edit it.
The code for the button works, when I make the tile itself a button and tap the tile the edit form comes up.
Is there a limit to swipe actions that limit you pulling up a form in a swipe action? Here is the code for the list. Thanks! (Program is coded using swift ui and swift)
ForEach(Item) { madeItem in
FoodListTileView(foodItem: madeItem)
.swipeActions(edge: .leading) {
Button {
madeItem.justBought()
FoodDataController.shared.saveItem(madeItem: madeItem)
} label: {
Label("Just Bought", systemImage: "checkmark.square")
}
.tint(.green)
}
.swipeActions(edge: .trailing) {
Button { FoodDataController.shared.deleteItem(madeItem: madeItem)} label : {
Label("Delete", systemImage:"trash")
}
.tint(.red)
Button(action: {showingCreateView = true}) {
Label("Edit", systemImage: "pencil.circle")
}
.sheet(isPresented: $showingCreateView) {
AddItemView(Item: madeItem)
}
.tint(.orange)
}
}
}
.listStyle(.plain)
#Raja is absolutely right. The .sheet inside swipeAction doesn't work, it has to be outside. Better even outside of the ForEach, as otherwise it will always only show the last value of the forEach.
If it's outside the ForEach, you have to use a different way to let sheet know which data to display: .sheet(item:) which is the better alternative in most cases anyway. For that go from #State var showingCreateView: Bool to something like #State var editingItem: Item? where Item should be your items Class. And in the button action, give it the current item value.
Here is the amended code:
// test item struct
struct Item: Identifiable {
let id = UUID()
var name: String
}
struct ContentView: View {
// test data
#State private var items = [
Item(name: "Butter"),
Item(name: "Honey"),
Item(name: "Bread"),
Item(name: "Milk"),
Item(name: "Ham")
]
#State private var editingItem: Item? // replace Item with your item type
var body: some View {
List {
ForEach(items) { madeItem in
Text(madeItem.name)
.swipeActions(edge: .leading){
Button {
} label: {
Label("Just Bought", systemImage: "checkmark.square")
}
.tint(.green)
}
.swipeActions(edge: .trailing){
Button {
} label : {
Label("Delete", systemImage:"trash")
}
.tint(.red)
Button {
editingItem = madeItem // setting editingItem (anything else but nil) brings up the sheet
} label: {
Label("Edit", systemImage: "pencil.circle")
}
.tint(.orange)
}
}
}
.sheet(item: $editingItem) { item in // change to item: init here
Text("Edit: \(item.name)")
}
.listStyle(.plain)
}
}

SwiftUI: Edit list row relement with swipe action and present modal sheet

Hi. I have a swiftui project with a list. I would now like to add 2 trailing swipe actions to this list, once the .onDelete and a edit swipe action to the left of it.
Like this:
Look:
To achieve this in swiftui I added the following code to my list:
List {
ForEach(timers, id: \.id) { timer in
TimerRow(timer: timer)
}
.onDelete(perform: { IndexSet in deleteTimer(IndexSet) })
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button {
// Open edit sheet
isShowEditTimer.toggle()
} label: {
Image(systemName: "pencil.circle")
}
}
}
But unfortunately only the edit function is displayed now:
Look πŸ™
Do you know how I can solve my problem?
But now to my real problem:
I now want to open a modal sheet when the edit swipe action of a line is pressed. But how do I find out which line was swiped on? With the .onDelete function we get an IndexSet, but nothing here. I would also like to give the struct that is called in my sheet this certain swiped element (CoreData object):
.sheet(isPresented: $isShowEditTimer) {
EditTimerView(timerObject: ???)
}
By the way, this sheet is applied to my navigation view.
I would be really happy if someone could help me and if you didn't report my post. Maybe this question has been asked somewhere deep in StackOverflow, but I'm also relatively new to swiftui (always UIKit before) and don't understand every stackoverflow post yet.
Thanks!!! πŸ˜€
Hi Eric and welcome to SO! If you wanna get answers to your question pls read this article https://stackoverflow.com/help/minimal-reproducible-example
Anyway, I faced the same problem that you describing and if you don't mind I will show how to solve it on my code base
import SwiftUI
struct Car: Identifiable {
let id = UUID().uuidString
var model: String
}
class CarViewModel: ObservableObject {
#Published var cars: [Car] = [
.init(model: "Audi"),
.init(model: "BMW"),
.init(model: "Chevrolet"),
.init(model: "Honda"),
.init(model: "Toyota")
]
func del(_ car: Car) {
if let deletingCarIndex = self.cars.firstIndex(where: { $0.id == car.id}) {
self.cars.remove(at: deletingCarIndex)
}
}
}
struct ContentView: View {
#ObservedObject var vm = CarViewModel()
#State var editingCar: Car?
var body: some View {
List {
ForEach(vm.cars) { car in
Text(car.model)
.swipeActions(edge: .trailing) {
Button(role: .destructive) { // if you implement custom swipeAction you have no more access to .onDelete
vm.del(car) // if you stay in ForEach loop you have access to car instance and can find index of this instance in array
} label: {
Label("Delete", systemImage: "trash")
}
Button {
edit(car)
} label: {
Label("Edit", systemImage: "pencil.circle")
}
}
}
.sheet(item: $editingCar) {
reset()
} content: { car in
VStack(spacing: 20) {
Text(car.model)
Text(car.id)
}
}
}
}
func reset() {
editingCar = nil
}
func edit(_ car: Car) {
editingCar = car
}
}
Hope now it clear for you πŸ™‚

SwiftUI Navigation Link embedded in a Button with an action

So I've made a Button view in my body and I have set it's action to a Google Sign In action, but I want it to also transition to a new view when the sign in flow is completed. The problem is that I have set the label of the button to a Navigation Link and when I click it, it directly transitions to a next view. How can I delay the transition? For context, VoucherView is the next view I want to transition to.
Button {
guard let clientID = FirebaseApp.app()?.options.clientID else { return }
// Create Google Sign In configuration object.
let config = GIDConfiguration(clientID: clientID, serverClientID: GlobalConstants.BACKEND_SERVER_REQUEST_ID_TOKEN)
guard let presenter = CommonHelper.GetRootViewController() else {return}
// Start the sign in flow!
GIDSignIn.sharedInstance.signIn(with: config, presenting: presenter) {user, error in
if let error = error {
print (error.localizedDescription)
return
}
guard
let authentication = user?.authentication,
let idToken = authentication.idToken
else {
print ("Something went wrong!")
return
}
print (authentication)
print (idToken)
let credential = GoogleAuthProvider.credential(withIDToken: idToken,
accessToken: authentication.accessToken)
print (credential)
UserManager.shared.firebaseAuthentication(credential: credential)
signed = true
}
} label: {
NavigationLink {
VoucherView()
} label: {
Text("Sign In")
}
}
Edit: After I tried using isValid as a #State variable, after every cycle of SignIn and SignOut the screen goes down.
First SignIn
FirstSignOut
SecondSignIn
SecondSignOut
Instead of using NavigationLink inside your Button, you can use a NavigationLink with an EmptyView for a label and then activate it programmatically with the isActive parameter.
See the following example -- the Google Sign In process is replaced by just a simple async closure for brevity.
struct ContentView: View {
#State private var navigateToNextView = false
var body: some View {
NavigationView {
VStack {
Button {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
navigateToNextView = true
}
} label: {
Text("Sign in")
}
NavigationLink(destination: View2(), isActive: $navigateToNextView) { EmptyView()
}
}
}.navigationTitle("Test")
}
}
struct View2 : View {
var body: some View {
Text("Next view")
}
}
A couple more notes:
Note that there has to be a NavigationView (make sure it's just one) in the hierarchy for this to work
In general, async work is better done in an ObservableObject than in a View. You can consider moving your login function to an ObservableObject and making the navigateToNextView a #Published property on that object.

How to get rid of a glitch when deleting a ForEach item

I have a MyObservableObject showing [ForEachItem] and a ForEachShowTest view.
When I want to "delete" an item from visibility, I want to do it via contextMenu and setting show = false. The issue is, there is a glitch with items overlay for a second. I managed to deal with it via using DispatchQueue but now it feels less responsive as it takes time for the item to disappear. Is there any other way?
You can try it yourselves or see a video: we.tl/t-5jMLsTD2dW
view:
struct ForEachShowTest: View {
#ObservedObject var myObservableObject = MyObservableObject()
var forEachList: [ForEachItem] { myObservableObject.forEachList.filter({ $0.show }) }
var body: some View {
VStack {
ForEach(forEachList) { item in
HStack {
Text(item.item)
}
.contextMenu {
Button("Don't show without glitch") {
item.show = false
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(800)) {
myObservableObject.updateUI = true
}
}
Button("Don't show with glitch") {
item.show = false
myObservableObject.updateUI = true
}
}
}
Spacer()
}
}
}
ObservableObject:
class MyObservableObject: ObservableObject {
var forEachList: [ForEachItem] = [ ForEachItem(item: "I love ForEach"), ForEachItem(item: "Another list"), ForEachItem(item: "I want to show this one"), ForEachItem(item: "I want to show this one"), ForEachItem(item: "I want to show this one") ]
#Published var updateUI: Bool = false
init() { }
}
class ForEachItem: Identifiable {
let id: UUID
let item: String
var show: Bool
init(item: String, show: Bool = true) {
id = UUID()
self.item = item
self.show = show
}
}
It seems to be a issue with Swift UI. You get the same behavior with lists too:
SwiftUI - delete row in list with context menu - UI glitch
If working with a List is an option for you, you may use the .onDelete() modifier for hiding the item without deleting it as a workaround:
List {
ForEach(forEachList) { item in
HStack {
Text(item.item)
}
}.onDelete(perform: { indexSet in
for index in indexSet{
forEachList[index].show = false
}
myObservableObject.updateUI = true
})
}
}

SwiftUI - Form with error message on button press and navigation

I have the following scenario. I have a text field and a button, what I would need is to show an error message in case the field is empty and if not, navigate the user to the next screen.
I have tried showing the error message conditionally by using the field value and checking if it is empty on button press, but then, I don't know how to navigate to the next screen.
struct SomeView: View {
#State var fieldValue = ""
#State var showErrorMessage = false
var body: some View {
NavigationView {
VStack {
TextField("My Field", text: $fieldValue).textFieldStyle(RoundedBorderTextFieldStyle())
if showErrorMessage {
Text("Error, please enter value")
}
Button(action: {
if self.fieldValue.isEmpty {
self.showErrorMessage = true
} else {
self.showErrorMessage = false
//How do I put navigation here, navigation link does not work, if I tap, nothing happens
}
}) {
Text("Next")
}
}
}
}
}
Using UIKit would be easy since I could use self.navigationController.pushViewController
Thanks to part of an answer here, here's some working code.
First, I moved everything into an EnvronmentObject to make things easier to pass to your second view. I also added a second toggle variable:
class Model: ObservableObject {
#Published var fieldValue = ""
#Published var showErrorMessage = false
#Published var showSecondView = false
}
Next, change two things in your ContentView. I added a hidden NavigationLink (with a isActive parameter) to actually trigger the push, along with changing your Button action to execute a local function:
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationView {
VStack {
TextField("My Field", text: $model.fieldValue).textFieldStyle(RoundedBorderTextFieldStyle())
NavigationLink(destination: SecondView(), isActive: $model.showSecondView) {
Text("NavLink")
}.hidden()
Button(action: {
self.checkForText()
}) {
Text("Next")
}
.alert(isPresented: self.$model.showErrorMessage) {
Alert(title: Text("Error"), message: Text("Please enter some text!"), dismissButton: .default(Text("OK")))
}
}
}
}
func checkForText() {
if model.fieldValue.isEmpty {
model.showErrorMessage.toggle()
} else {
model.showSecondView.toggle()
}
}
}
Toggling showErrorMessage will show the Alert and toggling `showSecondView will take you to the next view.
Finally, the second view:
struct SecondView: View {
#EnvironmentObject var model: Model
var body: some View {
ZStack {
Rectangle().fill(Color.green)
// workaround
.navigationBarBackButtonHidden(true) // not needed, but just in case
.navigationBarItems(leading: MyBackButton(label: "Back!") {
self.model.showSecondView = false
})
Text(model.fieldValue)
}
}
func popSecondView() {
model.showSecondView.toggle()
}
}
struct MyBackButton: View {
let label: String
let closure: () -> ()
var body: some View {
Button(action: { self.closure() }) {
HStack {
Image(systemName: "chevron.left")
Text(label)
}
}
}
}
This is where the above linked answer helped me. It appears there's a bug in navigation back that still exists in beta 6. Without this workaround (that toggles showSecondView) you will get sent back to the second view one more time.
You didn't post any details on the second view contents, so I took the liberty to add someText into the model to show you how to easily pass things into it can be using an EnvironmentObject. There is one bit of setup needed to do this in SceneDelegate:
var window: UIWindow?
var model = Model()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(model))
self.window = window
window.makeKeyAndVisible()
}
}
I noticed a slight change in this, depending on when your project was created (beta 6 declares an instance of contentView where older versions do not). Either way, declare an instance of model and then add the envoronmentObject modifier to contentView.
Another approach is to make the "Next" button conditionally a Button when the fieldValue is empty and a NavigationLink when the fieldValue is valid. The Button case will trigger your error message view and the NavigationLink will do the navigation for you. Keeping this close to your sample, the following seems to do the trick.
struct SomeView: View {
#State var fieldValue = ""
#State var showErrorMessage = false
var body: some View {
NavigationView {
VStack {
TextField("My Field", text: $fieldValue).textFieldStyle(RoundedBorderTextFieldStyle())
if showErrorMessage {
Text("Please Enter Data")
}
if fieldValue == "" {
Button(action: {
if self.fieldValue == "" {
self.showErrorMessage = true
}
}, label: {
Text("Next")
})
} else {
// move on case
NavigationLink("Next", destination: Text("Next View"))
}
}
}
}
}
By using this code we can display the alert if the fields are empty else . it will navigate.
struct SomeView: View {
#State var userName = ""
#State var password = ""
#State var showErrorMessage = false
var body: some View {
NavigationView {
VStack {
TextField("Enter Username", text: $userName).textFieldStyle(RoundedBorderTextFieldStyle())
SecureField("Enter Your Password", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
if userName == "" || password == "" {
Button(action: {
if self.userName == "" || self.password == "" {
self.showErrorMessage = true
}
}, label: {
Text("Login")
})
} else {
// move case
NavigationLink("Login", destination: Text("Login successful"))
}
}.alert(isPresented: $showErrorMessage) { () -> Alert in
Alert(title: Text("Important Message"), message: Text("Please Fill all the Fields"), primaryButton: .default(Text("Ok")), secondaryButton: .destructive(Text("Cancel")))
}
}
}
}