SwiftUI - NavigationLink not triggered in correct location - swiftui

I am running Xcode 11 GM2 and having issues with a NavigationLink not triggering where it is expected. The app I am working on is based on Apple's sample SwiftUI app found here. The problem I am having also happens in Apple's sample.
When I add the NavigationLink to the photos:
NavigationLink(destination: PhotoDetail(photo: photo)) {
PhotoItem(photo: photo)
.frame(width: 300)
.padding(.trailing, 30)
}
The link is not accessible when you click on the image and the other content within the PhotoItem. But if you move up a bit and touch/click just below the Category name above the image, it not only triggers the link to the destination, it triggers it for all five of the items inside the scroll view.
Here is the full code from the Home page and the PhotoRow:
Home
struct HomeView: View {
// Create a categories dictionary
var categories: [String:[Photo]] {
.init(
grouping: photoData,
by: { $0.category.rawValue }
)
}
var body: some View {
NavigationView {
List(categories.keys.sorted(), id: \String.self) { key in
PhotoRow(categoryName: "\(key)".uppercased(),
photos: self.categories[key]!)
.frame(height: 320)
.padding(.top)
}
.navigationBarTitle(Text("Portfolio"))
}
}
}
PhotoRow
struct PhotoRow: View {
var categoryName: String
var photos: [Photo]
var body: some View {
VStack(alignment: .leading){
Text(self.categoryName)
.font(.title)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top) {
ForEach (self.photos) { photo in
// add navigation to detail view:
NavigationLink(destination: PhotoDetail(photo: photo)) {
PhotoItem(photo: photo)
.frame(width: 300)
.padding(.trailing, 30)
}
}
}
}
}
}
}
Any help resolving this would be appreciated.

Related

How to recreate the grid (in screenshot) in SwiftUI without breaking navigation?

I am trying to recreate a layout similar to the Reminders app. Looking at it makes me think it was built with SwiftUI. I also believe Apple mentioned so in one of the WWDC videos (can't remember which one).
This above screenshot seems to be a List, with a LazyVGrid as the first View inside the List. Tapping on each of the items in the LazyVGrid, such as Today, Scheduled, All and Flagged, navigates to the relevant screen, which means they are all NavigationLinks. Also note that the LazyVGrid has 2 columns.
And then there is another section "My Lists" which has rows which look like regular list rows in a List with style .insetGrouped. Also, every item in this Section is a NavigationItem, and thus comes with the disclosure indicator on the right as usual. Recreating this is trivial, so it has been left out from the MRE.
I am having trouble recreating the first section, which has that LazyVGrid. I faced 3 problems (as mentioned in the image), of which I have been able to solve the first one only. The other two problems remain. I want to know if this MRE can be fixed, or is my entire approach incorrect.
I am including a minimum reproducible example below.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
RemindersView()
}
}
}
struct RemindersView: View {
private var columns: [GridItem] = [GridItem(.adaptive(minimum: 150))]
private var smartLists: [SmartList] = SmartList.sampleLists
var body: some View {
NavigationView {
List {
Section(header: Text("Using LazyVGrid")) {
grid
}
Section(header: Text("Using HStack")) {
hstack
}
}
.navigationTitle("Store")
}
.preferredColorScheme(.dark)
}
private var grid: some View {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(smartLists) { smartList in
// This use of **ZStack with an EmptyView with opacity 0** is a hack being used to avoid the disclosure indicator on each item in the grid
ZStack(alignment: .leading) {
NavigationLink( destination: SmartListView(list: smartList)) {
EmptyView()
}
.opacity(0)
SmartListView(list: smartList)
}
}
}
.listRowInsets(EdgeInsets())
.listRowBackground(Color.clear)
}
private var hstack: some View {
ScrollView(.horizontal) {
HStack {
ForEach(smartLists) { smartList in
NavigationLink(destination: SmartListView(list: smartList)) {
SmartListView(list: smartList)
}
.buttonStyle(.plain)
}
}
}
.listRowInsets(EdgeInsets())
.listRowBackground(Color.clear)
}
}
struct RemindersView_Previews: PreviewProvider {
static var previews: some View {
RemindersView()
}
}
struct SmartList: Identifiable {
var id: UUID = UUID()
var title: String
var count: Int
var icon: String
var iconColor: Color
static var sampleLists: [SmartList] {
let today = SmartList(title: "Today", count: 5, icon: "20.circle.fill", iconColor: .blue)
let scheduled = SmartList(title: "Scheduled", count: 12, icon: "calendar.circle.fill", iconColor: .red)
let all = SmartList(title: "All", count: 77, icon: "tray.circle.fill", iconColor: .gray)
let flagged = SmartList(title: "Flagged", count: 5, icon: "flag.circle.fill", iconColor: .orange)
return [today, scheduled, all, flagged]
}
}
struct SmartListView: View {
var list: SmartList
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .center) {
Image(systemName: list.icon)
.renderingMode(.original)
.font(.title)
.foregroundColor(list.iconColor)
Spacer()
Text("\(list.count)")
.font(.system(.title, design: .rounded))
.fontWeight(.bold)
.padding(.horizontal, 8)
}
Text(list.title)
.font(.system(.headline, design: .rounded))
.foregroundColor(.secondary)
}
.padding(8)
.background(
RoundedRectangle(cornerRadius: 12)
.foregroundColor(.gray.opacity(0.25))
)
.padding(2)
.frame(minWidth: 150)
}
}
EDIT 1: Adding video demo of what editing the dynamic Grid looks like and how the Grid has dynamic grid items (via the Edit button at the top right): https://imgur.com/a/TV0kifY

Passing data across views for unique objects in a forEach loop in swift

I have two views, ViewAssignment and TaskDetailView. My ViewAssignment page fetches data from an environment object, and creates a list using the data.
Upon each item of the list being clicked on, the TaskDetailView pops in as a navigation link, however, I am having trouble making the information in the TaskDetailView unique to that particular iteration (the item in the list)
I believe the trouble comes from my TaskDetailView.swift
import SwiftUI
struct TaskDetailView: View {
#EnvironmentObject var assignment: Assignments
#State var taskNotes = ""
var body: some View {
VStack(spacing: 10) {
Image("english-essay")
.resizable()
.scaledToFit()
.frame(width: 250, height: 160)
.cornerRadius(20)
Text(self.assignment.data.first?.taskName ?? "Untitled Task")
.font(.title2)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.center)
HStack(spacing: 20) {
Label(self.assignment.data.first?.weighting ?? "0", systemImage: "percent")
.font(.subheadline)
.foregroundColor(.secondary)
Text(self.assignment.data.first?.dueDate ?? "No Date")
.font(.subheadline)
.foregroundColor(.secondary)
}
TextField("Write any notes here", text: $taskNotes)
.font(.body)
.padding()
Spacer()
}
}
}
struct TaskDetailView_Previews: PreviewProvider {
static var previews: some View {
TaskDetailView() // I assume there is some information I have to pass through here
}
}
For details, this is my other view:
import SwiftUI
struct ViewAssignment: View {
// Observed to update the UI
#EnvironmentObject var assignment: Assignments
var body: some View {
ZStack {
NavigationView {
List(self.assignment.data) { task in
NavigationLink (
destination: TaskDetailView(),
label: {
Image(systemName: "doc.append.fill")
.scaleEffect(2.5)
.padding()
VStack(alignment: .leading, spacing: 3) {
Text(task.taskName)
.fontWeight(.semibold)
.lineLimit(2)
Text(task.dueDate + " - " + task.subject)
.font(.subheadline)
.foregroundColor(.secondary)
}
})
}
.navigationTitle("My Tasks")
.listStyle(InsetGroupedListStyle())
}
}
}
}
struct ViewAssignment_Previews: PreviewProvider {
static var previews: some View {
ViewAssignment()
}
}
I would also like to know if, upon making the screen unique for each item in the list, would I be able to have the contents of the text field saved upon reloading the app, Perhaps through #AppStorage?
Thank you for the assistance.
If I understand correctly what you are trying to do:
a TaskDetailView displays the detail of a ... Task.
So you should have a Task structure like this:
struct Task {
let name: String
let subject: String
...
}
You have to create one (or more) instance of Task to test your TaskDetailView:
extension Task {
var test: Task {
Task(name: "Test", subject: "Test Subject")
}
}
Now in the preview of your TaskDetailView you can try to display your example :
struct TaskDetailView_Previews: PreviewProvider {
static var previews: some View {
TaskDetailView(task: Task.test) // here
}
}
For the moment nothing is happening. Because your TaskDetailView doesn't have a task parameter.
struct TaskDetailView: View {
var task: Task
var body: some View {
...
}
Now its body can use the different parameters of this Task.
Text(task.name)
.font(.title2)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.center)
Now in your List:
List(self.assignment.data) { task in
NavigationLink (
destination: TaskDetailView(task: task), // <- here !!!
label: {
Image(systemName: "doc.append.fill")
.scaleEffect(2.5)
.padding()
VStack(alignment: .leading, spacing: 3) {
Text(task.name)
.fontWeight(.semibold)
.lineLimit(2)
}
})
}

SwiftUI pop out temporary alert

I tried to do a app that pop out a temporary alert that only appear for 1 or 2 seconds. It’s something like App Store rating.
But I don’t know what this called in swiftui. Can anyone answer me?
That is just a view that is shown or hidden conditionally. Here is a complete example that uses a ZStack to place the thank you view over the other view content. The thank you view is either present or not based upon the #State variable showThankYou. DispatchQueue.main.asyncAfter is used to remove the view after 3 seconds.
struct ContentView: View {
#State private var showThankYou = false
var body: some View {
ZStack {
VStack {
Spacer()
Text("Stuff in the view")
Spacer()
Button("submit") {
showThankYou = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.showThankYou = false
}
}
Spacer()
Text("More stuff in the View")
Spacer()
}
if showThankYou {
RoundedRectangle(cornerRadius: 16)
.foregroundColor(Color.gray)
.frame(width: 250, height: 250)
.overlay(
VStack {
Text("Submitted").font(.largeTitle)
Text("Thanks for your feedback").font(.body)
}
)
}
}
}
}

SwiftUI Navigation View - Handling Size Classes over multiple views

I have the scenario where I intend on using a menu ('MenuView') that varies if the device is in portrait / landscape mode. I am using size classes to determine the device type and this successfully redraws the view on rotation. The menu uses navigation view/links to take you to a further view (the 'DetailedView'); this view also has differing views for portrait and landscape. Again I'm using size classes to successfully redraw the view based on the rotation.
However, what I find is that when I'm in the DetailedView and rotate the device, the display jumps straight back to the MenuView as, of course, it has recognized the size class change and adjusted that view. I would like the display to remain in this view.
How can I prevent the app from jumping to the 'MenuView' when I rotate the device that is displaying the 'DetailedView'? Code from the MenuView below:
Note: I'm using the StackNavigationViewStyle in this instance.
Any help would be gratefully received, thanks in advance!
struct MenuView: View {
#Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
#Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
var body: some View {
NavigationView {
Color("MainBg")
.edgesIgnoringSafeArea(.all)
if horizontalSizeClass == .compact {
PortraitMenuView()
} else {
LandscapeMenuView()
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
Update: DetailedView - This is called from the PortraitMenuView and LandscapeMenuView. Included code from PortraitMenuView - the same navigation link is included in the LandscapeMenuView
struct DetailedView: View {
#Environment(\.horizontalSizeClass) var sizeHClass
#Environment(\.verticalSizeClass) var sizeVClass
var formulae: Functions
var body: some View {
ZStack {
Color.offWhite
.edgesIgnoringSafeArea(.all)
VStack {
Group {
if sizeHClass == .compact && sizeVClass == .compact {
DetailedViewLandscape()
} else {
DetailedViewPortrait()
}
}
}
}
}
}
struct PortraitMenuView: View {
var body: some View {
VStack {
ZStack {
Circle()
.fill(Color("kMainBg"))
.frame(width: 300, height: 300)
.overlay(
Text("FORMULA FINDER")
.font(.largeTitle).bold()
.multilineTextAlignment(.center)
.minimumScaleFactor(0.005)
.lineLimit(2)
.frame(width: 210, height: 210)
)
}
Spacer()
NavigationLink(destination: DetailedView()) {
TileView(title: "Formulae", subtitle: "Functions and formulas", boxColor: Color.pastelGreen)
}.offset(x: 40)
}.padding(.bottom, 20)
}
}

SwiftUI Navigation Controller stuttering with two Navigationlinks per List row

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