SwiftUI NavigationStack NavigationLink same Value, multiple destinations - swiftui

I have a model like so...
struct User: Codable {
let followersCount: Int
let followingCount: Int
}
Using NavigationStack I would like to be able to use NavigationLink based on Value
NavigationLink(value: user.followersCount) {
Text("Followers")
}
NavigationLink(value: user.followingCount) {
Text("Following")
}
.navigationDestination(for: Int.self) { _ in
FollowersView()
}
.navigationDestination(for: Int.self) { _ in
FollowingView()
}
As both values are an Int. Is there a way to differentiate the two?

Declare an object:
enum Link {
case followers(Int)
case following(Int)
}
then use it like:
NavigationLink(value: Link.followers(user.followersCount)) {
Text("Followers")
}
NavigationLink(value: Link.following(user.followingCount)) {
Text("Following")
}
.navigationDestination(for: Link) { link in
switch link {
case let .followers(count):
FollowersView()
case let .following(count):
FollowingView()
}
}

Related

NavigationLink fires more one times in NavigationStack

I faced the problem when NavTestChildView called more one times. I don't understand what going wrong. I tested on a real device with iOS 16.0.3 and emulator Xcode 14.0.1
I replaced original code to give more info about the architecture why I create NavTestService into navigationDestination.
enum NavTestRoute: Hashable {
case child(Int)
}
class NavTestService: ObservableObject {
let num: Int
init(num: Int) {
self.num = num
print("[init][NavTestService]")
}
deinit {
print("[deinit][NavTestService]")
}
}
struct NavTestChildView: View {
#EnvironmentObject var service: NavTestService
init() {
print("[init][NavTestChildView]")
}
var body: some View {
Text("NavTestChildView \(service.num)")
}
}
struct NavTestMainView2: View {
var body: some View {
VStack {
ForEach(1..<10, id: \.self) { num in
NavigationLink(value: NavTestRoute.child(num)) {
Text("Open child \(num)")
}
}
}
}
}
struct NavTestMainView: View {
var body: some View {
NavigationStack {
NavTestMainView2()
.navigationDestination(for: NavTestRoute.self) { route in
switch route {
case let .child(num):
NavTestChildView().environmentObject(NavTestService(num: num))
}
}
}
}
}
logs:
[init][NavTestChildView]
[init][NavTestService]
[deinit][NavTestService]
[init][NavTestChildView]
[init][NavTestService]
Looks like there is a period when instance of NavTestService is not held by anyone and it leaves the heap. In practice this would hardly ever happen because .environmentObject vars are usually held somewhere up the hierarchy.
If you change NavTestMainView accordingly:
struct NavTestMainView: View {
let navTestService = NavTestService()
var body: some View {
NavigationStack {
NavigationLink(value: NavTestRoute.child) {
Text("Open child")
}
.navigationDestination(for: NavTestRoute.self) { route in
switch route {
case .child:
NavTestChildView().environmentObject(navTestService)
}
}
}
}
}
... you get no deinits and no extra init as well. The console will output:
[init()][NavTestService]
[init()][NavTestChildView]
[init()][NavTestChildView]
Also note that if you comment out let navTestService = NavTestService() and wrap NavTestChildView().environmentObject(NavTestService()) in LazyView you'll get the following output:
[init()][NavTestChildView]
[init()][NavTestService]
Where LazyView is:
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
It's not "firing" it's just initing the View struct multiple times which is perfectly normal and practically zero overhead because View structs are value types. It tends to happen because UIKit's event driven design doesn't align well with SwiftUI's state driven design.
You can simplify your code by replacing the router enum / case statement with multiple navigationDestination for each model type.

Undesired interplay between tapable, movable items and scrolling in SwiftUI List

I'm working on a SwiftUI list that shows tapable and long-pressable full-width items, which are movable, and allow for detail navigation.
I've noticed that .onLongPressGesture isn't detected when the list allows for moving of items, because the List switches to drag-moving the long-pressed item instead.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
let data = Array(0..<20)
var body: some View {
NavigationStack {
List {
ForEach(data, id:\.self) { item in
NavigationLink(destination: EmptyView(), label: {
Rectangle().fill(.mint)
.onTapGesture { print("tapped", item) }
.onLongPressGesture{ print("longPressed", item)}
})
}.onMove(perform: moveItems)
}
}
}
func moveItems(from source: IndexSet, to destination: Int) { }
}
PlaygroundPage.current.setLiveView(ContentView())
I've experimented further and found that using simultaneous gesture via simultaneousGesture() fixes the missing notification on long presses, but instead removes scrolling ability from the List.
import SwiftUI
import PlaygroundSupport
struct ContentViewSimultaneous: View {
let data = Array(0..<20)
var body: some View {
NavigationStack {
List {
ForEach(data, id:\.self) { item in
NavigationLink(destination: EmptyView(), label: {
Rectangle().fill(.blue)
.simultaneousGesture(TapGesture().onEnded { print("tapped", item) })
.simultaneousGesture(LongPressGesture().onEnded { _ in
print("longPressed", item) })
})
}.onMove(perform: moveItems)
}
}
}
func moveItems(from source: IndexSet, to destination: Int) { }
}
PlaygroundPage.current.setLiveView(ContentViewSimultaneous())
I'm now looking for a way to make this work and would appreciate any insights! I'm new to SwiftUI and might miss something important.
I think I was able to get this working as you describe. It works with no issues on iOS 15, but there seems to be an animation bug in iOS 16 that causes the rearrange icon not to animate in for some/all List rows. Once you drag an item in edit mode, the icon will display.
struct ContentView: View {
#State private var editMode: EditMode = .inactive
#State var disableMove: Bool = true
var body: some View {
let data = Array(0..<20)
NavigationView {
List {
ForEach(data, id:\.self) { item in
NavigationLink(destination: EmptyView(), label: {
Rectangle().fill(.mint)
.onTapGesture { print("tapped", item) }
.onLongPressGesture{ print("longPressed", item)}
})
}
.onMove(perform: disableMove ? nil : moveItems)
}
.toolbar {
ToolbarItem {
Button {
withAnimation {
self.disableMove.toggle()
}
} label: {
Text(editMode == .active ? "Done" : "Edit")
}
}
}
.environment(\.editMode, $editMode)
}
.onChange(of: disableMove) { disableMove in
withAnimation {
self.editMode = disableMove ? .inactive : .active
}
}
.navigationViewStyle(.stack)
}
func moveItems(from source: IndexSet, to destination: Int) { }
}
Not sure if this helps
enum Status {
case notPressed
case pressed
case longPressed
}
struct ContentView: View {
#State private var status = Status.notPressed
var body: some View {
Rectangle()
.foregroundColor(color)
.simultaneousGesture(LongPressGesture().onEnded { _ in
print("longPressed")
status = .longPressed
})
.simultaneousGesture(TapGesture().onEnded { _ in
print("pressed")
status = .pressed
})
}
var color: Color {
switch status {
case .notPressed:
return .mint
case .pressed:
return .yellow
case .longPressed:
return .orange
}
}
}

Display a View when rest of content is empty

I have a view body with logic such as this:
var body: some View {
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
}
}
This works fine to conditionally show elements vertically stacked. However, if none of the conditions are satisfied, the VStack is empty and my UI looks broken. I would like to show a placeholder instead. My current solution is to add a manual check at the end on !someCondition && !anotherCondition && !thirdCondition:
var body: some View {
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
if !someCondition && !anotherCondition && !thirdCondition { // 👈
Text("Please select an element.")
}
}
}
However, this is difficult to keep the condition in sync with the content above. I was hoping there was some sort of view modifier I could use such as:
var body: some View {
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
}.emptyState { // 👈
Text("Please select an element.")
}
}
The closest thing I could find is this tutorial, but that requires passing in the condition as well.
Is there a way to build a view modifier like this emptyState which doesn't require duplicating the condition logic?
I was thinking I could use a ZStack for this:
var body: some View {
ZStack { // 👈
// empty state text
Text("Please select an element.")
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
}
}
}
... but then I run into a different issue where if I'm showing real content (e.g. SomeView()) but it's not large enough, I could see both SomeView() and the empty state text.
Here's one implementation using GeometryReader & it's named emptyState:
extension View {
func emptyState<Content: View>(#ViewBuilder content: () -> Content) -> some View {
return self.modifier(EmptyStateModifier(placeHolder: content()))
}
}
struct EmptyStateModifier<PlaceHolder: View>: ViewModifier {
#State var isEmpty = false
let placeHolder: PlaceHolder
func body(content: Content) -> some View {
ZStack {
if isEmpty {//Thanks to #Asperi
placeHolder
}
content
.background(
GeometryReader { reader in
Color.clear
.onChange(of: reader.frame(in: .global).size == .zero) { newValue in
isEmpty = reader.frame(in: .global).size == .zero
}
}
)
}
}
}
If it is a long chaining condition, you can handle it with switch{}, then use the benefit of default to display the placeholder when 0 condition is met(stack is empty or no selection)
#State var selected = ""
var body: some View {
VStack {
switch selected {
case "a":
SomeView()
case "b":
AnotherView()
case "c":
ThirdView()
//this default will show up
//when there is no selection
//and when the stack is empty meaning that all the above
//conditions did not meet
default:
Text("Please select an element")
}
}
}
There is no straightforward, officially supported way of telling what the return value of a view builder contains.
It is more sensible to handle this at the model layer than in your view. Each of your conditions are part of your model. These should be wrapped up into a single value type, and you can use the presence or absence of that (or an internal calculated value of that, depending on your requirements) to inform what to put in the stack. For example:
struct Model {
let one: Bool
let two: Bool
let three: Bool
}
struct MyView: View {
let model: Model?
var body: some View {
VStack {
switch model {
case .some(let model):
if model.one {
Text("One")
}
if model.two {
Text("Two")
}
if model.three {
Text("Three")
}
case .none:
Text("Empty")
}
}
}
}
(I'm assuming here that there is no valid Model which doesn't contain any of the values, that would be when it is set to nil)

Using switch/enums in SwiftUI Views

As of Xcode 11.4, SwiftUI doesn't allow switch statements in Function builder blocks like VStack {}, failing with a generic error like Generic parameter 'Content' could not be inferred. How can the switch statement be used in SwiftUI to create different Views depending on an enum value?
switch in SwiftUI view builders is supported since Xcode 12:
enum Status {
case loggedIn, loggedOut, expired
}
struct SwiftUISwitchView: View {
#State var userStatus: Status = .loggedIn
var body: some View {
VStack {
switch self.userStatus {
case .loggedIn:
Text("Welcome!")
case .loggedOut:
Image(systemName: "person.fill")
case .expired:
Text("Session expired")
}
}
}
}
You can use enum with #ViewBuilder as follow ...
Declear enum
enum Destination: CaseIterable, Identifiable {
case restaurants
case profile
var id: String { return title }
var title: String {
switch self {
case .restaurants: return "Restaurants"
case .profile: return "Profile"
}
}
}
Now in the View file
struct ContentView: View {
#State private var selectedDestination: Destination? = .restaurants
var body: some View {
NavigationView {
view(for: selectedDestination)
}
}
#ViewBuilder
func view(for destination: Destination?) -> some View {
switch destination {
case .some(.restaurants):
CategoriesView()
case .some(.profile):
ProfileView()
default:
EmptyView()
}
}
}
If you want to use the same case with the NavigationLink ... You can use it as follow
struct ContentView: View {
#State private var selectedDestination: Destination? = .restaurants
var body: some View {
NavigationView {
List(Destination.allCases,
selection: $selectedDestination) { item in
NavigationLink(destination: view(for: selectedDestination),
tag: item,
selection: $selectedDestination) {
Text(item.title).tag(item)
}
}
}
}
#ViewBuilder
func view(for destination: Destination?) -> some View {
switch destination {
case .some(.restaurants):
CategoriesView()
case .some(.profile):
ProfileView()
default:
EmptyView()
}
}
}

How to display a multi-dimensional array?

I have a 2d array with custom types.
I'd like to do something like:
HStack {
ForEach(2dArray) { x in
VStack {
ForEach(self.2dArray[x.index]) { y in
The main issue I have is that I can't figure out how to get x.index.
So I was going to say you should do this to iterate a 2D array:
var data = [[1,2,3],[4,5,6],[7,8,9]]
ForEach(data.identified(by: \.self)) { array in
ForEach(array.identified(by: \.self)) { element in
Text("\(element)")
}
}
but XCode 11 beta is still very buggy with SwiftUI and type inference, so you get a compiler error for doing that, even though it should work. So for now, you'll have to separate everything into functions that the XCode compiler can handle, but with the complex types that SwiftUI uses, it gets ugly very fast. Here is an example:
var data = [[1,2,3],[4,5,6],[7,8,9]]
var body: some View {
doubleForEach(data: data)
}
func doubleForEach(data: [[Int]]) -> ForEach<IdentifierValuePairs<[[Int]], [Int]>,ForEach<IdentifierValuePairs<[Int], Int>, Text>> {
return ForEach(data.identified(by: \.self)) { row in
self.foreach(dataArr: row)
}
}
func foreach(dataArr: [Int]) -> ForEach<IdentifierValuePairs<[Int], Int>, Text> {
return ForEach(dataArr.identified(by: \.self)) { data in
Text("\(data)")
}
}
Which looks like this:
actually the example RPatel99 postet in the earlier response works fine for me:
var data = [[1,2,3],[4,5,6],[7,8,9]]
struct ForTesting : View {
var body: some View {
VStack {
ForEach(data.identified(by: \.self)) { array in
ForEach(array.identified(by: \.self)) { element in
Text("\(element)")
}
}
}
}
}
But was this your problem?
var data = [[1,2,3],[4,5,6],[7,8,9]]
struct ContentView : View {
var body: some View {
VStack {
ForEach(data, id: \.self) { array in
HStack{
ForEach(array, id: \.self) { element in
Text("\(element)")
}
}
}
}
}
}
struct ContentView : View {
var body: some View {
VStack {
ForEach(data.identified(by: \.self)) { array in
HStack{
ForEach(array.identified(by: \.self)) { element in
Text("\(element)")
}
}
}
}
}
}