NavigationLink doesn't work with ScrollView inside a List in SwiftUI - ios

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)
}
}

Related

How to navigate to specific page in tab view using ForEach in SwiftUI?

I want to create some simple navigation, where when I press on a button from a list, I am taken to a specific page on a page style tab view.
My code is below:
struct NavView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: PageView(tag: 0)) {
Text("0")
}
NavigationLink(destination: PageView(tag: 1)) {
Text("1")
}
NavigationLink(destination: PageView(tag: 2)) {
Text("2")
}
NavigationLink(destination: PageView(tag: 3)) {
Text("3")
}
NavigationLink(destination: PageView(tag: 4)) {
Text("4")
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
}
struct PageView: View {
var tag: Int
var body: some View {
TabView {
ForEach(0..<5, id: \.self) { i in
Text("\(i)")
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
}
As an example, if I wanted to go to the page labeled 3, I would like to use the click on the list option labeled 3 on the NavView to direct me to that page on PageView, which is being created with a ForEach. How would I be able to accomplish this? I hope my request made sense.
You can use the selection binding of TabView like this.
struct NavView: View {
var body: some View {
NavigationView {
List {
ForEach(0..<5, id: \.self) { i in
NavigationLink(destination: PageView(tag: i)) {
Text("\(i)")
}
}
}
}
}
}
struct PageView: View {
#State var tag: Int = 0
var body: some View {
TabView(selection: $tag) {
ForEach(0..<5, id: \.self) { i in
Text("\(i)")
.tag(i)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
}

Odd animation with .matchedGeometryEffect and NavigationView

I'm working on creating a view which uses matchedGeometryEffect, embedded within a NavigationView, and when presenting the second view another NavigationView.
The animation "works" and it matches correctly, however when it toggles from view to view it happens to swing as if unwinding from the navigation stack.
However, if I comment out the NavigationView on the secondary view the matched geometry works correctly.
I am working on iOS 14.0 and above.
Sample code
Model / Mock Data
struct Model: Identifiable {
let id = UUID().uuidString
let icon: String
let title: String
let account: String
let colour: Color
}
let mockItems: [Model] = [
Model(title: "Test title 1", colour: .gray),
Model(title: "Test title 2", colour: .blue),
Model(title: "Test title 3", colour: .purple)
...
]
Card View
struct CardView: View {
let item: Model
var body: some View {
VStack {
Text(item.title)
.font(.title3)
.fontWeight(.heavy)
}
.padding()
.frame(maxWidth: .infinity, minHeight: 100, alignment: .leading)
.background(item.colour)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Secondary / Detail View
struct DetailView: View {
#Binding var isShowingDetail: Bool
let item: Model
let animation: Namespace.ID
var body: some View {
NavigationView { // <--- comment out here and it works
VStack {
CardView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.onTapGesture {
withAnimation { isShowingDetail = false }
}
ScrollView(.vertical, showsIndicators: false) {
Text("Lorem ipsum dolor...")
}
}
.padding(.horizontal)
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(.stack)
}
}
Primary View
struct ListView: View {
#State private var selectedCard: Model?
#State private var isShowingCard: Bool = false
#Namespace var animation
var body: some View {
ZStack {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
ForEach(mockItems) { item in
CardView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.onTapGesture {
withAnimation {
selectedCard = item
isShowingCard = true
}
}
}
}
.navigationTitle("Test title")
}
.padding(.horizontal)
.navigationViewStyle(.stack)
// show detail view
if let selectedCard = selectedCard, isShowingCard {
DetailView(
isShowingDetail: $isShowingCard,
item: selectedCard,
animation: animation
)
}
}
}
}
Video examples
With NavigationView in DetailView
Without NavigationView in DetailView
Ignore the list view still visible
You don't need second NavigationView (actually I don't see links at all, so necessity of the first one is also under question). Anyway we can just change the layout order and put everything into one NavigationView, like below.
Tested with Xcode 13.4 / iOS 15.5
struct ListView: View {
#State private var selectedCard: Model?
#State private var isShowingCard: Bool = false
#Namespace var animation
var body: some View {
NavigationView {
ZStack { // container !!
if let selectedCard = selectedCard, isShowingCard {
DetailView(
isShowingDetail: $isShowingCard,
item: selectedCard,
animation: animation
)
} else {
ScrollView(.vertical, showsIndicators: false) {
ForEach(mockItems) { item in
CardView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.onTapGesture {
selectedCard = item
isShowingCard = true
}
}
}
.navigationTitle("Test title")
}
}
.padding(.horizontal)
.navigationViewStyle(.stack)
.animation(.default, value: isShowingCard) // << animated here !!
}
}
}
struct DetailView: View {
#Binding var isShowingDetail: Bool
let item: Model
let animation: Namespace.ID
var body: some View {
VStack {
CardView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.onTapGesture {
isShowingDetail = false
}
ScrollView(.vertical, showsIndicators: false) {
Text("Lorem ipsum dolor...")
}
}
.padding(.horizontal)
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.stack)
}
}
Test module is here

Unwanted top padding on Swift TabView

I have an issue which I cannot resolve. I have a TabView inside NavigationView with two tabs. Both tabs are constructed in the same way SearchBar + List, only difference is content, but issue appears even if I try to display the same content on both.
Issue is that on first tab there is extra padding between search bar and list, it does not appear on second tab. If I switch order in code issue is the same padding only on first tab (old second tab).
Important remark, it only happens with .listStyle(SidebarListStyle()) with .listStyle(PlainListStyle()) there is no extra padding, but then I cannot expand and colapse categories.
Code:
var body: some View {
NavigationView {
TabView {
ProductList()
.tabItem {
Label("Produkty", systemImage: "bag.circle.fill")
}
.onAppear {
self.title = "Produkty"
}
RecipeList()
.tabItem {
Label("Przepisy", systemImage: "book.circle.fill")
}
.onAppear {
self.title = "Przepisy"
}
}
.accentColor(.white)
.navigationBarTitle(title, displayMode: .inline)
}
}
Code for the ProductList():
import SwiftUI
struct ProductList: View {
#Environment(\.presentationMode) var presentationMode
#State private var searchText = ""
let categories: [String] = ["A", "B", "C"]
var body: some View {
VStack() {
SearchBar(text: $searchText)
.padding(.top, 10)
List(){
ForEach(categories, id: \.self) { cat in
Section(
header: Text(cat)
.fontWeight(.bold)
.foregroundColor(.gray)
){
ForEach(productData.filter{
searchText.isEmpty && $0.category.rawValue == cat ? true : $0.name.localizedCaseInsensitiveContains(searchText) && $0.category.rawValue == cat
}) { product in
NavigationLink(destination: ProductDetail(product: product)){
ProductRow(
product: product,
color: colorRank(value: product.rank[0].rawValue)
)
}
}
}
.accentColor(.gray)
}
}
.resignKeyboardOnDragGesture()
.listStyle(SidebarListStyle())
}.background(Color(UIColor.secondarySystemBackground).ignoresSafeArea())
}
}

NavigationLink inside List applies to HStack instead of each element

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"))
}
}

SwiftUI How to push to next screen when tapping on Button

I can navigate to next screen by using NavigationButton (push) or present with PresentationButton (present) but i want to push when i tap on Buttton()
Button(action: {
// move to next screen
}) {
Text("See More")
}
is there a way to do it?
You can do using NavigationLink
Note: Please try in real device. in simulator sometimes not work properly.
struct MasterView: View {
#State var selection: Int? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailsView(), tag: 1, selection: $selection) {
Button("Press me") {
self.selection = 1
}
}
}
}
}
}
struct DetailsView: View {
#Environment(\.presentationMode) var presentation
var body: some View {
Group {
Button("Go Back") {
self.presentation.wrappedValue.dismiss()
}
}
}
}
As you can see to display the new view, add the NavigationLink with isActive: $pushView using <.hidden()> to hide the navigation "arrow".
Next add Text("See More") with tapGesture to make the text respond to taps. The variable pushView will change (false => true) when you click "See More" text.
import SwiftUI
struct ContentView: View {
#State var pushView = false
var body: some View {
NavigationView {
List {
HStack{
Text("test")
Spacer()
NavigationLink(destination: NewView(), isActive: $pushView) {
Text("")
}.hidden()
.navigationBarTitle(self.pushView ? "New view" : "default view")
Text("See More")
.padding(.trailing)
.foregroundColor(Color.blue)
.onTapGesture {
self.pushView.toggle()
}
}
}
}
}
}
struct NewView: View {
var body: some View {
Text("New View")
}
}
ContentView picture
NewView picture
To tap on button and navigate to next screen,You can use NavigationLink like below
NavigationView{
NavigationLink(destination: SecondView()) {
Text("Login")
.padding(.all, 5)
.frame(minWidth: 0, maxWidth: .infinity,maxHeight: 45, alignment: .center)
.foregroundColor(Color.white)
}
}
You can use NavigationLink to implement this:
struct DetailsView: View {
var body: some View {
VStack {
Text("Hello world")
}
}
}
struct ContentView: View {
#State var selection: Int? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailsView(), tag: 1, selection: $selection) {
Button("Press me") {
self.selection = 1
}
}
}
}
}
}

Resources