Swift UI passing NavigationLink to a View - ios

I have a section header which includes a "VIEW ALL" button. I would like to pass the NavigationLink as a parameter so that I can use this SectionHeaderView from different places where clicking the button will result in different navigation destination.
SectionHeaderView code:
struct SectionHeaderView: View {
var title: String
var subtitle: String
var viewAllNavigationLink: NavigationLink<View, View>
var body: some View {
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 4) {
Text(subtitle)
Text(title)
}.frame(maxWidth: .infinity)
viewAllNavigationLink {
Text("View All")
}
}
.frame(maxWidth: .infinity)
}
}
But I get an error on:
var viewAllNavigationLink: NavigationLink<View, View>
which says "Type 'any View' cannot conform to 'View'"
How to solve the error here? Or Alternatively, what is the best practice to handle this case?

Related

SwiftUI List subview not showing up

I'm trying to embed a view that contains a Header view and a List The header and list render in the original view, but the list completely disappears when I put it in the parent view. I don't understand what I'm doing wrong. I tried putting the header in the list, but then the entire view disappears.
Here is the code for the view that contains the list:
import SwiftUI
struct CityDetailCategoryView: View {
let cityDetailViewModel: CityDetailViewModel
let titles = ["Category 2", "Category 3", "Category 4", "Category 5", "Category 6", "Category 7"]
var body: some View {
VStack(alignment: .leading) {
SectionHeadingView(cityDetailViewModel: CityDetailViewModel, sectionTitle: "Categories")
List {
ForEach(titles, id: \.self) { title in
Text(title)
.font(.title3)
.fontWeight(.medium)
}
}
}
}
}
struct CityDetailCategoryView_Previews: PreviewProvider {
static var previews: some View {
CityDetailCategoryView(cityDetailViewModel: CityDetailViewModel())
.previewLayout(.sizeThatFits)
}
}
...and the code of the parent view:
import SwiftUI
struct CityDetailView: View {
#ObservedObject var cityDetailViewModel: CityDetailViewModel
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false, content: {
VStack(alignment: .leading, spacing: 6, content: {
DetailHeaderImageView(cityDetailViewModel: CityDetailViewModel)
Group {
CityDetailTitleView(cityDetailViewModel: CityDetailViewModel)
CityDetailQuickFactsView(cityDetailViewModel: CityDetailViewModel)
CityDetailCategoryView(cityDetailViewModel: CityDetailViewModel)
CityDetailMiscellaneousView(cityDetailViewModel: CityDetailViewModel)
HStack {
Spacer()
Text("Data provided by ****")
.font(.caption2)
Spacer()
}
.padding()
}
.padding(.horizontal, 24)
})
})
.edgesIgnoringSafeArea(.top)
.navigationBarTitle(cityDetailViewModel.cityDetail.name, displayMode: .inline)
.navigationBarHidden(true)
}
}
}
struct CityDetailView_Previews: PreviewProvider {
static var previews: some View {
CityDetailView(cityDetailViewModel: CityDetailViewModel())
}
}
image of the view containing the list:
image of the parent view:
You're embedding a scrolling view (List) inside another scrolling view (ScrollView), which is always going to have some inherent difficulties (and potentially strange UI/UX consequences with scrolling).
Because the CityDetailCategoryView doesn't have any inherent height besides its header (the rest can be collapsed into the ScrollView), it collapses to zero.
You can use frame to set a height for it, as shown in this simplified example (if you remove the frame call, it behaves the same as yours):
var items = [1,2,3,4,5,6]
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(items, id: \.self) { item in
Text("item")
}
List {
ForEach(items, id: \.self) { item in
Text("item")
}
}.frame(height: 200) //<-- here
Text("last item")
}
}
}
That being said, I think you'll still run the risk of funny scrolling behavior.
My suggestion would be to rearchitect your views so that you have one list or scrolling view for all of the content.

Make Capsule() move when swiping between tabs - matchedGeometryEffect

I'm implementing a swiping tab view using SwiftUI's PageTabViewStyle() tabView style, similar to, say, Reddit mobile. On the top are all the tab names which turn blue, and right under a small Capsule() which moves to the selected tab. I'm having difficulty making the capsule move using matchedGeometryEffect() when the user has gone to a new tab, however.
// The parent view
#State private var selectedTab: Category = .all
// [...]
VStack(spacing: 0) {
HistoryNavigationBar(selectedTab: $selectedTab)
TabView(selection: $selectedTab) {
ForEach(Category.allCases, id: \.self) { category in
HistoryList(category: category)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
With the important parts of HistoryNavigationBar being:
#Namespace private var animation
// [...]
ForEach(History.Category.allCases, id: \.self) { category in
VStack(alignment: .leading, spacing: 5) {
Button(action: { selectedTab = category }) {
Text(category.rawValue.capitalized)
.foregroundColor(selectedTab == category ? .blue : .gray)
}
Capsule()
.frame(width: 20, height: 2)
.foregroundColor(selectedTab == category ? .blue : .clear)
}
}
How should I implement matchedGeometryEffect() to get the desired effect of the capsule moving every time the tab is changed? I tried all sorts of combinations with matchedGeometryEffect() (putting it after the capsule, putting the capsule in an if-statement, etc.), but to no success – whenever I swipe between tabs, or click on one of the tab names, the capsule just jumps to that tab.
You need to put the .matchedGeometryEffect on the capsule, but only draw the Capsule when the tab is selected (using an if-statement). You also need to add animation to the Capsule. Here is an example:
struct MGE: View {
#State var selectedTab: String = "one"
#Namespace private var namespace
enum Category: String, CaseIterable {
case one, two, three
}
var body: some View {
HStack(alignment: .top) {
ForEach(Category.allCases, id: \.self) { category in
VStack(alignment: .leading, spacing: 5) {
Button(action: { selectedTab = category.rawValue }) {
Text(category.rawValue.capitalized)
.foregroundColor(selectedTab == category.rawValue ? .blue : .gray)
}
if selectedTab == category.rawValue {
Capsule()
.frame(width: 20, height: 2)
.foregroundColor(.blue)
.matchedGeometryEffect(id: "capsule", in: namespace)
.animation(.spring())
}
}
}
}
}
}

Why SwiftUI context menu show all row view in preview?

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.

How to properly include "Add Item" button in a NavigationView using SwiftUI?

I need a "Plus" ⊕ Button in my NavigationView's List .navigationBarItems (right on the navigation bar), which I'd like to add a row to the list, using a subsequent view in the navigation hierarchy to enter its name etc.
But first, I can't even get the button to navigate correctly! When I tap it in the Preview Canvas, the button's operation seems to work. Yet in an actual app, while it does navigate to my destination View, when I tap that View's "< Back" button, the app crashes with:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Tried to pop to a view controller that doesn't exist.'
Any suggestions how I may fix the following code, please? This task is so common, surely I'm missing something and doing it wrong.
import SwiftUI
struct ListItem: Identifiable {
var id = UUID()
var name: String
}
struct ContentView: View {
var listItems = [
ListItem(name: "List Item One"),
ListItem(name: "List Item Two")
]
var body: some View {
NavigationView {
List(listItems) { listItem in
NavigationLink(destination: DetailView(existingItem: listItem)) {
Text(listItem.name)
}
}
.navigationBarTitle(Text("Configure List Items"), displayMode: .inline)
.navigationBarItems(trailing:
NavigationLink(destination: DetailView(existingItem: nil)) {
Image(systemName: "plus")
.resizable()
.padding(6)
.frame(width: 24, height: 24)
.background(Color.blue)
.clipShape(Circle())
.foregroundColor(.white)
} )
}
}
}
struct DetailView: View {
var existingItem: ListItem?
var body: some View {
Text((existingItem != nil) ? "Edit existing: \(existingItem!.name)" : "Enter NEW List Item")
}
}
Thank you! By the way, I'm on macOS Catalina 10.15.2 using: Xcode 11.3.1
NavigationLink should be inside NavigationView, not in navigation bar, so the following approach works...
#State private var addMode = false
var body: some View {
NavigationView {
VStack {
List(listItems) { listItem in
NavigationLink(destination: AddDetailView(existingItem: listItem)) {
Text(listItem.name)
}
}
.navigationBarTitle(Text("Configure List Items"), displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
// button activates link
self.addMode = true
} ) {
Image(systemName: "plus")
.resizable()
.padding(6)
.frame(width: 24, height: 24)
.background(Color.blue)
.clipShape(Circle())
.foregroundColor(.white)
} )
// invisible link inside NavigationView for add mode
NavigationLink(destination: AddDetailView(existingItem: nil),
isActive: $addMode) { EmptyView() }
}
}
}

SwiftUI View displayed with blue background

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.

Resources