Context menu in SwiftUI with nested views - swiftui

Looks like there's a problem when using the .contextMenu view modifier with nested views.
Here's sample code showing the problem:
import SwiftUI
enum SampleEnum: String, CaseIterable {
case one, two, three, four
}
struct ContentView: View {
var body: some View {
Form {
Section {
VStack {
HStack {
ForEach(SampleEnum.allCases, id:\.self) { id in
Text(id.rawValue)
.contextMenu {
Button {
print("Change country setting")
} label: {
Label("Choose Country", systemImage: "globe")
}
}
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here's the result:
So, it doesn't appear that a context menu can be performed on a single Text view since the entire section/stack is selected.
Is there any way to get the contextMenu to work on an individual Text view in such a nested layout?

Try this workaround, I changed contextMenu to Menu() and passed your Text() as its param, made each text has its own menu as you wanted and prints each ID correctly as a proof that each action is independent to each Text()
Code:
struct ContentView: View {
var body: some View {
Form {
Section {
VStack {
HStack {
ForEach(SampleEnum.allCases, id:\.self) { id in
Menu("\(Text(id.rawValue))") {
Button {
print("Change country setting")
print(id)
} label: {
Label("Choose Country", systemImage: "globe")
}
}
}
}
}
}
}
}
}

Related

Why does the SwiftUI "Form" cause my button to go to the bottom of the screen?

I want to create a view that has a form with a button below it. If I include a form and a button, the button goes to the bottom of the screen.
Without Form Element
With Form Element
Is this just a SwiftUI bug? Or am I doing something wrong?
//
// TestFile.swift
// searchparty
//
// Created by Me
//
import SwiftUI
struct TestFile: View {
var body: some View {
VStack {
Form{
Text("Hello, World!")
}
Button("Button") {
print("Button tapped!")
}
}
}
}
struct TestFile_Previews: PreviewProvider {
static var previews: some View {
TestFile()
}
}
An empty Section with a footer will do the job, although you'll want to explicitly set the font, or else the footer will change it based on the form footer environment:
struct ContentView: View {
var body: some View {
Form {
Text("Entry")
Section(footer:
HStack {
Spacer()
Button(action: {}) {
Text("My button")
.font(.system(.body))
}
Spacer()
}
) {
EmptyView()
}
}
}
}

Sidebar with different columns in SwiftUI

I'm playing with the new Sidebar that has come with SwiftUI 2 and the possibility to navigate in large screens with three columns. An example about how it works can be found here: https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-a-sidebar-for-ipados
It works fine, but I would like to go one step forward and make some options of my main menu that show the three columns but other options just two.
Here an example of some demo code.
import SwiftUI
struct ContentView: View {
var body: some View{
NavigationView{
List{
Section(header: Text("Three columns")){
NavigationLink(
destination: ItemsView(),
label: {
Label("Animals",systemImage: "tortoise")
})
NavigationLink(
destination: ItemsView(),
label: {
Label("Animals 2",systemImage: "hare")
})
}
Section(header: Text("Two columns")){
NavigationLink(
destination: Text("I want to see here a single view, without detail"),
label: {
Label("Settings",systemImage: "gear")
})
NavigationLink(
destination: Text("I want to see here a single view, without detail"),
label: {
Label("Settings 2",systemImage: "gearshape")
})
}
}
.listStyle(SidebarListStyle())
.navigationBarTitle("App Menu")
ItemsView()
DetailView(animal: "default")
}
}
}
struct ItemsView: View{
let animals = ["Dog", "Cat", "Lion", "Squirrel"]
var body: some View{
List{
ForEach(animals, id: \.self){ animal in
NavigationLink(
destination: DetailView(animal: animal)){
Text(animal)
}
}
}
.listStyle(PlainListStyle())
.navigationTitle("Animals")
}
}
struct DetailView: View{
var animal: String
var body: some View{
VStack{
Text("🐕")
.font(.title)
.padding()
Text(animal)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.sizeThatFits)
}
}
If you run the code in, for example, an iPad Pro (12,9-inch) in landscape mode, you can see the tree columns. First the App Menu (sidebar). If you click on one of the first two options (animals, and animals 2), you can see a list of animals (second column) and when you click on some animal, you reach the third column (detail view).
However, I want to have only two columns when I click on the last two options of the menu (Settings and Settings 2). Any clue how to achieve it?
I've tried to hide that section if some of the first options in menu are not selected (with the selected parameter in NavigationLink), but without luck. It seems it is not possible (or I don't know) to know which option is selected in the sidebar.
Any idea is welcome!
It took me a few days to figure it out.
Test on different iPad device & multiple tasking mode, all works as expected.(iOS14+, haven't test on iOS13)
Minimal Example:
extension UISplitViewController {
open override func viewDidLoad() {
super.viewDidLoad()
self.show(.primary) // show sidebar, this is the key, toke me days to find this...
self.showsSecondaryOnlyButton = true
}
}
struct ContentView: View {
#State var column: Int = 3
var body: some View {
switch column {
case 3: // Triple Column
NavigationView {
List {
HStack {
Text("Triple")
}
.onTapGesture {
column = 3
}
HStack {
Text("Double")
}
.onTapGesture {
column = 2
}
}
Text("Supplementary View")
Text("Detail View")
}
default: // Double Column
NavigationView {
List {
HStack {
Text("Triple")
}
.onTapGesture {
column = 3
}
HStack {
Text("Double")
}
.onTapGesture {
column = 2
}
}
Text("Supplementary View")
}
}
}
}
My another answer: set sidebar default selected item.
Combine with this two solution, I have built an 2&3 column co-exist style's app.
SwiftUI, selecting a row in a List programmatically
Here is a solution which uses a custom ViewModifier. It's working on iOS 14.2, 15.0 and 15.2. Since you are using SidebarListStyle and Label I didn't test for prior versions.
Testproject:
enum Item: Hashable {
case animals, animals2, settings, settings2
static var threeColumns = [Item.animals, .animals2]
static var twoColumns = [Item.settings, .settings2]
var title: String {
switch self {
case .animals:
return "animals"
case .animals2:
return "animals2"
case .settings:
return "settings"
case .settings2:
return "settings2"
}
}
var systemImage: String {
switch self {
case .animals:
return "tortoise"
case .animals2:
return "hare"
case .settings:
return "gear"
case .settings2:
return "gearshape"
}
}
}
struct ContentView: View {
#State var selectedItem: Item?
var body: some View {
NavigationView {
List {
Section(header: Text("Three columns")) {
ForEach(Item.threeColumns, id: \.self) { item in
NavigationLink(tag: item, selection: $selectedItem) {
ItemsView()
} label: {
Label(item.title.capitalized, systemImage: item.systemImage)
}
}
}
Section(header: Text("Two columns")) {
ForEach(Item.twoColumns, id: \.self) { item in
NavigationLink(
destination: Text("I want to see here a single view, without detail"),
label: {
Label(item.title, systemImage: item.systemImage)
})
}
}
}
.listStyle(SidebarListStyle())
.navigationBarTitle("App Menu")
}
}
}
struct ItemsView: View {
let animals = ["Dog", "Cat", "Lion", "Squirrel"]
var body: some View {
List {
ForEach(animals, id: \.self) { animal in
NavigationLink(
destination: DetailView(animal: animal)) {
Text(animal)
}
}
}
.listStyle(PlainListStyle())
.navigationTitle("Animals")
}
}
struct DetailView: View {
var animal: String
var body: some View {
VStack {
Text("🐕")
.font(.title)
.padding()
Text(animal)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.sizeThatFits)
}
}
private struct ColumnModifier: ViewModifier {
let item: Item?
func body(content: Content) -> some View {
if item == .settings || item == .settings2 {
content
EmptyView()
} else {
content
EmptyView()
DetailView(animal: "default")
}
}
}
Suppose you want to create a triple-column view (with two sidebars and a detail view). Here is an example to show Projects in the first column, Files in second column and File Content in the last column.
The core step is to add three View()s in NavagationView { ... }.
import SwiftUI
struct MainView: View {
var body: some View {
ProjectsSidebar()
}
}
struct ProjectsSidebar: View {
var body: some View {
NavigationView {
List {
ForEach([1, 2, 3], id: \.self) { project_id in
VStack {
NavigationLink {
FilesSidebar(project_id: project_id)
.navigationTitle("Project \(project_id)")
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.columns)
} label: {
Text("Project \(project_id)")
}
}
}
}
.listStyle(.sidebar)
.navigationTitle("Projects")
.navigationBarTitleDisplayMode(.inline)
FilesSidebar.DefaultView()
DetailView.DefaultView()
}
.navigationViewStyle(.columns)
}
}
struct FilesSidebar: View {
var project_id: Int
var body: some View {
List {
ForEach([1, 2, 3, 4], id: \.self) { file_id in
NavigationLink {
DetailView(project_id: project_id, file_id: file_id)
.navigationTitle("File")
.navigationBarTitleDisplayMode(.inline)
} label: {
Text("File \(file_id)")
}
}
}
.listStyle(.sidebar)
}
struct DefaultView: View {
var body: some View {
VStack {
Text("Please select a project.")
}
}
}
}
struct DetailView: View {
var project_id: Int
var file_id: Int
var body: some View {
VStack {
Text("Project \(project_id) - File \(file_id)")
}
}
struct DefaultView: View {
var body: some View {
VStack {
Text("Please select a file.")
}
}
}
}
first launch:
triple columns:
select project and file:

How to display some text when a button is clicked in Swift UI?

I have a function that displays "correct" when the user puts in the right answer.
func displayCorrectOrIncorrect() -> some View {
Group {
if correctAnswer == quiz.answer {
VStack {
Text("Correct")
}
} else {
VStack {
Text("Incorrect, correct answer is \(correctAnswer)")
}
}
}
}
This is how I call it:
Button(action: {
self.displayCorrectOrIncorrect()
self.updateUI()
})
The text is not showing up. How can I fix this?
You are attempting to return a view inside the action closure of your button. This closure merely specifies the action to be done, and does not render any views.
I would suggest going through Apple's SwiftUI tutorial.
Here is an example of what you asked...
struct ContentView: View {
#State var correctAnswer: Bool = false
var body: some View {
VStack {
Group {
if correctAnswer {
Text("Correct!")
} else {
Text("Incorrect!")
}
}
Button("Button") {
correctAnswer.toggle()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to disable NavigationView push and pop animations

Given this simple NavigationView:
struct ContentView : View {
var body: some View {
NavigationView {
VStack {
NavigationLink("Push Me", destination: Text("PUSHED VIEW"))
}
}
}
}
Did anyone find a way of disabling the NavigationView animation when a destination view is pushed/popped into/from the stack?
This has been possible in UIKit since iOS2.0! I think it is not too much to ask from the framework. I tried all sorts of modifiers on all views (i.e., the NavigationView container, the destination view, the NavigationLink, etc)
These are some of the modifiers I tried:
.animation(nil)
.transition(.identity)
.transaction { t in t.disablesAnimations = true }
.transaction { t in t.animation = nil }
None made a difference. I did not find anything useful in the EnvironmentValues either :-(
Am I missing something very obvious, or is the functionality just not there yet?
Xcode 11.3:
Right now there is no modifier to disable NavigationView animations.
You can use your struct init() to disable animations, as below:
struct ContentView : View {
init(){
UINavigationBar.setAnimationsEnabled(false)
}
var body: some View {
NavigationView {
VStack {
NavigationLink("Push Me", destination: Text("PUSHED VIEW"))
}
}
}
}
First you need state for the NavigationLink to respond to, then set that state inside a transaction with animations disabled, as follows:
struct ContentView : View {
#State var isActive = false
var body: some View {
NavigationView {
VStack {
NavigationLink(isActive: $isActive, destination: {
Text("PUSHED VIEW")}) {
Text("Push Me")
}
Button("Navigate Without Animation") {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
isActive = true
}
}
}
}
}
}
I recently created an open source project called swiftui-navigation-stack (https://github.com/biobeats/swiftui-navigation-stack) that contains the NavigationStackView, a view that mimics the navigation behaviours of the standard NavigationView adding some useful features. For example, you could use the NavigationStackView and disable the transition animations as requested by Kontiki in the question. When you create the NavigationStackView just specify .none as transitionType:
struct ContentView : View {
var body: some View {
NavigationStackView(transitionType: .none) {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
PushView(destination: View2()) {
Text("PUSH")
}
}
}
}
}
struct View2: View {
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.all)
PopView {
Text("POP")
}
}
}
}
PushView and PopView are two views that allow you push and pop views (similar to the SwiftUI NavigationLink). Here is the complete example:
import SwiftUI
import NavigationStack
struct ContentView : View {
var body: some View {
NavigationStackView(transitionType: .none) {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
PushView(destination: View2()) {
Text("PUSH")
}
}
}
}
}
struct View2: View {
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.all)
PopView {
Text("POP")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The result is:
It would be great if you guys joined me in improving this open source project.

How to go to another view with button click

I have a button in my code and I have a file called LogindView.swift
I cannot get the code to open another view file when clicking on the button.
Can anybody give me an example on how to do it.
In my button action I have tried to write LogindView() but i just gives me a warning.
"Result of 'LogindView' initializer is unused"
Button(action: {
// Do action
LogindView()
}, label: {
//** Label text
Text("Logind")
.font(.headline)
.padding(.all)
.foregroundColor(Color.white)
})
.background(Color.blue)
You essentially have 3 options to transition between views depending on your needs.
First, you can use a NavigationView. This will provide a back button and will allow the user to go back. Note that there are some bugs currently when you don't put the NavigationLink inside of a List as per https://stackoverflow.com/a/57122621/3179416
import SwiftUI
struct MasterView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: LoginView()) {
Text("Login")
}
}
.navigationBarTitle(Text("Master"))
}
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
Second, you can present a modal using .sheet. This will present a modal that appears on top of the current view but it can be dismissed by the user by dragging it down.
import SwiftUI
struct MasterView: View {
#State var isModal: Bool = false
var body: some View {
Button("Login") {
self.isModal = true
}.sheet(isPresented: $isModal, content: {
LoginView()
})
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
Third, you can just use an if statement to change the current view to your Login View like so
import SwiftUI
struct MasterView: View {
#State var showLoginView: Bool = false
var body: some View {
VStack {
if showLoginView {
LoginView()
} else {
Button("Login") {
self.showLoginView = true
}
}
}
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
If you would like to animate this, so that the transition doesn't appear so abruptly, you can also do this:
import SwiftUI
struct MasterView: View {
#State var showLoginView: Bool = false
var body: some View {
VStack {
if showLoginView {
LoginView()
.animation(.spring())
.transition(.slide)
} else {
Button("Login") {
withAnimation {
self.showLoginView = true
}
}.animation(.none)
}
}
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
You can use navigation link instead button
var body: some View {
VStack {
Text("Title")
.font(.headline)
Image("myimage").clipShape(Circle())
Text("mytext").font(.title)
NavigationLink(destination: AnotherView()) {
Image(systemName: "person.circle").imageScale(.large)
}
}
}