How to perform action before executing navigationLink - swift

I am building and app and I am currently in the Login view. In this view, the user enters its information and then clicks a "Log In" button. The thing is that I need to make some calls to my API when the user clicks the button, and then execute the NavigationLink method, if the data the user entered was correct. The only way I have managed to to this has been using isActive parameter, but it is no longer available as it is deprecated.
My code currently is the following:
var body: some View {
NavigationView(){
GeometryReader { geometry in
VStack(alignment: .center, spacing: 24){
TextField("Enter your username", text: $username)
.font(.title3)
.frame(maxWidth: geometry.size.width*0.85)
.padding()
.background(Color.white.opacity(0.9))
.cornerRadius(22)
SecureField("Enter your password", text: $password)
.font(.title3)
.frame(maxWidth: geometry.size.width*0.85)
.padding()
.background(Color.white.opacity(0.9))
.cornerRadius(22)
Text("Log in")
.font(.title2)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(Color.accent)
.cornerRadius(22)
.padding(.horizontal).onTapGesture {
buttonClicked()
// In the method buttonClicked I would like to make the API
// calls and then the navigate to main view
}
}
.frame(width: geometry.size.width)
.frame(minHeight: geometry.size.height)
.background(Color.welcomeBackground)
.navigationTitle("Log In")
}
}
.toolbar(.visible)
}
I have also tried with NavigationStack, but it doesn't work either.
#State private var signInPressed: Bool = false
var body: some View {
NavigationStack{
GeometryReader { geometry in
VStack(alignment: .center, spacing: 24){
TextField("Enter your username", text: $username)
.font(.title3)
.frame(maxWidth: geometry.size.width*0.85)
.padding()
.background(Color.white.opacity(0.9))
.cornerRadius(22)
SecureField("Enter your password", text: $password)
.font(.title3)
.frame(maxWidth: geometry.size.width*0.85)
.padding()
.background(Color.white.opacity(0.9))
.cornerRadius(22)
Text("Log in")
.font(.title2)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(Color.accent)
.cornerRadius(22)
.padding(.horizontal).onTapGesture {
signInPressed.toggle()
}
}
.frame(width: geometry.size.width)
.frame(minHeight: geometry.size.height)
.background(Color.welcomeBackground)
.navigationTitle("Log In")
}
}
.navigationDestination(isPresented: $signInPressed, destination: {
ContentView()
})
.toolbar(.visible)
}

Instead of using a navigation link use a button and have a separate navigation link which can be activate by a #State variable. I've added a fake login method that disables the button and shows an activity indicator while the login process in occurring.
< iOS 16:
func Login() async -> Bool {
try? await Task.sleep(nanoseconds: 5 * 1_000_000_000) // 5 seconds
return true
}
struct LoginView : View {
#State private var showMainView = false
#State private var isLoggingIn = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: Text("Logged in"), isActive: $showMainView) { EmptyView() }
Button(action: {
isLoggingIn = true
// do code to validate login here
Task {
//if login call worked then show the main view
showMainView = await Login()
isLoggingIn = false
}
})
{
HStack{
Spacer()
if(isLoggingIn){
ProgressView()
}
else{
Text("Login")
}
Spacer()
}.padding()
.background(.green)
}.disabled(isLoggingIn)
}
}
}
}
iOS 16:
In iOS 16 this can be replaced by using a NavigationStack and a navigationDestination as seen below.
struct LoginView : View {
#State private var showMainView = false
#State private var isLoggingIn = false
var body: some View {
NavigationStack{
VStack{
Button(action: {
isLoggingIn = true
// do code to validate login here
Task {
//if login call worked then show the main view
showMainView = await Login()
isLoggingIn = false
}
})
{
HStack{
Spacer()
if(isLoggingIn){
ProgressView()
}
else{
Text("Login")
}
Spacer()
}.padding()
.background(.green)
}.disabled(isLoggingIn)
}.navigationDestination(isPresented: $showMainView) {
Text("Logged in")
}
}
}
}

Related

How to change language of the app with toggles in SwiftUI

I have a view with list of toggles with languages that should change current app language after the restart. I understand how to create this feature with buttons, but it doesn't work the same way with toggles. I've tried to use on change, but it's not allowing to turn off the toggle after the second tap. How to do that properly?
struct Languages: View {
#State private var currentLanguage = true
#State private var currentLanguageEnglish = true
#State private var currentLanguageRussian = false
#State private var showingAlert = false
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#State var currentSysLanguage = UserDefaults.standard.string(forKey: "language")
var body: some View {
VStack(alignment: .leading) {
DoubleTextView(topText: LocalizedStringKey("languages"), buttomText: "", topTextSize: 24, buttomTextSize: 0)
// Works just right, wrong design.
Button("English", action: {
currentSysLanguage = "en"
UserDefaults.standard.set(currentSysLanguage, forKey: "language")
showingAlert.toggle()
})
.alert("Restart your app", isPresented: $showingAlert) {
Button("OK", role: .cancel) { }
}
Button("French", action: {
currentSysLanguage = "fr"
UserDefaults.standard.set(currentSysLanguage, forKey: "language")
showingAlert.toggle()
})
.alert("Restart your app", isPresented: $showingAlert) {
Button("OK", role: .cancel) { }
}
// Does not work, this is how it should be.
ZStack(alignment: .top) {
Toggle(isOn: $currentLanguageEnglish) {
Text("English")
.font(.custom("Manrope-Bold", size: 16))
.foregroundColor(.white)
}
.padding(.horizontal)
.tint(Color("active"))
}
.padding(.vertical, 30.0)
.background(Rectangle()
.fill(Color("navigation"))
.frame(height: 50)
.cornerRadius(8))
ZStack(alignment: .top) {
Toggle(isOn: $currentLanguageRussian) {
Text("French")
.font(.custom("Manrope-Bold", size: 16))
.foregroundColor(.white)
}
.padding(.horizontal)
.tint(Color("active"))
}
.background(Rectangle()
.fill(Color("navigation"))
.frame(height: 50)
.cornerRadius(8))
Spacer()
}
.background(
Image("background2")
.resizable()
.scaledToFill()
.edgesIgnoringSafeArea(.all)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
)
.onChange(of: currentLanguageEnglish, perform: { newValue in
currentLanguageRussian = true
})
.onChange(of: currentLanguageRussian, perform: { newValue in
currentLanguageEnglish = true
})
.padding(.top, 40)
.overlay {
HStack {
Spacer()
Button {
self.mode.wrappedValue.dismiss()
} label: {
HStack {
Image(systemName: "arrow.left")
.foregroundColor(.white)
.font(.system(size: 24))
Spacer()
}
}
}
.padding(.horizontal, 3.0)
.frame(maxHeight: .infinity, alignment: .top)
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
.padding(.horizontal, 10.0)
}
}
There are many ways you can accomplish something similar, one is to make the toggle bind to a custom binding object:
Toggle(isOn: .init(get: {
currentSysLanguage == "en"
}, set: { isOn in
currentSysLanguage = isOn ? "en" : defaultLanguage
})) {
Text("English")
}
Here the getter makes the toggle listen to currentSysLanguage == "en". It will be on/off based on if the statement evaluates true. Whereas the setter will be triggered when you manually toggle it, and set currentSysLanguage between "en" and defaultLanguage.
Note I have added the defaultLanguage to represent the state when all toggles are off.
You can then create as many toggles as needed with the same code by changing the language code it is comparing to.
Near the end, you would add a .onChange modifier to listen to currentSysLanguage to send the changes to UserDefaults:
.onChange(of: currentSysLanguage, perform: { newValue in
UserDefaults.standard.set(newValue, forKey: "language")
})

SwiftUI Button content uses its own animation where it shouldn't

I have a custom transition to present/dismiss a custom sheet. My problem is that the button content is using its own animation, where it should just follow the rest:
See how the "OK" button is jumping to the bottom in the dismiss animation. It should just be following the rest of the sheet.
Full code:
import SwiftUI
#main
struct SwiftUITestsApp: App {
var body: some Scene {
WindowGroup {
SheetButtonAnimationTestView()
}
}
}
struct SheetButtonAnimationTestView: View {
#State private var sheetPresented = false
var body: some View {
ZStack(alignment: .bottom) {
Button("Present sheet", action: { sheetPresented = true })
.frame(maxWidth: .infinity, maxHeight: .infinity)
if sheetPresented {
Sheet(title: "Sheet", dismiss: { sheetPresented = false })
.transition(.move(edge: .bottom))
}
}
.animation(.easeOut(duration: 2), value: sheetPresented)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white.edgesIgnoringSafeArea(.all))
}
}
struct Sheet: View {
var title: String
var dismiss: () -> ()
var body: some View {
VStack(spacing: 16) {
HStack {
Text(title)
.foregroundColor(.white)
Spacer()
Button(action: dismiss) {
Text("OK").font(.headline.bold()).foregroundColor(.blue)
.padding(10)
.background(Capsule().fill(Color.white))
}
}
Text("This is the sheet content")
.foregroundColor(.white)
.frame(height: 300)
.frame(maxWidth: .infinity)
}
.padding(24)
.frame(maxWidth: .infinity)
.background(
Rectangle().fill(Color.black).edgesIgnoringSafeArea(.bottom)
)
.ignoresSafeArea(.container, edges: .bottom) // allows safe area for keyboard
}
}
How to make the button follow the sheet animation?
Tested on iOS 16.0.3, iPhone 11, XCode 14.1
Seems like a bug in the library. You can add a slight delay in the button action, which will solve the issue:
Button(action: {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) {
dismiss()
}
}) {
Text("OK").font(.headline.bold()).foregroundColor(.blue)
.padding(10)
.background(Capsule().fill(Color.white))
}
Or if you don't want the click animation, you can remove the button and do the following:
Text("OK").font(.headline.bold()).foregroundColor(.blue)
.padding(10)
.background(Capsule().fill(Color.white))
.onTapGesture {
dismiss()
}

SwiftUI button glitches on keyboard dismiss

I have a button on my view that, when pressed, calls a hideKeyboard function which is the following:
func hideKeyboard() {
let resign = #selector(UIResponder.resignFirstResponder)
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
}
However, when the button is pressed and the keyboard gets dismissed, a glitch occurs where the button stays in place while the view moves downward:
https://giphy.com/gifs/Z7qCDpRSGoOb9CbVRQ
Reproducible example:
import SwiftUI
struct TestView: View {
#State private var username: String = ""
#State private var password: String = ""
#State private var isAnimating: Bool = false
var lightGrey = Color(red: 239.0/255.0,
green: 243.0/255.0,
blue: 244.0/255.0)
var body: some View {
ZStack() {
VStack() {
Spacer()
Text("Text")
.font(.title)
.fontWeight(.semibold)
.padding(.bottom, 15)
.frame(maxWidth: .infinity, alignment: .leading)
Text("Text")
.font(.subheadline)
.padding(.bottom)
.frame(maxWidth: .infinity, alignment: .leading)
TextField("username", text: $username)
.padding()
.background(self.lightGrey)
.cornerRadius(5.0)
.padding(.bottom, 20)
SecureField("password", text: $password)
.padding()
.background(self.lightGrey)
.cornerRadius(5.0)
.padding(.bottom, 20)
Button(action: {
self.hideKeyboard()
login()
})
{
if isAnimating {
ProgressView()
.colorScheme(.dark)
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.green)
.cornerRadius(10.0)
.padding(.bottom, 20)
}
else {
Text("Text")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.green)
.cornerRadius(10.0)
.padding(.bottom, 20)
}
}
.disabled(username.isEmpty || password.isEmpty || isAnimating)
Text("Text")
.font(.footnote)
.frame(maxWidth: .infinity, alignment:.leading)
Spacer()
}
.padding()
.padding(.bottom, 150)
.background(Color.white)
}
}
func hideKeyboard() {
let resign = #selector(UIResponder.resignFirstResponder)
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
}
func login() {
isAnimating = true
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
This is due to view replacement inside button, find below a fixed re-layout.
Tested with Xcode 13.2 / iOS 15.2 (slow animation was activated for demo)
Button(action: {
self.hideKeyboard()
login()
})
{
VStack { // << persistent container !!
if isAnimating {
ProgressView()
.colorScheme(.dark)
}
else {
Text("Text")
}
}
.foregroundColor(.white)
.font(.headline)
.padding()
.frame(maxWidth: .infinity)
.background(Color.green)
.cornerRadius(10.0)
.padding(.bottom, 20)
}
.disabled(username.isEmpty || password.isEmpty || isAnimating)
Looks like a conflict between UIApplication.shared.sendAction and SwiftUI: hiding the keyboard starts the animation, but updating the isAnimating property resets it.
Changing the order of calls solves the problem:
Button(action: {
login()
self.hideKeyboard()
})

How to apply a different type of transition to a view in SwiftUI, depending on a conditional

In my current project I have transitions applied to my authentication views. However, on sign in, I want a different transition to appear. (Different than if the user simply clicked the "back" button)
Here is some code for a basic mock up I made:
Auth View:
struct AuthView: View {
#EnvironmentObject var testModel: TestModel //Not used yet, used later
#State private var showSignIn: Bool = false
#State private var showSignUp: Bool = false
var body: some View {
if (!showSignIn && !showSignUp) {
WelcomeView(showSignIn: $showSignIn, showSignUp: $showSignUp)
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: .leading), removal: AnyTransition.move(edge: .leading)))
} else if showSignIn {
SignInView(showSignIn: $showSignIn)
.transition(AnyTransition.move(edge: .trailing))
} else {
SignUpView(showSignUp: $showSignUp)
.transition(AnyTransition.move(edge: .trailing))
}
}
}
Welcome View code:
struct WelcomeView: View {
#Binding var showSignIn: Bool
#Binding var showSignUp: Bool
var body: some View {
VStack {
Text("Welcome!")
Button(action: { withAnimation { showSignIn.toggle() } }) {
Text("Sign In")
.underline()
.foregroundColor(.white)
}
Button(action: { withAnimation { showSignUp.toggle() } }) {
Text("Sign Up")
.underline()
.foregroundColor(.white)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue.ignoresSafeArea())
}
}
Sign in view:
struct SignInView: View {
#EnvironmentObject var testModel: TestModel
#Binding var showSignIn: Bool
var body: some View {
VStack {
Text("Sign In")
Button(action: { withAnimation { showSignIn.toggle() } }) {
Text("Go back")
.underline()
.foregroundColor(.white)
}
Button(action: { withAnimation { testModel.signedIn.toggle() } }) {
Text("Complete Sign In")
.underline()
.foregroundColor(.white)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green.ignoresSafeArea())
}
}
ContentView:
struct ContentView: View {
#StateObject var testModel = TestModel()
var body: some View {
if testModel.signedIn {
Text("Home page")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
.transition(AnyTransition.opacity)
.onTapGesture {
testModel.signedIn.toggle()
}
} else {
AuthView()
.environmentObject(testModel)
}
}
}
What it makes:
What I want:
I'm trying to apply a different transition effect when I "finish" the sign in process. In my mock up, I simulated this through clicking "Complete sign in"
I tried changing my SignInView code (in my AuthView) to this, applying a ternary operator to the transitions:
SignInView(showSignIn: $showSignIn)
.transition(AnyTransition.move(edge: (testModel.signedIn ? .bottom : .trailing)))
But it had no effect.
Thanks for any help in advance!
I solved my own question!
I made a new #State variable, with a type of AnyTransition, using that variable in replacement of my ternary operator. I then used logic to change the type of transition (the #State variable) when I run my log-in function.

How to align the an image on top of a button in swiftui?

I wish to add a 'trash' image on the top-right side of each button when 'Delete Button' is pressed, so that when user hits the trash image, the button will be removed from the vstack.
I think I should use zstack to position the trash image but I don't know how for now.
Below shows where the trash image should be located in each button.
Also, when I press the 'Delete Button', it seems that each button's text size and spacing with another button is changed slightly. How do I overcome this problem? The button position, spacing, textsize should be unchanged when 'Delete Button' is hit.
struct someButton: View {
#Environment(\.editMode) var mode
#ObservedObject var someData = SomeData()
#State var newButtonTitle = ""
#State var isEdit = false
var body: some View {
NavigationView{
// List{ // VStack
VStack{
VStack{
ForEach(Array(someData.buttonTitles.keys.enumerated()), id: \.element){ ind, buttonKeyName in
//
Button(action: {
self.someData.buttonTitles[buttonKeyName] = !self.someData.buttonTitles[buttonKeyName]!
print("Button pressed! buttonKeyName is: \(buttonKeyName) Index is \(ind)")
print("bool is \(self.someData.buttonTitles[buttonKeyName]!)")
}) {
HStack{ //HStack, ZStack
if self.isEdit{
Image(systemName: "trash")
.foregroundColor(.red)
.onTapGesture{
print("buttonkey \(buttonKeyName) will be deleted")
self.deleteItem(ind: ind)
}
}
Text(buttonKeyName)
// .fontWeight(.semibold)
// .font(.title)
}
}
.buttonStyle(GradientBackgroundStyle(isTapped: self.someData.buttonTitles[buttonKeyName]!))
.padding(.bottom, 20)
}
}
HStack{
TextField("Enter new button name", text: $newButtonTitle){
self.someData.buttonTitles[self.newButtonTitle] = false
self.newButtonTitle = ""
}
}
}
.navigationBarItems(leading: Button(action: {self.isEdit.toggle()}){Text("Delete Button")},
trailing: EditButton())
// .navigationBarItems(leading: Button(action: {}){Text("ergheh")})
// }
}
}
func deleteItem(ind: Int) {
let key = Array(someData.buttonTitles.keys)[ind]
print(" deleting ind \(ind), key: \(key)")
self.someData.buttonTitles.removeValue(forKey: key)
}
}
struct GradientBackgroundStyle: ButtonStyle {
var isTapped: Bool
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.frame(maxWidth: .infinity, maxHeight: 50)
.padding()
.foregroundColor(isTapped ? Color.blue : Color.black)
.background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(40)
.overlay(RoundedRectangle(cornerRadius: 40)
.stroke(isTapped ? Color.blue : Color.black, lineWidth: 4))
.shadow(radius: 40)
.padding(.horizontal, 20)
.scaleEffect(configuration.isPressed ? 0.9 : 1.0)
//
}
}
class SomeData: ObservableObject{
#Published var buttonTitles: [String: Bool] = ["tag1": false, "tag2": false]
}
Here is a demo of possible approach. Tested with Xcode 11.4 / iOS 13.4 (with some replicated code)
var body: some View {
Button(action: { }) {
Text("Name")
}
.buttonStyle(GradientBackgroundStyle(isTapped: tapped))
.overlay(Group {
if self.isEdit {
ZStack {
Button(action: {print(">> Trash Tapped")}) {
Image(systemName: "trash")
.foregroundColor(.red).font(.title)
}.padding(.trailing, 40)
.alignmentGuide(.top) { $0[.bottom] }
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
}
})
.padding(.bottom, 20)
}