SwiftUI - confirmationDialog has abnormal behavior when inside a LazyVStack - swift

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()
}
}
}
}
}

Related

How to make the screen change when clicking on the button in SwiftUI (NavigationView)

I want to make it so that when the button is pressed, the screen (NavigationView) changes from the current one to another. Also, I don't really understand whether this can be implemented through the ContentView.
My code (ContentView):
import SwiftUI
struct acquaintance1: View {
// Первый экран знакомства
var body: some View {
ZStack{
Button (action: {})
{
VStack{
ZStack{
VStack{
Image("scren1")
.resizable()
.overlay {
Button {
// any action
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
impactMed.impactOccurred()
} label: {
Image(systemName: "arrow.right.square.fill")
.font(.system(size: 50))
.foregroundColor(Color(.systemOrange))
.position(x: 349, y: 621)
}
}
}
}
}
}
}
}
}
// Второй экран знакомства
struct View1_1: View {
var body: some View {
NavigationLink {
View1_2()
} label: {
Text("Переход на View1_2")
}
.navigationTitle("View1_1")
}
}
// Третий экран знакомства
struct View1_2: View {
var body: some View {
Text("Последний экран")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
acquaintance1()
}
}
Also I don't understand. This must be done through the ContentView or APP structure.
What you'd like to do is use a hidden navigation link with a trigger. You can then toggle the trigger in your button to navigate.
Here is an example with your code.
struct acquaintance1: View {
#State var navigate: Bool = false
// Первый экран знакомства
var body: some View {
ZStack{
Button (action: {})
{
VStack{
ZStack{
VStack{
Image("scren1")
.resizable()
.overlay {
Button {
// any action
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
impactMed.impactOccurred()
//trigger navigation here
navigate = true
} label: {
Image(systemName: "arrow.right.square.fill")
.font(.system(size: 50))
.foregroundColor(Color(.systemOrange))
.position(x: 349, y: 621)
}
}
NavigationLink(destination: View1_1(), isActive: $navigate, label: {
Text("")
}).opacity(0.0)
}
}
}
}
}
}
}

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")
}
}
}
}

SwiftUI List onTapGesture covered NavigationLink

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
}
}
}
}

Tabbar middle button utility function in SwiftUI

I'm trying to reproduce a "Instagram" like tabBar which has a "Utility" button in the middle which doesn't necessarily belong to the tabBar eco system.
I have attached this gif to show the behaviour I am after. To describe the issue. The tab bar in the middle (Black plus) is click a ActionSheet is presented INSTEAD of switching the view.
How I would do this in UIKit is simply use the
override func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
print("Selected item")
}
Function from the UITabBarDelegate. But obviously we can't do this in SwiftUI so was looking to see if there was any ideas people have tried. My last thought would be to simply wrap it in a UIView and use it with SwiftUI but would like to avoid this and keep it native.
I have seen a write up in a custom TabBar but would like to use the TabBar provided by Apple to avoid any future discrepancies.
Thanks!
Edit: Make the question clearer.
Thanks to Aleskey for the great answer (Marked as correct). I evolved it a little bit in addition to a medium article that was written around a Modal. I found it to be a little different
Here's the jist.
A MainTabBarData which is an Observable Object
final class MainTabBarData: ObservableObject {
/// This is the index of the item that fires a custom action
let customActiontemindex: Int
let objectWillChange = PassthroughSubject<MainTabBarData, Never>()
var previousItem: Int
var itemSelected: Int {
didSet {
if itemSelected == customActiontemindex {
previousItem = oldValue
itemSelected = oldValue
isCustomItemSelected = true
}
objectWillChange.send(self)
}
}
func reset() {
itemSelected = previousItem
objectWillChange.send(self)
}
/// This is true when the user has selected the Item with the custom action
var isCustomItemSelected: Bool = false
init(initialIndex: Int = 1, customItemIndex: Int) {
self.customActiontemindex = customItemIndex
self.itemSelected = initialIndex
self.previousItem = initialIndex
}
}
And this is the TabbedView
struct TabbedView: View {
#ObservedObject private var tabData = MainTabBarData(initialIndex: 1, customItemIndex: 2)
var body: some View {
TabView(selection: $tabData.itemSelected) {
Text("First Screen")
.tabItem {
VStack {
Image(systemName: "globe")
.font(.system(size: 22))
Text("Profile")
}
}.tag(1)
Text("Second Screen")
.tabItem {
VStack {
Image(systemName: "plus.circle")
.font(.system(size: 22))
Text("Profile")
}
}.tag(2)
Text("Third Screen")
.tabItem {
VStack {
Image(systemName: "number")
.font(.system(size: 22))
Text("Profile")
}
}.tag(3)
}.actionSheet(isPresented: $tabData.isCustomItemSelected) {
ActionSheet(title: Text("SwiftUI ActionSheet"), message: Text("Action Sheet Example"),
buttons: [
.default(Text("Option 1"), action: option1),
.default(Text("Option 2"), action: option2),
.cancel(cancel)
]
)
}
}
func option1() {
tabData.reset()
// ...
}
func option2() {
tabData.reset()
// ...
}
func cancel() {
tabData.reset()
}
}
struct TabbedView_Previews: PreviewProvider {
static var previews: some View {
TabbedView()
}
}
Similar concept, just uses the power of SwiftUI and Combine.
You could introduce new #State property for storing old tag of presented tab. And perform the next method for each of your tabs .onAppear { self.oldSelectedItem = self.selectedItem } except the middle tab. The middle tab will be responsible for showing the action sheet and its method will look the following:
.onAppear {
self.shouldShowActionSheet.toggle()
self.selectedItem = self.oldSelectedItem
}
Working example:
import SwiftUI
struct ContentView: View {
#State private var selectedItem = 1
#State private var shouldShowActionSheet = false
#State private var oldSelectedItem = 1
var body: some View {
TabView (selection: $selectedItem) {
Text("Home")
.tabItem { Image(systemName: "house") }
.tag(1)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Search")
.tabItem { Image(systemName: "magnifyingglass") }
.tag(2)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Add")
.tabItem { Image(systemName: "plus.circle") }
.tag(3)
.onAppear {
self.shouldShowActionSheet.toggle()
self.selectedItem = self.oldSelectedItem
}
Text("Heart")
.tabItem { Image(systemName: "heart") }
.tag(4)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Profile")
.tabItem { Image(systemName: "person.crop.circle") }
.tag(5)
.onAppear { self.oldSelectedItem = self.selectedItem }
}
.actionSheet(isPresented: $shouldShowActionSheet) { ActionSheet(title: Text("Title"), message: Text("Message"), buttons: [.default(Text("Option 1"), action: option1), .default(Text("Option 2"), action: option2) , .cancel()]) }
}
func option1() {
// do logic 1
}
func option2() {
// do logic 2
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Previous answers did not help me so I'm pasting my complete solution.
import SwiftUI
import UIKit
enum Tab {
case map
case recorded
}
#main
struct MyApp: App {
#State private var selectedTab: Tab = .map
#Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
VStack {
switch selectedTab {
case .map:
NavigationView {
FirstView()
}
case .recorded:
NavigationView {
SecondView()
}
}
CustomTabView(selectedTab: $selectedTab)
.frame(height: 50)
}
}
}
}
struct FirstView: View {
var body: some View {
Color(.systemGray6)
.ignoresSafeArea()
.navigationTitle("First view")
}
}
struct SecondView: View {
var body: some View {
Color(.systemGray6)
.ignoresSafeArea()
.navigationTitle("second view")
}
}
struct CustomTabView: View {
#Binding var selectedTab: Tab
var body: some View {
HStack {
Spacer()
Button {
selectedTab = .map
} label: {
VStack {
Image(systemName: "map")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
Text("Map")
.font(.caption2)
}
.foregroundColor(selectedTab == .map ? .blue : .primary)
}
.frame(width: 60, height: 50)
Spacer()
Button {
} label: {
ZStack {
Circle()
.foregroundColor(.secondary)
.frame(width: 80, height: 80)
.shadow(radius: 2)
Image(systemName: "plus.circle.fill")
.resizable()
.foregroundColor(.primary)
.frame(width: 72, height: 72)
}
.offset(y: -2)
}
Spacer()
Button {
selectedTab = .recorded
} label: {
VStack {
Image(systemName: "chart.bar")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
Text("Recorded")
.font(.caption2)
}
.foregroundColor(selectedTab == .recorded ? .blue : .primary)
}
.frame(width: 60, height: 50)
Spacer()
}
}
}