SwiftUI NavigationLink style within ScrollView - swift

I have a screen where I'm trying to display a list of NavigationLink and a grid of items (using LazyVGrid). I first tried putting everything in a List, like this:
List() {
ForEach(items) { item in
NavigationLink(destination: MyDestination()) {
Text("Navigation link text")
}
}
LazyVGrid(columns: columns) {
ForEach(gridItems) { gridItem in
MyGridItem()
}
}
}
However, it seems that putting a LazyVGrid in a List doesn't load the items in the grid lazily, it loads them all at once. So I replaced the List with a ScrollView and it works properly. However, I do want to keep the style of the NavigationLink that is shown when they are in a List. Basically what this looks like https://www.simpleswiftguide.com/wp-content/uploads/2019/10/Screen-Shot-2019-10-05-at-3.00.34-PM.png instead of https://miro.medium.com/max/800/1*LT7ZwIaidXrMuR6pu1Jvgg.png.
How can this be achieved? Or is there a way to put a LazyVGrid in a List and still have it load lazily?

Take a loot at this, the List is already a built-in Lazy load scroll view, you can check that is lazy on the onAppear event
import SwiftUI
struct item:Identifiable {
let id = UUID()
let value: String
}
struct listTest: View {
#State var items: [item]
init () {
var newItems = [item]()
for i in 1...50 {
newItems.append(item(value: "ITEM # \(i)"))
}
self.items = newItems
}
var body: some View {
NavigationView {
List($items) { $item in
NavigationLink(destination: MyDestination(value: item.value)) {
Text("Navigation link text")
.onAppear{
print("IM ITEM: \(item.value)")
}
.id(UUID())
}
}
}
}
}
struct MyDestination: View {
let value: String
var body: some View {
ZStack{
Text("HI A DETAIL: \(value)")
}
}
}

Related

SwiftUI - Create a list with navigation links with data and views from an array

I'm trying to create a list with NavigationLinks, where I can store my views in an array and loop through them with a for loop. Unfortunately with what I have until now this just doesn't work.
I get this error all the time: Closure containing control flow statement cannot be used with result builder 'ViewBuilder'
My code:
struct ContentView: View {
let list = [DiceView(), DomainResolver()] as [Any]
var body: some View {
NavigationView {
List {
for i in list {
NavigationLink{
i
} label: {
Label("Test", systemImage: "person").foregroundColor(.primary)
}
}
}
}
}
}
Thanks for every constructive answer.
In swiftUI, inside the struct you cannot use for loop. Instead, you can use ForEach.
struct ContentView: View {
let list = [DiceView(), DomainResolver()] as [Any]
var body: some View {
NavigationView {
List {
ForEach(list, id: \.self) { i in
NavigationLink{
i
} label: {
Label("Test", systemImage: "person").foregroundColor(.primary)
}
}
}
}
}
}
Notice, that in Foreach you also use id: .self, which means that ForEach makes every object unique.

NavigationLink with isActive creates unusual scrolling behavior in macOS SwiftUI app

I have a macOS app with a 3-column view. In the first column, there is a List of items that is lengthy -- perhaps a couple hundred items.
If my NavigationLink for each item contains a isActive parameter, when clicking on the NavigationLinks, I get unpredictable/unwanted scrolling behavior on the list.
For example, if I scroll down to Item 100 and click on it, the List may decide to scroll back up to Item 35 or so (where the active NavigationLink is out of the frame). The behavior seems somewhat non-deterministic -- it doesn't always scroll to the same place. It seems less likely to scroll to an odd location if I scroll through the list to my desired item and then wait for the system scroll bars to disappear before clicking on the NavigationLink, but it doesn't make the problem disappear completely.
If I remove the isActive parameter, the scroll position is maintained when clicking on the NavigationLinks.
struct ContentView : View {
var body: some View {
NavigationView {
SidebarList()
Text("No selection")
Text("No selection")
.frame(minWidth: 300)
}
}
}
struct Item : Identifiable, Hashable {
let id = UUID()
var name : String
}
struct SidebarList : View {
#State private var items = Array(0...300).map { Item(name: "Item \($0)") }
#State private var activeItem : Item?
func navigationBindingForItem(item: Item) -> Binding<Bool> {
.init {
activeItem == item
} set: { newValue in
if newValue { activeItem = item }
}
}
var body: some View {
List(items) { item in
NavigationLink(destination: InnerSidebar(),
isActive: navigationBindingForItem(item: item) //<-- Remove this line and everything works as expected
) {
Text(item.name)
}
}.listStyle(SidebarListStyle())
}
}
struct InnerSidebar : View {
#State private var items = Array(0...100).map { Item(name: "Inner item \($0)") }
var body: some View {
List(items) { item in
NavigationLink(destination: Text("Detail")) {
Text(item.name)
}
}
}
}
I would like to keep isActive, as I have some programatic navigation that I'd like to be able to do that depends on it. For example:
Button("Go to item 10") {
activeItem = items[10]
}
Is there any way to use isActive on the NavigationLink and avoid the unpredictable scrolling behavior?
(Built and tested with macOS 11.3 and Xcode 13.0)
The observed effect is because body of your main view is refreshed and all internals rebuilt. To avoid this we can separate sensitive part of view hierarchy into standalone view, so SwiftUI engine see that dependency not changed and view should not be updated.
Here is a fixed parts:
struct SidebarList : View {
#State private var items = Array(0...300).map { Item(name: "Item \($0)") }
#State private var activeItem : Item?
var body: some View {
List(items) {
SidebarRowView(item: $0, activeItem: $activeItem) // << here !!
}.listStyle(SidebarListStyle())
}
}
struct SidebarRowView: View {
let item: Item
#Binding var activeItem: Item?
func navigationBindingForItem(item: Item) -> Binding<Bool> {
.init {
activeItem == item
} set: { newValue in
if newValue {
activeItem = item
}
}
}
var body: some View {
NavigationLink(destination: InnerSidebar(),
isActive: navigationBindingForItem(item: item)) {
Text(item.name)
}
}
}
Tested with Xcode 13 / macOS 11.6

NavigationLink doesn't work with new .searchable modifier on SwiftUI 3.0

I'm trying the new features of SwiftUI 3.0 introduced at Apple WWDC21.
In particular I'm trying to use the .searchable modifier to implement a search bar in a list and let the user navigate to the Item view from the search result using a NavigationLink.
struct OrderItem : Identifiable {
let id = UUID()
var label : String
var value : Bool = false
}
struct BindingTest: View {
#State var items = [OrderItem(label: "Shirts"), OrderItem(label: "Pants"), OrderItem(label: "Socks")]
#State private var searchedText = ""
#ViewBuilder
var body: some View {
NavigationView {
List($items) { $item in
NavigationLink(destination: ItemView(item: $item)) {
HStack {
if(item.value) {
Image(systemName: "star.fill").foregroundColor(.yellow)
}
Text(item.label)
}
}
}.navigationBarTitle("Clothing")
// Search bar
.searchable(text: $searchedText, placement: .automatic) {
if searchedText != "" {
// Search result
ForEach($items.filter({$0.label.wrappedValue.contains(searchedText)})) { $item in
NavigationLink(destination: ItemView(item: $item)) {
HStack {
if(item.value) {
Image(systemName: "star.fill").foregroundColor(.yellow)
}
Text(item.label)
}
}.searchCompletion(item.label)
}
}
}
}
}
}
But when I'm in the search result list and I tap on one result, the NavigationLink doesn't work and it shows me the original list. Anyone else experiencing this issue?
[EDIT]
I checked this NavigationLink inside .searchable does not work but unfortunately it doesn't answer my question because my NavigationLink is already inside a NavigationView.
You have two Navigation Links here, one inside your original content, and one passed as a closure to your searchable modifier, which is actually being shown as the searchText changes. The latter isn't inside a Navigation View, and hence is unable to navigate to the second screen. To achieve your expectation, you should have the ForEach with the NavigationLink in the original content itself, and filter the list, based on the search text.
The search completion is supposed to be a guidance to complete the searchText, not to navigate to the second screen.
The updated code should look something like this
struct BindingTest: View {
#State var items = [OrderItem(label: "Shirts"), OrderItem(label: "Pants"), OrderItem(label: "Socks")]
#State private var searchedText = ""
#ViewBuilder
var body: some View {
NavigationView {
List($items.filter({$0.label.wrappedValue.contains(searchedText)})) { $item in
NavigationLink(destination: ItemView(item: $item)) {
HStack {
if(item.value) {
Image(systemName: "star.fill").foregroundColor(.yellow)
}
Text(item.label)
}
}
}
.searchable(text: $searchedText, placement: .automatic) {
if searchedText != "" {
ForEach($items.filter({$0.label.wrappedValue.contains(searchedText)})) { $item in
Text(item.label).searchCompletion(item.label)
}
}
}
}
}
}
Hope it answers your question :)

SwiftUI deep linking with NavigationLink inside List onAppear with tag: and selection: doesn't activate link

Trying to build deep linking into a list of NavigationList items; I will be reading a value on the SwiftUI view's .onAppear and based on that value, navigate to a specific cell. There are three issues that come up with different setups I have tried: (1) with the below code, navigation doesn't happen at all, (2) if it does navigate, it will immediately pop back, (3) if programmatic navigation works and it doesn't pop back, the manual navigation doesn't work.
I have tried this with a Binding dictionary, and I get issue #2 above. Not only this, but in both solutions, user has to scroll to the cell in order to even read the binding/selection.
import SwiftUI
struct ContentViewTwo: View {
var data = ["1", "2", "3"]
#State var shouldPushPage3: Bool = true
var page3: some View {
Text("Page 3")
}
#State var selected: String?
var body: some View {
return
List(data, id: \.self) { data in
NavigationLink(destination: self.page3, tag: data, selection: self.$selected) {
Text("Tap for Page 3 with Data: \(data):")
}.onAppear() {
print("link appeared.")
}
}.onAppear() {
if (self.shouldPushPage3) {
self.selected = "3" // Has no affect. 😢
self.shouldPushPage3 = false
}
}
}
}
struct ContentView: View {
var body: some View {
return NavigationView() {
VStack {
Text("Page 1")
NavigationLink(destination: ContentViewTwo()) {
Text("Tap for Page 2")
}
}
}
}
}
You need to dispatch the selection.
.onAppear {
guard shouldPushPage3 else { return }
shouldPushPage3 = false
DispatchQueue.main.async {
selection = "3"
}
}

Insert, update and delete animations with ForEach in SwiftUI

I managed to have a nice insert and delete animation for items displayed in a ForEach (done via .transition(...) on Row). But sadly this animation is also triggered when I just update the name of Item in the observed array. Of course this is because it actually is a new view (you can see that, since onAppear() of Row is called).
As we all know the recommended way of managing lists with cool animations would be List but I think that many people would like to avoid the standard UI or the limitations that come along with this element.
A working SwiftUI example snippet is attached (Build with Xcode 11.4)
So, the question:
Is there a smart way to suppress the animation (or have another one) for just updated items that would keep the same position? Is there a cool possibility to "reuse" the row and just update it?
Or is the answer "Let's wait for the next WWDC and let's see if Apple will fix it..."? ;-)
Cheers,
Orlando 🍻
Edit
bonky fronks answer is actually a good approach when you can distinguish between edit/add/delete (e.g. by manual user actions). As soon as the items array gets updated in background (for example by synced updates coming from Core Data in your view model) you don't know if this is an update or not. But maybe in this case the answer would be to manually implement the insert/update/delete cases in the view model.
struct ContentView: View {
#State var items: [Item] = [
Item(name: "Tim"),
Item(name: "Steve"),
Item(name: "Bill")
]
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(items, id: \.self) { item in
Row(name: item.name)
}
}
}
.navigationBarItems(leading: AddButton, trailing: RenameButton)
}
}
private var AddButton: some View {
Button(action: {
self.items.insert(Item(name: "Jeff"), at: 0)
}) {
Text("Add")
}
}
private var RenameButton: some View {
Button(action: {
self.items[0].name = "Craigh"
}) {
Text("Rename first")
}
}
}
struct Row: View {
#State var name: String
var body: some View {
HStack {
Text(name)
Spacer()
}
.padding()
.animation(.spring())
.transition(.move(edge: .leading))
}
}
struct Item: Identifiable, Hashable {
let id: UUID
var name: String
init(id: UUID = UUID(), name: String) {
self.id = id
self.name = name
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Luckily this is actually really easy to do. Simply remove .animation(.spring()) on your Row, and wrap any changes in withAnimation(.spring()) { ... }.
So the add button will look like this:
private var AddButton: some View {
Button(action: {
withAnimation(.spring()) {
self.items.insert(Item(name: "Jeff"), at: 0)
}
}) {
Text("Add")
}
}
and your Row will look like this:
struct Row: View {
#State var name: String
var body: some View {
HStack {
Text(name)
Spacer()
}
.padding()
.transition(.move(edge: .leading))
}
}
The animation must be added on the VStack with the modifier animation(.spring, value: items) where items is the value with respect to which you want to animate the view. items must be an Equatable value.
This way, you can also animate values that you receive from your view model.
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(items, id: \.self) { item in
Row(name: item.name)
}
}
.animation(.spring(), value: items) // <<< here
}
.navigationBarItems(leading: AddButton, trailing: RenameButton)
}
}