[SwiftUI] List’s selection and NavigationLink

SwiftUI2021

     

TAGS:

In this post, I'll try to explain the relation between List's selection and NavigationLink.

Environment

used following env for this post.

  • macOS Monterey 12.4 beta3
  • Xcode 13.3.1
  • iOS 15.4

Motivation

During the app development for macOS, I found strange behavior on List's selection using with NavigationLink.

At that time, I used hack-y workaround. But I believe this would make things more complicated. so I want to summarize my research about this behavior.

note:
I could not find any topic in StackOverflow. maybe my search words are wrong.
of course we should NOT expect detail explanation in Apple's....

check ont only on iOS but also on macOS

Sometimes we can see different behavior between iOS and macOS. so I decided to check both platforms.

but I did not check on tvOS, watchOS.

base app

I used TabView app for checking.

I'll replace each Text step by step.

ContentView.swift

let data = ["0", "1", "2", "3"]

struct ContentView: View {
    var body: some View {
        TabView {
            Text("Single Selection")
                .tabItem { Image(systemName: "1.circle"); Text("Single") }
            Text("Multiple Selection")
                .tabItem { Image(systemName: "1.square"); Text("Single+Nav") }
            Text("Single Nav Selection")
                .tabItem { Image(systemName: "number.circle"); Text("Multi") }
            Text("Multiple Nav Selection")
                .tabItem { Image(systemName: "number.square"); Text("Multi+Nav") }
        }
        .padding()
    }
}

List's selection

For List, there are selection argument and it keeps the selected element in the List.

MEMO
on iOS, .editMode should be .active for list element selection

We can choose single-selection/multi-selection with giving appropriate argument.

single selection

The list which can have only 1 element as selection named "SingleSelection"

・ with using onChange, we can observe selection variable
・ on iOS, set .editMode = .active to selectable in list

basically modification on ContentView is very simple and has similarity. if you want to see whole code, please check code at end of this post.

SingleSelection: View

let data = ["0", "1", "2", "3"]

struct ContentView: View {
    var body: some View {
        TabView {
            SingleSelection()
                .tabItem { Image(systemName: "1.circle"); Text("Single") }
#if os(iOS)
                .environment(\.editMode, .constant(.active))
#endif
            Text("Hello world")
                .tabItem { Image(systemName: "1.square"); Text("Single+Nav") }
            Text("Hello world")
                .tabItem { Image(systemName: "number.circle"); Text("Multi") }
            Text("Hello world")
                .tabItem { Image(systemName: "number.square"); Text("Multi+Nav") }
        }
        .padding()
    }
}
struct SingleSelection: View {
    @State private var selection: String? = nil
    
    var body: some View {
        List(data, id: \.self, selection: $selection) { datum in
            Text("\(datum)")
                .tag(datum)
        }
        .onChange(of: selection, perform: { newValue in
            print("selection changed to \(selection ?? "nil")")
        })
    }
}

App looks like followings for iOS and macOS

Single_ios
Single_macos

difference between iOS and macOS(List single selection)

no difference. on both, selected element is stored in selection.

multi selection

The list which can have only 1 element as selection named "MultiSelection"

MultiSelection: View

struct MultiSelection: View {
    @State private var selections: Set<String> = Set()
    
    var body: some View {
        List(data, id: \.self, selection: $selections) { datum in
            Text("\(datum)")
                .tag(datum)
        }
        .onChange(of: selections, perform: { newValue in
            print("selection changed to \(selections)")
        })
    }
}

App looks like followings for iOS and macOS

Multi_ios
Multi_macos

difference between iOS and macOS(List multi selection)

no difference. on both, selected elements are stored in selection.

Note:
on macOS, need to use ⌘-key, shift-key for multi selection

together with NavigationLink

Frequently NavigationLink is used together with List.

For example, "master-detail view" app uses this combination.

single selection

NavigationLink is used for single selection list.

on iOS, NavigationLink will lead to another view, so no one mid selection. but on macOS ( and iPadOS in some case) list is still there. so value in selection is important.

SingleNavSelection: View

struct SingleNavSelection: View {
    @State private var selection: String? = nil
    
    var body: some View {
        NavigationView {
            List(data, id: \.self, selection: $selection) { datum in
                NavigationLink("\(datum)", destination: {
                    Text("Detail for \(datum)")
                })
                .tag(datum)
            }
        }
        .onChange(of: selection, perform: { newValue in
            print("selection changed to \(selection ?? "nil")")
        })
    }
}

App looks like followings for iOS and macOS

Single_Nav_ios
Single_Nav_macos

difference between iOS and macOS(List single selection with NavigationLink)

on iOS, selection is not set appropriately ! NavigationLink will lead to another view. That's it.
on macOS, selection will be set then NavigationLink will lead to another view.
iOS では、selection は、設定されません。設定されずに、NavigationLink に従った画面遷移が行われます。
macOS では、selection は、設定されます。設定後、NavigationLink に従った画面表示が行われます。

Note:
I feel the behavior on iOS is strange.

multi selection

NavigationLink is used for multi-selection list.

struct MultiNavSelection: View {
    @State private var selections: Set<String> = Set()
    
    var body: some View {
        NavigationView {
            List(data, id: \.self, selection: $selections) { datum in
                NavigationLink("\(datum)", destination: {
                    Text("Detail for \(datum)")
                })
                .tag(datum)
            }
        }
        .onChange(of: selections, perform: { newValue in
            print("selection changed to \(selections)")
        })
    }
}

App looks like followings for iOS and macOS

Multi_Nav_ios
Multi_Nav_ios

difference between iOS and macOS(List multi selection with NavigationLink)

on iOS selection is not set appropriately same as single selection. NavigationLink will lead to another view anyway.
so multi-selection with NavigationLink does not make much sense.

on macOS, selection is set but strangely !

I'll try to explain first.
- when user select 2nd element, then list will try to reduce selection to 1 item or 0.
- maybe element order will effect reduce target (1 or 0)

I know above explanation is NOT easy to understand, so please see following video to understand actual behavior.

Followings are the operation I did. ' means internal behavior (from my guess).
1) select 0
2) de-select 0
3) select 1
4) de-select 1 ---- until here looks reasonable behavior
5) select 0
6) select 2 additionally (see console: 0 and 2 are stored in selections once)
6') only 2 is selected (see console: now only 2 is stored in selections)
7) select 3 additionally (see console: 2 and 3 are stored in selections)
7') only 3 is selected (see console: now only 3 is stored in selections)
8) select 2 additionally (see console: 2 and 3 are stored in selections)
8') nothing is selected (see console: selections is [])

MEMO
I have no idea for this behavior.

whole code used for this post

followings are the code used for this post.


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2022/05/02
//  © 2022  SmallDeskSoftware
//

import SwiftUI

let data = ["0", "1", "2", "3"]

struct ContentView: View {
    var body: some View {
        TabView {
            SingleSelection()
                .tabItem { Image(systemName: "1.circle"); Text("Single") }
#if os(iOS)
                .environment(\.editMode, .constant(.active))
#endif
            SingleNavSelection()
                .tabItem { Image(systemName: "1.square"); Text("Single+Nav") }
#if os(iOS)
                .environment(\.editMode, .constant(.active))
#endif
            MultiSelection()
                .tabItem { Image(systemName: "number.circle"); Text("Multi") }
#if os(iOS)
                .environment(\.editMode, .constant(.active))
#endif
            MultiNavSelection()
                .tabItem { Image(systemName: "number.square"); Text("Multi+Nav") }
#if os(iOS)
                .environment(\.editMode, .constant(.active))
#endif

        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct SingleSelection: View {
    @State private var selection: String? = nil
    
    var body: some View {
        List(data, id: \.self, selection: $selection) { datum in
            Text("\(datum)")
                .tag(datum)
        }
        .onChange(of: selection, perform: { newValue in
            print("selection changed to \(selection ?? "nil")")
        })
    }
}

struct SingleNavSelection: View {
    @State private var selection: String? = nil
    
    var body: some View {
        NavigationView {
            List(data, id: \.self, selection: $selection) { datum in
                NavigationLink("\(datum)", destination: {
                    Text("Detail for \(datum)")
                })
                .tag(datum)
            }
        }
        .onChange(of: selection, perform: { newValue in
            print("selection changed to \(selection ?? "nil")")
        })
    }
}

struct MultiSelection: View {
    @State private var selections: Set = Set()
    
    var body: some View {
        List(data, id: \.self, selection: $selections) { datum in
            Text("\(datum)")
                .tag(datum)
        }
        .onChange(of: selections, perform: { newValue in
            print("selection changed to \(selections)")
        })
    }
}

struct MultiNavSelection: View {
    @State private var selections: Set = Set()
    
    var body: some View {
        NavigationView {
            List(data, id: \.self, selection: $selections) { datum in
                NavigationLink("\(datum)", destination: {
                    Text("Detail for \(datum)")
                })
                .tag(datum)
            }
        }
        .onChange(of: selections, perform: { newValue in
            print("selection changed to \(selections)")
        })
    }
}

Summary

When selectable List with NavigationLink, need to understand its actual behavior.

Points: selectable List with NavigationLink
  • on iOS, selectable List with NavigationLink does NOT maintain selection
  • on macOS, selectable List with NavigationLink maintain selection "partially".

Hope this helps.
if you have any comments/advices, please contact me at here.

Leave a Reply

Your email address will not be published.