I'm trying to follow Composing Complex Interfaces guide on SwiftUI and having issues getting NavigationLink to work properly on iOS 13 beta 3 and now beta 4.
If you just download the project files and try running it, click on any of the Lake images - nothing will happen. However if you click on the header "Lakes" it'll start opening every single lake one after another which is not a behaviour anyone would expect.
Seems like NavigationLink is broken in "complex" interfaces. Is there a workaround?
I've tried making it less complex and removing HStack of List helps to get NavigationLinks somewhat to work but then I can't build the full interface like in example.
Relevant parts of the code:
var body: some View {
NavigationView {
List {
FeaturedLandmarks(landmarks: featured)
.scaledToFill()
.frame(height: 200)
.clipped()
.listRowInsets(EdgeInsets())
ForEach(categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: self.categories[key]!)
}
.listRowInsets(EdgeInsets())
NavigationLink(destination: LandmarkList()) {
Text("See All")
}
}
.navigationBarTitle(Text("Featured"))
.navigationBarItems(trailing: profileButton)
.sheet(isPresented: $showingProfile) {
ProfileHost()
}
}
}
struct CategoryRow: View {
var categoryName: String
var items: [Landmark]
var body: some View {
VStack(alignment: .leading) {
Text(self.categoryName)
.font(.headline)
.padding(.leading, 15)
.padding(.top, 5)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 0) {
ForEach(self.items, id: \.name) { landmark in
NavigationLink(
destination: LandmarkDetail(
landmark: landmark
)
) {
CategoryItem(landmark: landmark)
}
}
}
}
.frame(height: 185)
}
}
}
struct CategoryItem: View {
var landmark: Landmark
var body: some View {
VStack(alignment: .leading) {
landmark
.image(forSize: 155)
.renderingMode(.original)
.cornerRadius(5)
Text(landmark.name)
.foregroundColor(.primary)
.font(.caption)
}
.padding(.leading, 15)
}
}
It looks like there is a bug with NavigationLink instances that aren't directly contained in a List. If you replace the outermost List with a ScrollView and a VStack then the inner NavigationLinks work correctly:
i.e.
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: true) {
VStack {
FeaturedLandmarks(landmarks: featured)
.scaledToFill()
.frame(height: 200)
.clipped()
.listRowInsets(EdgeInsets())
ForEach(categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: self.categories[key]!)
}
.listRowInsets(EdgeInsets())
NavigationLink(destination: LandmarkList()) {
Text("See All")
}
}
}
.navigationBarTitle(Text("Featured"))
}
}
Related
Problem
First time when the button "Go to 990" is tapped the scroll view doesn't scroll to 990 on top. (see screenshot below)
The anchor is not respected.
Second time it scrolls to 990 top correctly.
Questions
What is wrong and how can it be fixed?
Versions
Xcode 14
Code
struct ContentView: View {
#State private var scrollProxy: ScrollViewProxy?
var body: some View {
NavigationStack {
ScrollViewReader { proxy in
List(0..<1000, id: \.self) { index in
VStack(alignment: .leading) {
Text("cell \(index)")
.font(.title)
Text("text 1")
.font(.subheadline)
Text("text 2")
.font(.subheadline)
}
}
.onAppear {
scrollProxy = proxy
}
}
.toolbar {
ToolbarItem {
Button("Go to 990") {
scrollProxy?.scrollTo(990, anchor: .top)
}
}
}
}
#if os(macOS)
.frame(minWidth: 500, minHeight: 300)
#endif
}
}
Screenshot
I have a complex view in List row:
var body: some View {
VStack {
VStack {
FullWidthImageView(ad)
HStack {
Text("\(self.price) \(self.ad.currency!)")
.font(.headline)
Spacer()
SwiftUI.Image(systemName: "heart")
}
.padding([.top, .leading, .trailing], 10.0)
Where FullWidthImageView is view with defined contexMenu modifier.
But when I long-press on an image I see not the only image in preview, but all row view.
There is no other contextMenu on any element.
How to make a preview in context with image only?
UPD. Here is a simple code illustrating the problem
We don't have any idea why in your case it doesn't work, until we see your FullWidthImageView and how you construct the context menu. Asperi's answer is working example, and it is correctly done! But did it really explain your trouble?
The trouble is that while applying .contextMenu modifier to only some part of your View (as in your example) we have to be careful.
Let see some example.
import SwiftUI
struct FullWidthImageView: View {
#ObservedObject var model = modelStore
var body: some View {
VStack {
Image(systemName: model.toggle ? "pencil.and.outline" : "trash")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)
}.contextMenu(ContextMenu {
Button(action: {
self.model.toggle.toggle()
}) {
HStack {
Text("toggle image to?")
Image(systemName: model.toggle ? "trash" : "pencil.and.outline")
}
}
Button("No") {}
})
}
}
class Model:ObservableObject {
#Published var toggle = false
}
let modelStore = Model()
struct ContentView: View {
#ObservedObject var model = modelStore
var body: some View {
VStack {
FullWidthImageView()
Text("Long press the image to change it").bold()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
while running, the "context menu" modified View seems to be "static"!
Yes, on long press, you see the trash image, even though it is updated properly while you dismiss the context view. On every long press you see trash only!
How to make it dynamic? I need that the image will be the same, as on my "main View!
Here we have .id modifier. Let see the difference!
First we have to update our model
class Model:ObservableObject {
#Published var toggle = false
var id: UUID {
UUID()
}
}
and next our View
FullWidthImageView().id(model.id)
Now it works as we expected.
For another example, where "standard" state / binding simply doesn't work check SwiftUI hierarchical Picker with dynamic data crashes
UPDATE
As a temporary workaround you can mimic List by ScrollView
import SwiftUI
struct Row: View {
let i:Int
var body: some View {
VStack {
Image(systemName: "trash")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)
.contextMenu(ContextMenu {
Button("A") {}
Button("B") {}
})
Text("I don’t want to show in preview because I don’t have context menu modifire").bold()
}.padding()
}
}
struct ContentView: View {
var body: some View {
VStack {
ScrollView {
ForEach(0 ..< 20) { (i) in
VStack {
Divider()
Row(i: i)
}
}
}
}
}
}
It is not optimal, but in your case it should work
Here is a code (simulated possible your scenario) that works, ie. only image is shown for context menu preview (tested with Xcode 11.3+).
struct FullWidthImageView: View {
var body: some View {
Image("auto")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)
.contextMenu(ContextMenu() {
Button("Ok") {}
})
}
}
struct TestContextMenu: View {
var body: some View {
VStack {
VStack {
FullWidthImageView()
HStack {
Text("100 $")
.font(.headline)
Spacer()
Image(systemName: "heart")
}
.padding([.top, .leading, .trailing], 10.0)
}
}
}
}
It's buried in the replies here, but the key discovery is that List is changing the behavior of .contextMenu -- it creates "blocks" that pop up with the menu instead of attaching the menu to the element specified. Switching out List for ScrollView fixes the issue.
I am trying to achieve Horizontal scroll of a scrollview inside a List. Which is working fine.
However, if I am adding NavigationLink to each ScrollView Element, it seems like NavigationView is unable to do his task.
I have added my code. Please have a look and let me know, what I am missing.
struct MovieHomeView: View {
#ObservedObject var moviesDataSource: MoviesHomeViewModel = MoviesHomeViewModel()
var body: some View {
NavigationView {
VStack {
HeaderView()
.frame(height: 50.0)
List {
MovieCategoryCell(category: "Now Playing", movies: moviesDataSource.nowPlayingMovies)
MovieCategoryCell(category: "Popular", movies: moviesDataSource.popularMovies)
}
.padding(.horizontal, -20)
}
.navigationBarTitle("Movies App")
.navigationBarHidden(true)
}
}
}
struct MovieCategoryCell: View {
var category: String
var movies: [MovieBaseProtocol]
init(category: String, movies: [MovieBaseProtocol]) {
self.category = category
self.movies = movies
}
var body: some View {
VStack(alignment: .leading) {
Text(category)
.font(.title)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(self.movies, id: \.id) { movie in
NavigationLink(destination: MovieUIView(movie: movie)) {
MovieCard(movie: movie)
}
}
}
}
.frame(height: 280)
}
.padding(.horizontal, 20)
}
}
I replace the Views with Text("1") and found everything was fine. So it must be something wrong inside your either/both of your views.
ForEach(self.movies, id: \.id) { movie in
NavigationLink(destination: Text("1")){// MovieUIView(movie: movie)) {
Text("1") // MovieCard(movie: movie)
}
}
I have tried to find a similar problem asked before, but failed.
I have a simple view with list. I am using a ForEach to show 10 iterations of the some list item to create a layout before I will add real data to this list. I have a problem with last 2 rows not rendering correctly. But sometimes it’s other row. I have tested on an iPhone too and sometimes it’s one row, sometimes another. The code for the view with list is this:
import SwiftUI
struct LocksView: View {
#State private var locksPaid = 0
var body: some View {
NavigationView {
List {
DateView()
.listRowInsets(EdgeInsets())
Picker(selection: $locksPaid, label: Text("Picker")) {
Text("All").tag(0)
Text("Not paid (2)").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.padding(10)
ForEach(0 ..< 10) {item in
LocksItemView()
}
}
.navigationBarTitle(Text("Locks"))
.navigationBarItems(trailing: EditButton())
}
}
}
The code for list items is this:
import SwiftUI
struct LocksItemView: View {
#State private var paid : Bool = false
var body: some View {
HStack {
Text("L15")
.font(.title)
.fontWeight(.heavy)
.multilineTextAlignment(.center)
.frame(width: 80)
VStack(alignment: .leading) {
Text("nickname")
.fontWeight(.bold)
Text("category")
Text("4 000 THB")
.fontWeight(.bold)
}
Spacer()
Toggle(isOn: self.$paid) {
Text("Paid")
}
.labelsHidden()
}
}
}
Why is toggle broken in some rows on my list? Why it moves to the left side?
It is not last items. If you set in your ForEach 20 instead of 10 and scroll up & down you'll see much more interesting issue.
I assume the reason, actually List bug, is the same as in this topic.
Workaround If it is not critical to you then use ScrollView instead of List, as tested there is no bug for it. Otherwise, file a Radar and wait for fix.
I tried your code at simulators first and had same issue too. But then I remembered, that there are some problems with 13.2 iOS and tried to run it on my device (iPhone 7, iOS 13.1.1) and everything works fine! I think that is the problem in 13.2 iOS, not in the List. There is sample, how I changed code for demonstration that everything is ok:
import SwiftUI
struct LocksView: View {
#State private var locksPaid = 0
var body: some View {
NavigationView {
List {
Picker(selection: $locksPaid, label: Text("Picker")) {
Text("All").tag(0)
Text("Not paid (2)").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.padding(10)
ForEach(0 ..< 200) {item in
LocksItemView(number: item)
}
}
.navigationBarTitle(Text("Locks"))
.navigationBarItems(trailing: EditButton())
}
}
}
struct LocksItemView: View {
#State private var paid : Bool = false
var number: Int
var body: some View {
HStack {
Text("L\(self.number)")
.font(.title)
.fontWeight(.heavy)
.multilineTextAlignment(.center)
.frame(width: 80)
VStack(alignment: .leading) {
Text("nickname")
.fontWeight(.bold)
Text("category")
Text("4 000 THB")
.fontWeight(.bold)
}
Spacer()
Toggle(isOn: self.$paid) {
Text("Paid")
}
.labelsHidden()
}
}
}
and on my phone the result is:
so there are bugs in 13.2 version and I hope Apple will fix them all
I'm trying to reproduce the Apple tutorial(Composing Complex Interfaces) and I have a very weird problem. My CategoryItem view is being displayed as a blue frame.
If I remove the NavigationLink which wraps it, everything works fine but with that one it doesn't.
struct CategoryRow: View {
var categoryName: String
var items: [Landmark]
var body: some View {
VStack(alignment: .leading) {
Text(self.categoryName)
.font(.headline)
.padding(.leading, 15)
.padding(.top, 5)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 0) {
ForEach(self.items) { landmark in
NavigationLink(
destination: LandmarkDetail(
landmark: landmark
)
) {
CategoryItem(landmark: landmark)
}
}
}
}.frame(height: 185)
}
}
}
NavigationLink has a blue accent color by default, just call .accentColor(Color.clear) on it
Or you could try this:
NavigationView {
NavigationLink(destination: Text("Detail view here")) {
Image("YourImage")
}
.buttonStyle(PlainButtonStyle())
}
https://www.hackingwithswift.com/quick-start/swiftui/how-to-disable-the-overlay-color-for-images-inside-button-and-navigationlink
renderingMode(.original) is what did it for me; .accentColor(Color.clear) made the image invisible (my best explanation here is because it didn't have a transparency).
NavigationView {
NavigationLink(destination: Text("Detail view here")) {
Image("YourImage")
.renderingMode(.original)
}
}
As the answer above mentioned, How to disable the overlay color for images inside Button and NavigationLink is a good write up as well.