SwiftUI: Conditional Context Menu Shown Unexpectedly - swiftui

In the following SwiftUI view, why does the conditional .contextMenu not work correctly?
Steps:
Long press the list item
Tap Edit
Long press the list item again
On the second long press the context menu should not appear because editMode?.wrappedValue is .active. But it does appear. How to fix that?
struct ContentView: View {
#Environment(\.editMode) private var editMode
var body: some View {
let contextMenu = ContextMenu {
Button("Do nothing", action: {})
}
VStack(alignment: .leading, spacing: 12) {
EditButton().padding()
List {
ForEach(1..<2) {i in
Text("Long press me. Editing: \((editMode?.wrappedValue == .active).description)")
.contextMenu(editMode?.wrappedValue == .active ? nil : contextMenu)
}
.onDelete(perform: { _ in })
.onMove(perform: { _, _ in })
}
Spacer()
}
}
}

Works fine with Xcode 14 / iOS 16
Here is possible workaround for older versions (it is possible to try different places for .id modifier to have appropriate, acceptable, UI feedback)
Tested with Xcode 13.4 / iOS 15.5
Text("Long press me. Editing: \((editMode?.wrappedValue == .active).description)")
.contextMenu(editMode?.wrappedValue == .active ? nil : contextMenu)
.id(editMode?.wrappedValue) // << this !!

Related

Why is my preview crashing when I try to press a button to go to another view in SwiftUI?

Using iOS 16.0 and Xcode 14.2
I am super new to SwiftUI and it honestly still hasn't really clicked yet, so this code probably is very inefficient. But basically I just want a button that when you press it, you go to another view. But when I try it like this, the preview crashes. Is there a better way to do this? And also what is causing the preview to crash? I've tried a bunch of different things and it either causes the preview to crash or the button just doesn't do anything.
Homescreen (had other buttons that were working that I redacted for clarity)
import SwiftUI
struct WordAndArrows: View {
#State private var showLibrary = false
var body: some View {
LibraryButton (action: {
self.navigateToLibraryScreen()
})
}
VStack {
if showLibrary {
LibraryView()
}
}
}
func navigateToLibraryScreen() {
self.showLibrary = true
}
}
struct WordAndArrows_Previews: PreviewProvider {
static var previews: some View {
ZStack {
GradientBackground()
WordAndArrows()
}
}
}
Library Button View
import SwiftUI
struct LibraryButton: View {
var action: () -> Void
var body: some View {
//Button("Check out my library") {
Button(action: action) {
Text("Library")
}
.padding()
.background(Color("Lime"))
.clipShape(Capsule())
.buttonStyle(SquishButtonStyle(fadeOnPress: false))
.font(Font.custom("Quicksand-Bold", size: 15))
.foregroundColor(Color("Indigo"))
.shadow(color: .gray, radius: 2.5, x: 0, y: 5)
}
}
I tried:
Making a navigatetolibraryscreen function that would be triggered when the buttn was hit
Using a navigationLink
3 Wrapping the navigation link in a Vstack
And it either caused the button to not do anything or the preview to crash

SwiftUI Constraint Issue For NavigationTitle in iOS 15

I'm having an issue with Xcode throwing a constraint message in the console every time I use a NavigationTitle.
I'd like to say that I've read through the following post and have tried adding StackedNavigationViewStyle(). This worked for iOS 14 and Xcode 12.3. Since upgrading to iOS 15 and Xcode 13 the solution stops working.
SwiftUI NavigationView navigationBarTitle LayoutConstraints issue
The message is.
[LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one
you don't want. Try this: (1) look at each constraint and try to
figure out which you don't expect; (2) find the code that added the
unwanted constraint or constraints and fix it. (
"<NSLayoutConstraint:0x28189d540 UIView:0x155f53c50.trailing == _UIBackButtonMaskView:0x155f538f0.trailing (active)>",
"<NSLayoutConstraint:0x281890af0 'Mask_Trailing_Trailing' _UIBackButtonMaskView:0x155f538f0.trailing == _UIButtonBarButton:0x155f52d00.trailing (active)>",
"<NSLayoutConstraint:0x2818912c0 'MaskEV_Leading_BIB_Trailing' H:[_UIModernBarButton:0x155f53610]-(0)-[UIView:0x155f53c50]
(active)>",
"<NSLayoutConstraint:0x281890c30 'UINav_static_button_horiz_position'
_UIModernBarButton:0x155f53610.leading == UILayoutGuide:0x2802a5ce0'UIViewLayoutMarginsGuide'.leading
(active)>",
"<NSLayoutConstraint:0x281890640 'UINavItemContentGuide-leading' H:[_UIButtonBarButton:0x155f52d00]-(6)-[UILayoutGuide:0x2802a5dc0'UINavigationBarItemContentLayoutGuide']
(active)>",
"<NSLayoutConstraint:0x28189dd60 'UINavItemContentGuide-trailing' UILayoutGuide:0x2802a5dc0'UINavigationBarItemContentLayoutGuide'.trailing
== _UINavigationBarContentView:0x155f51f30.trailing (active)>",
"<NSLayoutConstraint:0x28189b340 'UIView-Encapsulated-Layout-Width'
_UINavigationBarContentView:0x155f51f30.width == 0 (active)>",
"<NSLayoutConstraint:0x28189e300 'UIView-leftMargin-guide-constraint'
H:|-(8)-UILayoutGuide:0x2802a5ce0'UIViewLayoutMarginsGuide'
(active, names: '|':_UINavigationBarContentView:0x155f51f30 )>" )
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x28189d540 UIView:0x155f53c50.trailing ==
_UIBackButtonMaskView:0x155f538f0.trailing (active)>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints
to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView
listed in <UIKitCore/UIView.h> may also be helpful.
When adding StackedNavigationViewStyle to an iOS 15 app the Title and TabBar become inactive and the list moves underneath.
The code for this is below.
struct Tab1: View {
var body: some View {
// // Used for PacificBlue color.
UITableView.appearance().backgroundColor = .clear
// UITableView.appearance().separatorColor = UIColor(Color.white)
return NavigationView {
ZStack {
Color.pacificBlue
.edgesIgnoringSafeArea(.all)
List {
ForEach(1..<100) { index in
if #available(iOS 15.0, *) {
NavigationLink(destination: DetailView()) {
HStack {
Text("Row \(index)")
//
.listRowSeparatorTint(Color.pink)
}
.background(Color.pacificBlue)
} // NavigationLink
.listRowSeparatorTint(Color.pink)
} else {
// Fallback on earlier versions
NavigationLink(destination: DetailView()) {
Text("Row \(index)")
} // NavigationLink
} // if else
} // ForEach
.listRowBackground(Color.pacificBlue)
} // List
.listStyle(PlainListStyle())
} // ZStack
.navigationTitle("Title")
} // NavigationView
.navigationViewStyle(StackNavigationViewStyle())
} // View
}
If built on iOS 14 the expected behavior is seen.
Any help would be appreciated.
Recently, I was challenged by the same issue of NavigationBar and TabBar becoming transparent and unresponsive to scroll with IOS 15.0 exclusively, while IOS 14 and IOS 15.2 and above had normal behavior. After many hours of identifying the problem, I discovered that it caused by combination of .navigationViewStyle(.stack) and the ZStack that's used inside to provide Color as background. I have no explanation why it behaves like that, neither why the solutions work.
Option 1: Wrap the NavigationView with another ZStack
struct ContentView: View {
var body: some View {
UITableView.appearance().backgroundColor = .clear
return ZStack {
NavigationView {
ZStack {
Color.blue
.edgesIgnoringSafeArea(.all)
List {
ForEach(1...50, id:\.self) { index in
Text("Row \(index)")
}
}
}
.navigationTitle("Title")
}
.navigationViewStyle(.stack)
}
}
}
Option 2: Apply the ZStack only to IOS 15 with a ViewModifier
struct ContentView: View {
var body: some View {
UITableView.appearance().backgroundColor = .clear
return NavigationView {
ZStack {
Color.blue
.edgesIgnoringSafeArea(.all)
List {
ForEach(1...50, id:\.self) { index in
Text("Row \(index)")
}
}
}
.navigationTitle("Title")
}
.navigationViewStyle(.stack)
.modifier(NavigationModifier())
}
}
struct NavigationModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 15.0, *) {
ZStack {
content
}
} else {
content
}
}
}
I had the same problem using NavigationView and TabView together. Until iOS 14 I used the code bellow and it worked without problem:
NavigationView{
TabView(selection: $selection) {
...
}
}
However, since iOS 15, the same code started having the same navigationTitle you're having.
After a lot of time searching for a solution I found that if we change the order it works perfectly:
struct ContentView: View {
var body: some View {
TabView(selection: $selection) {
NavigationView {
Text("Example 1")
.navigationTitle("One")
}
.tabItem {
Text("One")
}
.tag("One")
NavigationView {
Text("Example 2")
.navigationTitle("Two")
}
.tabItem {
Text("Two")
}
.tag("Two")
}
}
}

Toolbar menu is closed when updates are made to UI in SwiftUI

I've noticed a problem with the toolbar menu closing when updates are made to the UI. I made a test project to verify this is the case, and the code is shown below. Here are the steps to reproduce the issue.
Open the menu
Wait 5s before an update is made to the UI (in this case the name)
The menu will automatically close
I found this problem is directly caused by the button action code: selection = String(describing: View2.self). Here are the test cases I tried to find this out.
If I comment the selection code, then the problem doesn't exist.
To test that it isn't because it's linked to the NavigationLink, I commented the NavigationLink and uncommented the selection code, but the problem still exists.
If I replace the selection code with a simple print statement, then the problem doesn't exist.
I'm using Xcode 12.5.1. Tested on iOS 14.
How can I fix this problem?
struct ContentView: View {
#State var name = "John"
#State var selection: String?
var body: some View {
NavigationView {
ZStack {
NavigationLink(destination: View2(), tag: String(describing: View2.self), selection: $selection){}
Text("Hello \(name)")
.padding()
}
.toolbar {
Menu {
Button {
selection = String(describing: View2.self)
} label: {
Text("Edit")
}
Button {
selection = String(describing: View2.self)
} label: {
Text("Help")
}
} label: {
Text("Menu")
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
name = "Hunter"
}
}
}
}
}

SwiftUI - how to detect long press on Button?

I have a Button where when it gets pressed, it performs some actions. But I would like to modify the same Button to detect a longer press, and perform a different set of processes. How do I modify this code to detect a long press?
Button(action: {
// some processes
}) {
Image(systemName: "circle")
.font(.headline)
.opacity(0.4)
}
Here is possible variant (tested with Xcode 11.2 / iSO 13.2).
Button("Demo") {
print("> tap")
}
.simultaneousGesture(LongPressGesture().onEnded { _ in
print(">> long press")
})
Daniel Wood's comment is correct in both statements. First, it is the cleanest solution. For the second, there is a straightforward workaround.
struct ContentView: View {
#State private var didLongPress = false
var body: some View {
Button("Demo") {
if self.didLongPress {
self.didLongPress = false
doLongPressThing()
} else {
doTapThing()
}
}.simultaneousGesture(
LongPressGesture().onEnded { _ in self.didLongPress = true }
)
}
}
This way you can also recognize a long press on a button and distinguish it from a simple tap gesture:
Button(action: {}) {
Text("Push")
}
.simultaneousGesture(TapGesture().onEnded { _ in
print("simultaneousGesture TapGesture")
})
.simultaneousGesture(LongPressGesture().onEnded { _ in
print("simultaneousGesture LongPressGesture")
})
As of iOS 15 you should use a Menu with a primaryAction

How to set button focus in SwiftUI?

!!! TVOS !!!
I have list of buttons and it somehow autofocus first button in that list (when view is loaded). Is there any way how to focus another button in that list using SwiftUI?
I know there is preferredFocusedView in UIKit but id like to do this in SwiftUI.
Thanks for any advice!
SwiftUI 2.0
Note: !! Partial solution !!
The following approach, as tested with Xcode 12b5 / tvOS 14 works only with stacks and does not work (in any tested combination) for List/ScrollView.
Anyway for small on-screen sets of buttons it is applicable so worth posting.
struct DemoPreferredFocusView: View {
#Namespace var ns
#Environment(\.resetFocus) var resetFocus
#AppStorage("initialButton") var initialButton: Int = 3
var body: some View {
VStack {
ForEach(0..<10, id: \.self) { i in
Button(action: {
print(">> tapped \(i)")
self.initialButton = i
}) {
Text("Button \(i)")
}
.prefersDefaultFocus(i == initialButton, in: ns)
}.focusScope(ns)
}
.onAppear {
DispatchQueue.main.async {
self.resetFocus.callAsFunction(in: ns)
}
}
}
}