Sponsor Link
Environment
- 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.
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
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
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
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.
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
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 [])
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<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)")
})
}
}
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)")
})
}
}
Summary
When selectable List with NavigationLink, need to understand its actual behavior.
- 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.
Sponsor Link