I am just finishing up an iOS/watchOS app. After upgrading my watch to WatchOS8 I discovered a new glitch that was not present with WatchOS7. I have a list of regions each containing some locations and a link in each cell of the list to navigate to a list of these locations. In OS8 I find that the first time I tap a region name it navigates properly to the location list, but after that it does not. Rather it seems to store the link so that when I tab to another screen (TripView or ExcursionView in the code below) it briefly navigates to the correct location list then immediately back to the region list (not to TripView or ExcursionView). Here is the relevant part of the code.
struct ContentView: View {
var body: some View {
TabView {
WatchCurrentView()
WatchTripView(tripType:.trip)
WatchTripView(tripType:.interval)
WatchRegionView()
WatchExcursionView()
}
}
}
struct WatchRegionView: View {
#FetchRequest(fetchRequest:Region.fetch())
var regions: FetchedResults<Region>
var body: some View {
VStack{
Spacer()
.frame(height:5)
List{ForEach(regions, id:\.self){region in
ZStack(alignment: Alignment(horizontal: .leading, vertical: .center)) {
RegionCell(region:region)
.frame(height:14)
NavigationLink(
destination: WatchLocationsInRegionView(region:region),
label: {
EmptyView()
}
)
}
}
.environment(\.defaultMinListRowHeight, 0.1)
Spacer()
}
.navigationBarTitle("Regions")
}
}
}
struct RegionCell:View {
#EnvironmentObject var prefs: Prefs
var region:Region
var body: some View {
HStack{
Spacer()
.frame(width:15)
Text(region.name)
Spacer()
}
.frame(height:25)
.background(Color(prefs.colorTheme.navBarFG))
.foregroundColor(Color(prefs.colorTheme.navBarBG))
}
}
struct WatchLocationsInRegionView:View {
#State var region:Region
var body: some View {
List{ForEach(region.locations, id:\.self)
{location in
ZStack{
WatchLocationView (aLocation: InternalLocation(location: location), tripType:.trip,locationType: .list)
NavigationLink(
destination: BigWatchLocationView(aLocation: InternalLocation(location: location)),
label: {
EmptyView()
})
}
}
.font(.system(size: 14))
.navigationBarTitle("\(region.name)")
.onAppear(){prefs.watchDisplayMode = .coordinates}
}
}
}
Related
I just shared this Bug with Apple. I want to share with you.
Application Follow
1 - After the user logs on to the onBoardingView page, they are directed to ContentView with fullScreenCover.
2 - ContentView page contains objects in TabView that are repeated with ForEach. Clicking on these objects will take you to the DetailView page.
3 - However, Navigation repeats itself several times after clicking the object.
My English is bad. Sorry for this.
Video is here
Project file is here
struct OnboardView: View {
#State var isLogin: Bool = false
var body: some View {
Button(action: {self.isLogin = true}) {
Text("Login")
}
.fullScreenCover(isPresented: self.$isLogin) {
ContentView()
}
}
}
struct ContentView: View {
#State var selected: String = ""
var items: [String] = ["1","2","3","4","5","6","7","8","9","10"]
var body: some View {
NavigationView {
TabView(selection: $selected) {
ForEach(items, id: \.self) { item in
NavigationLink(
destination: DetailView(),
label: {
Text(item)
.foregroundColor(.white)
.padding()
.background(Color.orange)
.cornerRadius(10)
})
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
}
When working with ForEach in SwiftUI, you have to be extra careful on the ids.
Try changing items to items.indices instead:
ForEach(items.indices, id: \.self) { item in
NavigationLink(
destination: Text("Detail View"),
label: {
Text(items[item])
.foregroundColor(.white)
.padding()
.background(Color.orange)
.cornerRadius(10)
}
)
}
I am trying to create two NavigationLinks in a repeating List. Each has a separate destination. The code all works fine until I imbed the call to the root view in a List/ForEach loop. At which point the navigation becomes very strange.
Try to click on either link and then click the back indicator at the top. It will go to one NavigationLink, and then the other. Sometimes in a different order, and sometimes it will auto-return from one of the links, and othertimes it won't open the second detail view until you return from the first detail view. It does this both in Preview, as well as if you build and run the application.
I have distilled down the code to the most basic below. If you comment the 2 lines as indicated in ContentView, you will then see correct behavior.
I am running Catalina 10.15.5, xCode 11.6, with the application target of IOS 13.6.
How can I modify the code, so that it will work with the List/ForEach loop?
import SwiftUI
struct DetailView1: View {
var body: some View {
HStack {
Text("Here is Detail View 1." )}
.foregroundColor(.green)
}
}
struct DetailView2: View {
var body: some View {
HStack {
Text( "Here is Detail View 2.") }
.foregroundColor(.red)
}
}
struct RootView: View {
var body: some View {
HStack {
VStack {
NavigationLink(destination: DetailView1())
{ VStack { Image(systemName: "ant.circle").resizable()
.frame(width:75, height:75)
.scaledToFit()
}.buttonStyle(PlainButtonStyle())
Text("Tap for Detail 1.")
.foregroundColor(.green)
}
}
NavigationLink(destination: DetailView2())
{ Text("Tap for Detail 2.")
.foregroundColor(.red)
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
// Comment the following line for correct behavior
List { ForEach(0..<3) {_ in
RootView()
// Comment the following line for correct behavior
} }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
.navigationBarTitle("Strange Behavior")
}
}
}
In your case both navigation links are activated at once user tap a row, to avoid this below is possible approach
Tested with Xcode 12 / iOS 14
The idea is to have one link which is activated programmatically and destination is selected dynamically depending on which button is clicked
struct RootView: View {
#State private var isFirst = false
#State private var isActive = false
var body: some View {
HStack {
VStack {
Button(action: {
self.isFirst = true
self.isActive = true
})
{ VStack { Image(systemName: "ant.circle").resizable()
.frame(width:75, height:75)
.scaledToFit()
}
Text("Tap for Detail 1.")
.foregroundColor(.green)
}
}
Button(action: {
self.isFirst = false
self.isActive = true
})
{ Text("Tap for Detail 2.")
.foregroundColor(.red)
}
.background(
NavigationLink(destination: self.destination(), isActive: $isActive) { EmptyView() }
)
}
.buttonStyle(PlainButtonStyle())
}
#ViewBuilder
private func destination() -> some View {
if isFirst {
DetailView1()
} else {
DetailView2()
}
}
}
Simple SwiftUI TabView with two tabs, with a simple Picker view used on each tab.
When the app starts, the Picker is visible and updates the variable.
Select the second tab, and the Picker vanishes.
let names = ["Fred", "Wilma", "Betty", "Barney"]
struct WordPickerView: View {
#State var kind: Int = 0
var body: some View {
VStack {
Text(names[kind])
Picker(selection: $kind, label: EmptyView()) {
ForEach(0 ..< names.count) {index in
Text(names[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
}
}
}
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection){
WordPickerView()
.font(.title)
.tabItem {
VStack {
Image("first")
Text("First")
}
}
.tag(0)
WordPickerView()
.font(.title)
.tabItem {
VStack {
Image("second")
Text("Second")
}
}
.tag(1)
}
}
}
In such cases (when you have absolutely equal views) it is better to make them unique with .id.
So your case is fixed, say, with the following
Picker(selection: $kind, label: EmptyView()) {
ForEach(0 ..< names.count) {index in
Text(names[index]).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
.id(UUID().uuidString)
Why I am putting TabView into a NavigationView is because I need to hide the bottom tab bar when user goes into 2nd level 'detail' views which have their own bottom action bar.
But doing this leads to another issue: all the 1st level 'list' views hosted by TabView no longer display their titles. Below is a sample code:
import SwiftUI
enum Gender: String {
case female, male
}
let members: [Gender: [String]] = [
Gender.female: ["Emma", "Olivia", "Ava"], Gender.male: ["Liam", "Noah", "William"]
]
struct TabItem: View {
let image: String
let label: String
var body: some View {
VStack {
Image(systemName: image).imageScale(.large)
Text(label)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
ListView(gender: .female).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
ListView(gender: .male).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
}
}
}
struct ListView: View {
let gender: Gender
var body: some View {
let names = members[gender]!
return List {
ForEach(0..<names.count, id: \.self) { index in
NavigationLink(destination: DetailView(name: names[index])) {
Text(names[index])
}
}
}.navigationBarTitle(Text(gender.rawValue), displayMode: .inline)
}
}
struct DetailView: View {
let name: String
var body: some View {
ZStack {
VStack {
Text("profile views")
}
VStack {
Spacer()
HStack {
Spacer()
TabItem(image: "pencil.circle", label: "Edit")
Spacer()
TabItem(image: "minus.circle", label: "Delete")
Spacer()
}
}
}
.navigationBarTitle(Text(name), displayMode: .inline)
}
}
What I could do is to have a #State var title in the root view and pass the binding to all the list views, then have those list views to set their title back to root view on appear. But I just don't feel so right about it, is there any better way of doing this? Thanks for any help.
The idea is to join TabView selection with NavigationView content dynamically.
Demo:
Here is simplified code depicting approach (with using your views). The NavigationView and TabView just position independently in ZStack, but content of NavigationView depends on the selection of TabView (which content is just stub), thus they don't bother each other. Also in such case it becomes possible to hide/unhide TabView depending on some condition - in this case, for simplicity, presence of root list view.
struct TestTabsOverNavigation: View {
#State private var tabVisible = true
#State private var selectedTab: Int = 0
var body: some View {
ZStack(alignment: .bottom) {
contentView
tabBar
}
}
var contentView: some View {
NavigationView {
ListView(gender: selectedTab == 0 ? .female : .male)
.onAppear {
withAnimation {
self.tabVisible = true
}
}
.onDisappear {
withAnimation {
self.tabVisible = false
}
}
}
}
var tabBar: some View {
TabView(selection: $selectedTab) {
Rectangle().fill(Color.clear).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
Rectangle().fill(Color.clear).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
.frame(height: 50) // << !! might be platform dependent
.opacity(tabVisible ? 1.0 : 0.0)
}
}
This maybe a late answer, but the TabView items need to be assigned tag number else binding selection parameter won't happen. Here is how I do the same thing on my project:
#State private var selectedTab:Int = 0
private var pageTitles = ["Home", "Customers","Sales", "More"]
var body: some View {
NavigationView{
TabView(selection: $selectedTab, content:{
HomeView()
.tabItem {
Image(systemName: "house.fill")
Text(pageTitles[0])
}.tag(0)
CustomerListView()
.tabItem {
Image(systemName: "rectangle.stack.person.crop.fill")
Text(pageTitles[1])
}.tag(1)
SaleView()
.tabItem {
Image(systemName: "tag.fill")
Text(pageTitles[2])
}.tag(2)
MoreView()
.tabItem {
Image(systemName: "ellipsis.circle.fill")
Text(pageTitles[3])
}.tag(3)
})
.navigationBarTitle(Text(pageTitles[selectedTab]),displayMode:.inline)
.font(.headline)
}
}
I am trying to get a context menu to navigate to another view using the following code
var body: some View
{
VStack
{
Text(self.event.name).font(.body)
...
Spacer()
NavigationLink(destination: EditView(event: self.event))
{
Image(systemName: "pencil")
}
}
.navigationBarTitle(Text(appName))
.contextMenu
{
NavigationLink(destination: EditView(event: self.event))
{
Image(systemName: "pencil")
}
}
}
The NavigationLink within the VStack works as expected and navigates to the edit view but I want to use a contextMenu. Although the context menu displays the image, when I tap on it it doesn't navigate to the edit view, instead it just cancels the context menu.
I am doing this within a watch app but don't think that should make a difference, is there anything special I have to do with context menu navigation?
I would use the isActive variant of NavigationLink that you can trigger by setting a state variable. Apple documents this here
This variant of NavigationLink is well fit for dynamic/programatic navigation.
Your .contextMenu sets the state variable to true and that activates the NavigationLink. Because you don't want the link to be visible, set the label view to EmptyView
Here's an example, not identical to your post but hopefully makes it clear.
struct ContentView: View {
#State private var showEditView = false
var body: some View {
NavigationView {
VStack {
Text("Long Press Me")
.contextMenu {
Button(action: {
self.showEditView = true
}, label: {
HStack {
Text("Edit")
Image(systemName: "pencil")
}
})
}
NavigationLink(destination: Text("Edit Mode View Here"), isActive: $showEditView) {
EmptyView()
}
}
.navigationBarTitle("Context Menu")
}
}
}
In Xcode 11.4 it's now possible to do this with sensible NavigationLink buttons. Yay! 🎉
.contextMenu {
NavigationLink(destination: VisitEditView(visit: visit)) {
Text("Edit visit")
Image(systemName: "square.and.pencil")
}
NavigationLink(destination: SegmentsEditView(timelineItem: visit)) {
Text("Edit individual segments")
Image(systemName: "ellipsis")
}
}
This works on Xcode 11.6
struct ContentView: View {
#State var isActiveFromContextMenu = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination : detailTwo(), isActive: $isActiveFromContextMenu ){
EmptyView()
}
List{
NavigationLink(destination: detail() ){
row(isActiveFromContextMenu: $isActiveFromContextMenu)
}
NavigationLink(destination: detail() ){
row(isActiveFromContextMenu: $isActiveFromContextMenu)
}
NavigationLink(destination: detail() ){
row(isActiveFromContextMenu: $isActiveFromContextMenu)
}
}
}
}
}
}
struct detail: View {
var body: some View{
Text("Detail view")
}
}
struct detailTwo: View {
var body: some View{
Text("DetailTwo view")
}
}
struct row: View {
#Binding var isActiveFromContextMenu : Bool
var body: some View {
HStack{
Text("item")
}.contextMenu{
Button(action: {
self.isActiveFromContextMenu = true
})
{
Text("navigate to")
}
}
}
}
I found success in masking the NavigationLink in the background and switching the context with a Button as the shortest yet simplest alternative.
struct ContentView: View {
#State private var isShowing = false
var body: some View {
NavigationView {
Text("Hello")
.background(NavigationLink("", destination: Text("World!"), isActive: $isShowing))
.contextMenu {
Button {
isShowing = true
} label: {
Label("Switch to New View", systemImage: "chevron.forward")
}
}
}
}
}