SwiftUI List onTapGesture covered NavigationLink - swift

I want to hide keyboard when tapped the list background, but onTapGesture will cover NavigationLink. Is this a bug or have a better solution?
struct ContentView: View {
#State var text: String = ""
var body: some View {
NavigationView {
List {
NavigationLink("NextPage", destination: Text("Page"))
TextField("Placeholder", text: $text)
}
.onTapGesture {
// hide keyboard...
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
}
}
Thanks!
update
Thanks to asperi, and I found an alternative way: just put it in section header. As for style, we should create a custom ButtonStyle for NavigationLink.
Here is an example of using InsetGroupedListStyle.
struct ContentView: View {
#State var text: String = ""
var body: some View {
NavigationView {
List {
Section(
header: NavigationLink(destination: Text("Page")) {
HStack {
Text("NextPage")
.font(.body)
.foregroundColor(Color.primary)
Spacer()
Image(systemName: "chevron.forward")
.imageScale(.large)
.font(Font.caption2.bold())
.foregroundColor(Color(UIColor.tertiaryLabel))
}
.padding(.vertical, 12)
.padding(.horizontal)
}
.textCase(nil)
.buttonStyle(CellButtonStyle())
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding(.horizontal, -16)
) {}
TextField("Placeholder", text: $text)
}.listStyle(InsetGroupedListStyle())
.onTapGesture {
// hide keyboard...
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
}
}
struct CellButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(
configuration.isPressed
? Color(UIColor.systemGray5)
: Color(UIColor.secondarySystemGroupedBackground)
)
}
}

Here is a possible direction to solve this - by making all taps handled simultaneously and navigate programmatically. Tested with Xcode 12 / iOS 14.
truct ContentView: View {
#State var text: String = ""
var body: some View {
NavigationView {
List {
MyRowView()
TextField("Placeholder", text: $text)
}
.simultaneousGesture(TapGesture().onEnded {
// hide keyboard...
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
})
}
}
}
struct MyRowView: View {
#State private var isActive = false
var body: some View {
NavigationLink("NextPage", destination: Text("Page"), isActive: $isActive)
.contentShape(Rectangle())
.onTapGesture {
DispatchQueue.main.async { // maybe even with some delay
self.isActive = true
}
}
}
}

Related

SwiftUI - confirmationDialog has abnormal behavior when inside a LazyVStack

I have a ScrollView with a LazyVStack which holds n subviews.
Each subview has a button which will present a confirmation dialog, the confirmation dialog is created inside the child.
the confirmation dialog for some reason doesn't work after seeing 3 (more or less) subviews, you could press the button many times but won't immediately show the dialog, if you wait around while scrolling, suddenly every dialog will popup one after another.
video testing
Code for testing:
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack(spacing: 50) {
ForEach(0...100, id: \.self) { _ in
SubView()
}
}
}
.padding()
}
}
struct SubView: View {
#State var flag = false
var body: some View {
ZStack(alignment: .bottom) {
RoundedRectangle(cornerRadius: 30)
.frame(height: 500)
.foregroundColor(.gray)
.overlay {
Button("Press me") {
flag.toggle()
}
.confirmationDialog("", isPresented: $flag, actions: {
Button(role: .none) {
print("option 1")
} label: {
Text("option 1")
}
Button(role: .cancel) {
flag = false
} label: {
Text("cancel")
}
})
}
}
}
}
Approach
Move the confirmationDialog outside the LazyVStack
Code
struct ContentView: View {
#State private var flag = false
var body: some View {
ScrollView {
LazyVStack(spacing: 50) {
ForEach(0...100, id: \.self) { _ in
SubView(flag: $flag)
}
}
.confirmationDialog("", isPresented: $flag) {
Button(role: .none) {
print("option 1")
} label: {
Text("option 1")
}
Button(role: .cancel) {
flag = false
} label: {
Text("cancel")
}
}
}
.padding()
}
}
struct SubView: View {
#Binding var flag: Bool
var body: some View {
ZStack(alignment: .bottom) {
RoundedRectangle(cornerRadius: 30)
.frame(height: 500)
.foregroundColor(.gray)
.overlay {
Button("Press me") {
flag.toggle()
}
}
}
}
}

NavigationLink Dismisses after Textfield Value is Updated to Firestore

I'm trying to make something similar to the iOS notes app but for journaling; basically, I want there to be a list of journal entry cells users can scroll through which each display a detail view after they're clicked on where the user can view and edit their journal entry. The updating works fine, the only issue is that JournalDetailView dismisses itself after updateEntry() is called (after the user taps the "Done" button). I'm guessing this is because updateEntry() forces the view to reload, but I'm not sure how to get around this.
Here's the model:
struct JournalEntry: Identifiable, Hashable, Codable {
#DocumentID var id: String? = UUID().uuidString
#ServerTimestamp var date: Timestamp?
var text: String
var userId: String?
}
Here's the view code:
struct JournalCellView: View {
#ObservedObject var vm: JournalViewModel
#Binding var addButtonTapped: Bool
#State var showDetail = false
#State var entry: JournalEntry
var body: some View {
NavigationLink(destination: JournalDetailView(vm: vm, entry: $entry, text: entry.text), isActive: $showDetail, label: {
VStack {
HStack {
Text(entry.date!.dateValue(), style: .date)
.fontWeight(.bold)
.font(.system(size: 18))
.foregroundColor(.black)
.padding(.bottom, 3)
Spacer()
}
HStack {
Text(entry.text)
.font(.system(size: 14))
.foregroundColor(.secondary)
.lineLimit(1)
Spacer()
}
}.padding()
.background(RoundedRectangle(cornerRadius: 18).foregroundColor(.white))
.padding(.vertical, 4)
.onTapGesture {
showDetail = true
}
.onAppear {
if addButtonTapped {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
showDetail = true
}
addButtonTapped = false
}
}
})
}
}
struct JournalDetailView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var vm: JournalViewModel
#Binding var entry: JournalEntry
#State var text: String
#State var isTyping = false
var body: some View {
VStack {
HStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: { Image(systemName: "chevron.left").foregroundColor(.burgundy) })
Spacer()
if isTyping {
Button(action: {
endEditing()
updateEntry()
isTyping = false
}) {
Text("Done")
.foregroundColor(.burgundy)
}
} else {
Text("")
}
}.padding(.vertical)
Text(entry.date!.dateValue(), style: .date)
TextEditor(text: $text)
.onTapGesture {
isTyping = true
}
Spacer()
}.padding()
.navigationBarHidden(true)
}
func updateEntry() {
vm.updateJournalEntry(docID: entry.id!, date: entry.date!, text: text)
}
}
Here's updateJournalEntry():
func updateJournalEntry(docID: String, date: Timestamp, text: String) {
db.collection("journals").document(docID
).updateData(["date": date, "text": text, "userId": Auth.auth().currentUser!.uid])
}
I managed to get around this by only updating after the view would be dismissed naturally using .onDisappear and .onReceive. Not the cleanest solution, but it works. If someone has another suggestion, please contribute!
.onDisappear {
updateEntry()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
updateEntry()
}

Multiple Bottom sheets - the content doesn't load SwiftUI

I have made a view with two possible bottom sheets. The action works, and Bottom Sheets do open. Crazy thing is they open without the view inside. I have to close the one I opened and open the other one. When I do and than come back to the first one I will see the content. The code builds without warnings:
LogInView - where the logic is:
import SwiftUI
struct LogInView: View {
#EnvironmentObject var userInfo: UserInfo
enum Action{
case resetPW, signUp
}
#State private var showSheet = false
#State private var action:Action?
var body: some View {
LoginEmailView(showSheet: $showSheet, action: $action)
.sheet(isPresented: $showSheet){
if self.action == .resetPW{
ModalResetPWView()
}else if self.action == .signUp{
ModalSignUpView()
}
}
}
}
The view from which actions come:
import SwiftUI
struct LoginEmailView: View {
#EnvironmentObject var userInfo: UserInfo
#StateObject var user:LogInViewModel = LogInViewModel()
// ----- > THERE IS BINDING
#Binding var showSheet: Bool
#Binding var action:LogInView.Action?
// ----- >
var body: some View {
VStack{
Spacer()
Image("logo")
HStack{
Text("Adres email:")
.padding(.horizontal, 10)
.font(.title)
.foregroundColor(.black)
Spacer()
}
TextField("Enter e-mail adress", text: self.$user.email)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.title)
.padding(.horizontal, 10)
.keyboardType(.emailAddress)
HStack{
Text("Password:")
.padding(.horizontal, 10)
.font(.title)
.foregroundColor(.black)
Spacer()
}
SecureField("Enter password", text: self.$user.password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.title)
.padding(.horizontal,10)
HStack{
Spacer()
// ----- > First Bottom sheet
Button(action: {
self.action = .resetPW
self.showSheet = true
}) {
Text("Forgot Password")
}
.padding(.top, 5)
.padding(.trailing, 10)
// ----- >
}
Button(action: {
self.userInfo.isAuthenticated = .signedIn
}) {
Text("Log in")
}
.font(.title)
.padding(5)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.padding(.top, 10)
.opacity(user.isLogInComplete ? 1 : 0.7)
.disabled(!user.isLogInComplete)
// ----- > Second bottom sheet
Button(action: {
self.action = .signUp
self.showSheet = true
}) {
Text("Sign Up")
}
// ----- >
.padding(.top, 35)
Spacer()
}
}
}
The .sheet modifier will create the sheet view as soon as LogInView() is initialized. In your 'if.. else if..' statement, there is no logic to catch 'else' situations (situations where action == nil). Therefore, since action == nil on init(), the first .sheet that will present will fail your 'if..else if' and an EmptyView will present.
But don't worry! This is a common issue and can be easily solved. Here are 2 easy ways to implement methods to fix this (I prefer the 2nd method bc it's cleaner):
METHOD 1: Present a single view & change that view's content instead of switching between which view to present.
Instead of doing the 'if.. else if..' statement within the .sheet modifier, present a static view (I've called it SecondaryView ) that has a #Binding variable connected to your action. This way, when LogInView() appears, we can ensure that it will definitely render this view and then we can simply modify this view's content by changing the #Binding action.
import SwiftUI
struct LogInView: View {
enum Action{
case resetPW, signUp
}
#State private var showSheet = false
#State private var action: Action?
var body: some View {
LoginEmailView(showSheet: $showSheet, action: $action)
.sheet(isPresented: $showSheet) {
SecondaryView(action: $action)
}
}
}
struct LoginEmailView: View {
#Binding var showSheet: Bool
#Binding var action: LogInView.Action?
var body: some View {
VStack(spacing: 40 ){
Text("Forgot Password")
.onTapGesture {
action = .resetPW
showSheet.toggle()
}
Text("Sign Up")
.onTapGesture {
action = .signUp
showSheet.toggle()
}
}
}
}
struct SecondaryView: View {
#Binding var action: LogInView.Action?
var body: some View {
if action == .signUp {
Text("SIGN UP VIEW HERE")
} else {
Text("FORGOT PASSWORD VIEW HERE")
}
}
}
METHOD 2: Make each Button it's own View, so that it can have it's own .sheet modifier.
In SwiftUI, we are limited to 1 .sheet() modifier per View. However, we can always add Views within Views and each subview is then allowed it's own .sheet() modifier as well. So the easy solution is to make each of your buttons their own view. I prefer this method because we no longer need to pass around the #State/#Binding variables between views.
struct LogInView: View {
var body: some View {
LoginEmailView()
}
}
struct LoginEmailView: View {
var body: some View {
VStack(spacing: 40 ){
ForgotPasswordButton()
SignUpButton()
}
}
}
struct ForgotPasswordButton: View {
#State var showSheet: Bool = false
var body: some View {
Text("Forgot Password")
.onTapGesture {
showSheet.toggle()
}
.sheet(isPresented: $showSheet, content: {
Text("FORGOT PASSWORD VIEW HERE")
})
}
}
struct SignUpButton: View {
#State var showSheet: Bool = false
var body: some View {
Text("Sign Up")
.onTapGesture {
showSheet.toggle()
}
.sheet(isPresented: $showSheet, content: {
Text("SIGN UP VIEW HERE")
})
}
}

Swiftui Progress View Hidden

Hello I want to make undetermined Progress View on bar button item. when its done I want to make it hidden, but the hidden() method doesn't have parameter like disabled(Bool). how can I hide the progress view when the task getting done?
This is what I want
I don't know how to hide it programmatically on swiftui because it has no parameter.
this is the code
.navigationBarItems(leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
.foregroundColor(.orange)
})
, trailing:
//this should be hidden when the work done not always
ProgressView()
.hidden()
)
You can create that ViewExtension
extension View {
#ViewBuilder func isHidden(_ isHidden: Bool) -> some View {
if isHidden {
self.hidden()
} else {
self
}
}
}
And then dynamically hide the view:
struct ContentView : View {
#State var isHidden = false
var body : some View {
NavigationView {
VStack {
Text("Hello World")
Button(action: {
self.isHidden.toggle()
})
{
Text("Change loading")
}
}
.navigationBarItems(leading:
Button(action: {
}, label: {
Text("Cancel")
.foregroundColor(.orange)
})
, trailing:
ProgressView()
.isHidden(isHidden) //<< isHidden takes a bool whether it should be hidden
)
}
}
}
Custom reusable ProgressView - Circular
struct CustomProgressView: View {
var title: String
var total: Double = 100
#Binding var isShown: Bool
#Binding var value: Double
var body: some View {
VStack {
ProgressView(value: value, total: total) {
Text(title)
.font(.headline)
.padding()
}
.background(RoundedRectangle(cornerRadius: 25.0)
.fill(Color.white)
.overlay(RoundedRectangle(cornerRadius: 25.0)
.stroke(Color.gray, style: StrokeStyle()))
)
.progressViewStyle(CircularProgressViewStyle(tint: .muckleGreen))
.padding()
}
.padding(.top)
.isHidden(!isShown)
}
}
You can use it like this in your view
VStack {
ZStack {
CustomProgressView(title: "Adding Post", isShown: self.$viewModel.isLoading,
value: self.$viewModel.uploadPercentageComplete)
}
}
Oh my friend had another solution. It use EmptyView if the $isProgressViewShow state is false.
.navigationBarItems(leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
.foregroundColor(.orange)
})
, trailing: self.isProgressViewShow ?
AnyView(ProgressView()) : AnyView(EmptyView())
)
to hide the Progress View
self.isProgressViewShow = true

SwiftUI TextField Bug

I'm having trouble with a bug in the TextField Keyboard.
When I tap the corresponding textField, the Keyboard appears, but something like a white View appears together and the TextField is hidden. (See image)
Xcode12 Iphone11-Ios13.5 Simulator doesn't have this bug, but Ios14 does. Does anyone know a solution?
struct PlaceholderTextField: View {
var placeholderTxt: String
var keyboardType: UIKeyboardType?
#Binding var text: String
var body: some View {
ZStack(alignment: .trailing) {
VStack(alignment: .leading) {
VStack {
if self.keyboardType != nil {
TextField(self.placeholderTxt, text: $text)
.autocapitalization(.none)
.padding(20)
.keyboardType(self.keyboardType!)
} else {
TextField(self.placeholderTxt, text: $text)
.autocapitalization(.none)
.padding(20)
}
}
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 15))
.padding()
}
}
}
}
struct LoginView: View {
#ObservedObject(initialValue: LoginViewModel()) var loginController: LoginViewModel
#EnvironmentObject var userData: UserData
#State var emailLogin: Bool = true
#Environment(\.presentationMode) var presentation
#Binding var rootIsActive: Bool
var body: some View {
ZStack {
GeometryReader { bodyView in
ZStack {
Color.backgroundColor.edgesIgnoringSafeArea(.all)
VStack(spacing: 0) {
SwitchAccountIDButton(emailLogin: self.$emailLogin)
self.userIdTextField
self.passwordTextField
Button(action: {
self.userData.isLoading = true
if self.emailLogin {
self.loginController.singin(self.loginController.email, self.loginController.password, self.emailLogin)
} else {
self.loginController.singin(self.loginController.phoneNumber, self.loginController.password, self.emailLogin)
}
}) {
ButtonView(title: "ログイン", fontColor: .white, bgColor: Color.primaryColor, width: bodyView.size.width * 0.9)
.accessibility(identifier: "login_login_button")
}
NavigationLink(destination: ReissuePassword(shouldPopToRootView: self.$rootIsActive)) {
Text("パスワードを忘れた場合")
.underline()
.foregroundColor(Color.primaryColor)
.padding(.top)
}.isDetailLink(false)
Spacer()
}
}
}
.navigationBarTitle("ログイン")
.navigationBarBackButtonHidden(true)
.navigationBarItems(trailing: VStack {
Button(action: {
self.presentation.wrappedValue.dismiss()
}, label: { Text("キャンセル").foregroundColor(Color.primaryColor).fontWeight(.regular) })
})
}
}
var userIdTextField: some View {
VStack {
if self.emailLogin {
PlaceholderTextField(placeholderTxt: "メールアドレス",keyboardType: .default ,text: self.$loginController.email)
.accessibility(identifier: "login_mailaddress_textfield")
} else {
PlaceholderTextField(placeholderTxt: "電話番号", keyboardType: .phonePad, text: self.$loginController.phoneNumber)
.accessibility(identifier: "login_phonenumber_textfield")
}
}
}
}