TabView in sheet: "Simultaneous accesses to ..., but modification requires exclusive access" - swiftui

I have a CreateNewCard View that is opened in a sheet. In this view I have the KeyboardHelperView that should be displayed if showHelp is true. This view contains a TabView. But when I run this code I get an Simultaneous accesses to XX, but modification requires exclusive access. The weird part now is: When I change the TabView() to List it works without any problem. Is this a SwiftUI bug? Does anyone know how to fix it?
struct CreateNewCard: View {
#State var showHelp = false
var body: some View {
ZStack {
Color(.blue).edgesIgnoringSafeArea(.all)
Text("open")
.onTapGesture {
withAnimation(.spring()){
self.showHelp = true
}
}
KeyboardHelpView(show: $showHelp)
.frame(width: 300, height: 500)
.opacity(showHelp ? 1 : 0)
}
}
}
struct KeyboardHelpView: View {
#Binding var show: Bool
var steps = ["A", "B", "C", "D","E"]
var images = ["1", "2", "3", "4", "5"]
var body: some View {
ZStack(alignment: .topTrailing){
Color(.red)
TabView() {
ForEach(steps.indices) { index in
VStack(alignment: .center, spacing: 10){
Text(steps[index])
Text(images[index])
}
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
Image(systemName: "xmark.circle.fill")
.padding()
.foregroundColor(Color(.red))
.font(.system(size: 30, weight: .semibold))
.onTapGesture {
withAnimation(Animation.spring()){
self.show = false
}
}
}
.cornerRadius(20)
.background(
RoundedRectangle(cornerRadius: 20)
.shadow(color: Color.black.opacity(0.3), radius: 5, x: 5, y: 5)
)
}
}
EDIT: Okay, the next weird observations: It works on a real device without any problem. And when I comment out .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always)) it works on the simulator as well. I'm not sure whether this is now a Xcode/simulator bug or is this just a random behavior?!

Related

How can I change button status in SwiftUI?

I am trying to change the color programmatically of some buttons in SwiftUI.
The buttons are stored in a LazyVGrid. Each button is built via another view (ButtonCell).
I'm using a #State in the ButtonCell view to check the button state.
If I click on the single button, his own state changes correctly, just modifying the #State var of the ButtonCell view. If I try to do the same from the ContentView nothing is happening.
This is my whole ContentView (and ButtonCell) view struct:
struct ContentView: View {
private var gridItemLayout = [GridItem(.adaptive(minimum: 30))]
var body: some View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
ScrollView {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(0..<10) { number in
ButtonCell(value: number + 1)
}
}
}
Button(action: {
ButtonCell(value: 0, isEnabled: true)
ButtonCell(value: 1, isEnabled: true)
ButtonCell(value: 1, isEnabled: true)
}){
Rectangle()
.frame(width: 200, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("Change isEnabled state").foregroundColor(.white)
)
}
}
struct ButtonCell: View {
var value: Int
#State var isEnabled:Bool = false
var body: some View {
Button(action: {
print (value)
print (isEnabled)
isEnabled = true
}) {
Rectangle()
.foregroundColor(isEnabled ? Color.red : Color.yellow)
.frame(width: 50, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("\(value)").foregroundColor(.white)
)
}
}
}
}
How I may change the color of a button in the LazyVGrid by clicking the "Change isEnabled state" button?
You need a different approach here. Currently you try to change the State of ButtonCell from the outside. State variables should always be private and therefore should not be changed from outside. You should swap the state and parameters of ButtonCell into a ViewModel. The ViewModels then are stored in the parent View (ContentView) and then you can change the ViewModels and the child views automatically update. Here is a example for a ViewModel:
final class ButtonCellViewModel: ObservableObject {
#Published var isEnabled: Bool = false
let value: Int
init(value: Int) {
self.value = value
}
}
Then store the ViewModels in the ContentView:
struct ContentView: View {
let buttonViewModels = [ButtonCellViewModel(value: 0), ButtonCellViewModel(value: 1), ButtonCellViewModel(value: 2)]
private var gridItemLayout = [GridItem(.adaptive(minimum: 30))]
var body: some View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
ScrollView {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(0..<3) { index in
ButtonCell(viewModel: buttonViewModels[index])
}
}
}
Button(action: {
buttonViewModels[0].isEnabled.toggle()
}){
Rectangle()
.frame(width: 200, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("Change isEnabled state").foregroundColor(.white)
)
}
}
}
And implement the ObservedObject approach in ButtonCell.
struct ButtonCell: View {
#ObservedObject var viewModel: ButtonCellViewModel
var body: some View {
Button(action: {
print (viewModel.value)
print (viewModel.isEnabled)
viewModel.isEnabled = true
}) {
Rectangle()
.foregroundColor(viewModel.isEnabled ? Color.red : Color.yellow)
.frame(width: 50, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("\(viewModel.value)").foregroundColor(.white)
)
}
}
}

Change View using EmptyView fullScreenCover not working in iOS 15

Days ago, I was using:
class SplashViewModel: ObservableObject {
#Published var isActive: Bool = false
#Published var isMantinance: Bool = false
#Published var hasPreviousLogIn:Bool = false
}
struct SplashView: View {
#ObservedObject var model = SplashViewModel()
func verifyMantinance() {
model.isActive = true
}
var body: some View {
Color.init(hex: "#FFFFFF")
.overlay(
ZStack {
VStack {
EmptyView().fullScreenCover(isPresented: $model.isActive)
{ QueryView() }
GeometryReader { gp in
ZStack {
Image("logo")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(EdgeInsets(top: 35, leading: 20, bottom: 10, trailing: 20))
.frame(width: UIScreen.main.bounds.width * 0.75)
.position(x: gp.size.width / 2, y: gp.size.height / 2)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.verify()
}
}
}
}
}
}
).ignoresSafeArea(.all)
}
}
It's was working properly with iOS 14, once I updated my device to iOS 15, the redirection with EmptyView().fullScreenCover is not working anymore. Nothing happens, no error, no warning, nothing related to this process.
Someone have an idea about how I can make it work again?
this is easily fixed, just "attach"
.fullScreenCover(isPresented: $model.isActive){ QueryView() }
to the VStack.
There is no need for the EmptyView

SwiftUI TextField acts disabled in VStack inside ZStack (simulating an Alert with a TextField)

I need to make an Alert in SwiftUI that has an editable TextField in it. Currently, this isn't supported by SwiftUI (as of Xcode 11.3), so I'm looking for a work-around.
I know I can implement by wrapping the normal UIKit bits in a UIHostingController, but really want to stick with an all-SwiftUI implementation.
I've got two VStacks in a ZStack, with the front one (the one with the TextView) being hidden and disabled until until you tap the button. Take a look at this:
import SwiftUI
struct ContentView: View {
#State var isShowingEditField = false
#State var text: String = "12345"
var body: some View {
ZStack {
VStack {
Text("Value is \(self.text)")
Button(action: {
print("button")
self.isShowingEditField = true
}) {
Text("Tap To Test")
}
}
.disabled(self.isShowingEditField)
.opacity(self.isShowingEditField ? 0.25 : 1.00)
VStack(alignment: .center) {
Text("Edit the text")
TextField("", text: self.$text)
.multilineTextAlignment(.center)
.lineLimit(1)
Divider()
HStack {
Button(action: {
withAnimation {
self.isShowingEditField = false
print("completed... value is \(self.text)")
}
}) {
Text("OK")
}
}
}
.padding()
.background(Color.white)
.shadow(radius: CGFloat(1.0))
.disabled(!self.isShowingEditField)
.opacity(self.isShowingEditField ? 1.0 : 0.0)
}
}
}
This seems like it should work to me. Switching between the two VStacks works well, but the TextField is not editable.
It acts like it's disabled, but it's not. Explicitly .disabled(false) to the TextField doesn't help. Also, it should already be enabled anyway since 1) that's the default, 2) the VStack it's in is specifically being set as enabled, and 3) The OK button works normally.
Ideas/workarounds?
Thanks!
You need to force the TextField updating with some methods. Like the following:
TextField("", text: self.$text)
.multilineTextAlignment(.center)
.lineLimit(1)
.id(self.isShowingEditField)
That is really ridiculous, but the problem disappears if you comment just one line of code:
//.shadow(radius: CGFloat(1.0))
I commented it and everything works. I think you need to use ZStack somewhere in your custom alert view to avoid this.. hm, bug, maybe?
update
try some experiments. If you leave just this code in that View:
struct CustomAlertWithTextField: View {
#State var isShowingEditField = false
#State var text: String = "12345"
var body: some View {
TextField("", text: $text)
.padding()
.shadow(radius: CGFloat(1.0))
}
}
it would not work again. only if you comment .padding() or .shadow(...)
BUT if you relaunch Xcode - this code begin to work (which made me crazy). Tried it at Xcode Version 11.2 (11B52)
update 2 the working code version:
struct CustomAlertWithTextField: View {
#State var isShowingEditField = false
#State var text: String = "12345"
var body: some View {
ZStack {
VStack {
Text("Value is \(self.text)")
Button(action: {
print("button")
self.isShowingEditField = true
}) {
Text("Tap To Test")
}
}
.disabled(self.isShowingEditField)
.opacity(self.isShowingEditField ? 0.25 : 1.00)
ZStack {
Rectangle()
.fill(Color.white)
.frame(width: 300, height: 100)
.shadow(radius: 1) // moved shadow here, so it doesn't affect TextField now
VStack(alignment: .center) {
Text("Edit the text")
TextField("", text: self.$text)
.multilineTextAlignment(.center)
.lineLimit(1)
.frame(width: 298)
Divider().frame(width: 300)
HStack {
Button(action: {
withAnimation {
self.isShowingEditField = false
print("completed... value is \(self.text)")
}
}) {
Text("OK")
}
}
}
}
.padding()
.background(Color.white)
.disabled(!self.isShowingEditField)
.opacity(self.isShowingEditField ? 1.0 : 0.0)
}
}
}
after some research i solved this problem by adding .clipped() to the VStack.
For your problem, it would look like this:
VStack(alignment: .center) {
Text("Edit the text")
TextField("", text: self.$text)
.multilineTextAlignment(.center)
.lineLimit(1)
Divider()
HStack {
Button(action: {
withAnimation {
self.isShowingEditField = false
print("completed... value is \(self.text)")
}
}) {
Text("OK")
}
}
}
.padding()
.clipped() // <- here
.background(Color.white)
.shadow(radius: CGFloat(1.0))
.disabled(!self.isShowingEditField)
.opacity(self.isShowingEditField ? 1.0 : 0.0)

How to perform an action after NavigationLink is tapped?

I have a Plus button in my first view. Looks like a FAB button. I want to hide it after I tap some step wrapped in NavigationLink. So far I have something like this:
ForEach(0 ..< 12) {item in
NavigationLink(destination: TransactionsDetailsView()) {
VStack {
HStack(alignment: .top) {
Text("List item")
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.foregroundColor(.black)
Divider()
}
}
.simultaneousGesture(TapGesture().onEnded{
self.showPlusButton = false
})
.onAppear(){
self.showPlusButton = true
}
}
It works fine with single tap. But when I long press NavigationLink it doesn't work. How should I rewrite my code to include long press as well? Or maybe I should make it work different than using simultaneousGesture?
I'm using the following code. I prefer it to just NavigationLink by itself because it lets me reuse my existing ButtonStyles.
struct NavigationButton<Destination: View, Label: View>: View {
var action: () -> Void = { }
var destination: () -> Destination
var label: () -> Label
#State private var isActive: Bool = false
var body: some View {
Button(action: {
self.action()
self.isActive.toggle()
}) {
self.label()
.background(
ScrollView { // Fixes a bug where the navigation bar may become hidden on the pushed view
NavigationLink(destination: LazyDestination { self.destination() },
isActive: self.$isActive) { EmptyView() }
}
)
}
}
}
// This view lets us avoid instantiating our Destination before it has been pushed.
struct LazyDestination<Destination: View>: View {
var destination: () -> Destination
var body: some View {
self.destination()
}
}
And to use it:
var body: some View {
NavigationButton(
action: { print("tapped!") },
destination: { Text("Pushed View") },
label: { Text("Tap me") }
)
}
Yes, NavigationLink does not allow such simultaneous gestures (might be as designed, might be due to issue, whatever).
The behavior that you expect might be implemented as follows (of course if you need some chevron in the list item, you will need to add it manually)
struct TestSimultaneousGesture: View {
#State var showPlusButton = false
#State var currentTag: Int?
var body: some View {
NavigationView {
List {
ForEach(0 ..< 12) { item in
VStack {
HStack(alignment: .top) {
Text("List item")
NavigationLink(destination: Text("Details"), tag: item, selection: self.$currentTag) {
EmptyView()
}
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.foregroundColor(.black)
Divider()
}
.simultaneousGesture(TapGesture().onEnded{
print("Got Tap")
self.currentTag = item
self.showPlusButton = false
})
.simultaneousGesture(LongPressGesture().onEnded{_ in
print("Got Long Press")
self.currentTag = item
self.showPlusButton = false
})
.onAppear(){
self.showPlusButton = true
}
}
}
}
}
}
Another alternative I have tried. Not using simultaneousGesture, but an onDisappear modifier instead. Code is simple and It works. One downside is that those actions happen with a slight delay. Because first the destination view slides in and after this the actions are performed. This is why I still prefer #Asperi's answer where he added .simultaneousGesture(LongPressGesture) to my code.
ForEach(0 ..< 12) {item in
NavigationLink(destination: TransactionsDetailsView()) {
VStack {
HStack(alignment: .top) {
Text("List item")
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.foregroundColor(.black)
Divider()
}
}
.onDisappear(){
self.showPlusButton = false
}
.onAppear(){
self.showPlusButton = true
}
}
I have tried an alternative approach to solving my problem. Initially I didn't use "List" because I had a problem with part of my code. But it cause another problem: PlusButton not disappearing on next screen after tapping NavigationLink. This is why I wanted to use simultaneousGesture - after tapping a link some actions would be performed as well (here: PlusButton would be hidden). But it didn't work well.
I have tried an alternative solution. Using List (and maybe I will solve another problem later.
Here is my alternative code. simultaneousGesture is not needed at all. Chevrons are added automatically to the list. And PlusButton hides the same I wanted.
import SwiftUI
struct BookingView: View {
#State private var show_modal: Bool = false
var body: some View {
NavigationView {
ZStack {
List {
DateView()
.listRowInsets(EdgeInsets())
ForEach(0 ..< 12) {item in
NavigationLink(destination: BookingDetailsView()) {
HStack {
Text("Booking list item")
Spacer()
}
.padding()
}
}
}.navigationBarTitle(Text("Booking"))
VStack {
Spacer()
Button(action: {
print("Button Pushed")
self.show_modal = true
}) {
Image(systemName: "plus")
.font(.largeTitle)
.frame(width: 60, height: 60)
.foregroundColor(Color.white)
}.sheet(isPresented: self.$show_modal) {
BookingAddView()
}
.background(Color.blue)
.cornerRadius(30)
.padding()
.shadow(color: Color.black.opacity(0.3), radius: 3, x: 3, y: 3)
}
}
}
}
}

SwiftUI multiline text always truncating at 1 line

I am trying to create a list of options for a user to choose from. The debug preview shows the general look and feel. My problem is that passing nil to .lineLimit in MultipleChoiceOption doesn't allow the text to grow beyond 1 line. How can I correct this?
struct Card<Content: View> : View {
private let content: () -> Content
init(content: #escaping () -> Content) {
self.content = content
}
private let shadowColor = Color(red: 69 / 255, green: 81 / 255, blue: 84 / 255, opacity: 0.1)
var body: some View {
ZStack {
self.content()
.padding()
.background(RoundedRectangle(cornerRadius: 22, style: .continuous)
.foregroundColor(.white)
.shadow(color: shadowColor, radius: 10, x: 0, y: 5)
)
}
.aspectRatio(0.544, contentMode: .fit)
.padding()
}
}
struct MultipleChoiceOption : View {
var option: String
#State var isSelected: Bool
var body: some View {
ZStack {
Rectangle()
.foregroundColor(self.isSelected ? .gray : .white)
.cornerRadius(6)
.border(Color.gray, width: 1, cornerRadius: 6)
Text(self.option)
.font(.body)
.foregroundColor(self.isSelected ? .white : .black)
.padding()
.multilineTextAlignment(.leading)
.lineLimit(nil)
}
}
}
struct MultipleChoice : View {
#State var selectedIndex = 1
var options: [String] = [
"Hello World",
"How are you?",
"This is a longer test This is a longer test This is a longer test This is a longer test This is a longer test This is a longer test"
]
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack(alignment: .leading, spacing: 12) {
ForEach(self.options.indices) { i in
MultipleChoiceOption(option: self.options[i],
isSelected: i == self.selectedIndex)
.tapAction { self.selectedIndex = i }
}
}
.frame(width: geometry.size.width)
}
}
.padding()
}
}
struct MultipleChoiceCard : View {
var question: String = "Is this a question?"
var body: some View {
Card {
VStack(spacing: 30) {
Text(self.question)
MultipleChoice()
}
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
// NavigationView {
VStack {
MultipleChoiceCard()
Button(action: {
}) {
Text("Next")
.padding()
.foregroundColor(.white)
.background(Color.orange)
.cornerRadius(10)
}
}
.padding()
// .navigationBarTitle(Text("Hello"))
// }
}
}
#endif
Please see this answer for Xcode 11 GM:
https://stackoverflow.com/a/56604599/30602
Summary: add .fixedSize(horizontal: false, vertical: true) — this worked for me in my use case.
The modifier fixedSize() prevents the truncation of multiline text.
Inside HStack
Text("text").fixedSize(horizontal: false, vertical: true)
Inside VStack
Text("text").fixedSize(horizontal: true, vertical: false)
There is currently a bug in SwiftUI causing the nil lineLimit to not work.
If you MUST fix this now, you can wrap a UITextField:
https://icalvin.dev/post/403
I had the same problem and used this workaround:
Add the modifier:
.frame(idealHeight: .infinity)