Sponsor Link
環境&対象
- macOS Monterey 12.4 beta
- Xcode 13.3
- iOS 15.4
EditMode
There are many Apps which has following UIs.
User can remove item, reorder item with using “Edit” button.
Editing mode(after clicking edit button)
following code are used for above screen shots
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2022/04/11
// © 2022 SmallDeskSoftware
//
import SwiftUI
struct ContentView: View {
@State private var data = ["One", "Two", "Three"]
var body: some View {
NavigationView {
List {
ForEach(data, id: \.self) { data in
Text(data)
}
.onDelete { _ in } // remove
.onMove { _, _ in } // reorder
}
.toolbar {
EditButton()
}
}
}
}
“EditButton” makes easy to build app with editable capability. (In above, EditButton is attached to NavigationView)
Environment Variable: EditMode
In some case, developer might want to check it is under editing or not for appropriate UI.
With using EditButton, there is no way to set internal flag for editing mode….
But you can get the information via Environment variable. EnvironmentVariable Key is .editMode.
please refer to here。
Variable type is Binding<EditMode>? .
You can find there it is under editing or not with checking isEditing.
In Apple example, you can find following code for refering isEditing. (note: comment are added by me.)
@Environment(\.editMode) private var editMode
@State private var name = "Maria Ruiz"
var body: some View {
Form {
// check isEditing, then use appropriate UI
if editMode?.wrappedValue.isEditing == true {
TextField("Name", text: $name)
} else {
Text(name)
}
}
.animation(nil, value: editMode?.wrappedValue)
.toolbar { // Assumes embedding this view in a NavigationView.
EditButton()
}
}
Let’s refine UI with using editMode… ?!
here is the first code for using editMode.
List cell will be “EditMode” or “Non-EditMode” whether current mode is Editing or not.
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2022/04/11
// © 2022 SmallDeskSoftware
//
import SwiftUI
struct ContentView: View {
@Environment(\.editMode) var editMode
@State private var data = ["One", "Two", "Three"]
var body: some View {
NavigationView {
List {
ForEach(data, id: \.self) { data in
Text(editMode?.wrappedValue.isEditing == true ? "EditMode" : "Non-EditMode")
}
.onDelete { _ in } // remove
.onMove { _, _ in } // reorder
}
.toolbar {
EditButton()
}
}
}
}
Looks in editing mode, but not in editing mode??
left: non-editing mode right: editing mode
But you can see “Non-EditMode” even in “Editing” mode….
I just copied code from Apple example, so it should be correct.
So I started to investigate why.
How to investigate?
Firstly, I need to decide how to investigate. Because editMode is environment variable, so it is not easy to debug with using debugger.
Looks List already knows it is “editing” mode, so I believe environment variable is changed accordingly somehow.
So my first hypothesis is following
“View does not receive variable update notification by some reason, so view is NOT updated correctly.”
EditMode is struct, and it is conformance to Equatable, so we can observe it with using .onChange.
So I added following code to many place for checking.
.onChange(of: editMode?.wrappedValue) { newValue in
print("EditMode -> \(newValue)")
}
variable change is notified?
at first I added above small code snippet to EditButton and Text.
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2022/04/11
// © 2022 SmallDeskSoftware
//
import SwiftUI
struct ContentView: View {
@Environment(\.editMode) var editMode
@State private var data = ["One", "Two", "Three"]
var body: some View {
NavigationView {
List {
ForEach(data, id: \.self) { data in
Text(editMode?.wrappedValue.isEditing == true ? "EditMode" : "Non-EditMode")
.onChange(of: editMode?.wrappedValue) { newValue in
print("EditMode@Text -> \(newValue)")
}
}
.onDelete { _ in } // remove
.onMove { _, _ in } // reorder
}
.toolbar {
EditButton()
.onChange(of: editMode?.wrappedValue) { newValue in
print("EditMode@EditButton -> \(newValue)")
}
}
}
}
}
Result: Clicking Edit button does not trigger “both” onChange. i.e. There are no way to know the editMode change for Text and EditButton.
Initially I expected notification will be sent to very limited Views. but not only EditButton but also Text did not receive.
# Of course, EditButton should know the change internally.
This result means editMode in ContentView does not have any change.
OK. but if so, how does NavigationView/List detect the change and change UI accordingly?
Consideration: Why no notification/no change?
I tried to google many times.
I found “There are many who is investigating this topic” and “No one has clear answer for this”.
Of course Apple should know, but looks they don’t provide detail documentation about this.
So I started to consider why by myself….
How widely does editMode be shared?
At first, I thought “editMode is defined as Environment variable, but can we refer this variable from anywhere?”
In iOS, usually there is only ONE List in the screen. so it looks OK to refer.
But considering macOS (or iPadOS), there might be many List in one screen.
This reminds me past my blog post about drag&drop.
“App which has 2 Lists in one screen can be crashed easily with using drag&drop”
[SwiftUI] List/ForEach の onMove についての メモ書き
From this, I thought if there are 2 lists those has separate “Edit” buttons, user want to make list in edit mode separately.)
Let’s try with 2 List
I found each List has individual editing state.
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2022/04/11
// © 2022 SmallDeskSoftware
//
import SwiftUI
struct ContentView: View {
@Environment(\.editMode) var editMode
@State private var data = ["One", "Two", "Three"]
@State private var anotherData = ["1", "2", "3", "4"]
var body: some View {
VStack {
NavigationView {
List {
ForEach(data, id: \.self) { data in
Text(editMode?.wrappedValue.isEditing == true ? "EditMode" : "Non-EditMode")
}
.onDelete { _ in } // remove
.onMove { _, _ in } // reorder
}
.toolbar {
EditButton()
}
}
NavigationView {
List {
ForEach(anotherData, id: \.self) { data in
Text(editMode?.wrappedValue.isEditing == true ? "EditMode" : "Non-EditMode")
}
.onDelete { _ in } // remove
.onMove { _, _ in } // reorder
}
.toolbar {
EditButton()
}
}
}
}
}
It is nice to have separate editing mode, but if so, what is the meaning of editMode in ContentView??
this make me in confusion more.
updates will be only in external child view
# consideration/trial are after summary.
(personal) summary
In person, I believe followings are correct.
– EditMode will be updated only in target external child view.
“Target” means element under NavigationView/ListView in this example.
“external” means it should NOT be defined in the view same with NavigationView/ListView.
Followings are my thought and trials.
# anyway only Apple knows the fact. but comments are appreciated.
consideration/trials
After consideration, following hypothesis popped up in my mind.
1) for editing mode, (from UI perspective) Navigation/List does not do anything. Only child views do some layout update.
2) it means Navigation/List does not need to know editing state. It is enough that child views know the state.
2′) additionally there might be other NavigationView/List in same view, so changing editMode might be dangerous.
3) Probably there is the chance to inject new variable only when creating external view.
4) so only external view has correct environment variable info.
past trial is done only in the same view with Navigation/List. if above hypothesis, it is natural to fail to get changed.
To confirm, I created external Text version.
In short, “BINGO!!”
The cell which use external Text is updated correctly. (i.e. “EditMode” appeared)
Followings are code that I used. But nothing special
overview
– TextInList is external TextView
– TextNotInList is external TextView which is same with TextInList. but for comparison, I gave different name.
– 2 List are used in this example, but no change in lower list.
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2022/04/11
// © 2022 SmallDeskSoftware
//
import SwiftUI
struct ContentView: View {
@Environment(\.editMode) var editMode
@State private var data = ["One", "Two", "Three"]
@State private var anotherData = ["1", "2", "3", "4"]
var body: some View {
VStack {
NavigationView {
List {
ForEach(data, id: \.self) { data in
TextInList()
}
.onDelete { _ in } // remove
.onMove { _, _ in } // reorder
}
.toolbar {
EditButton()
}
}
TextNotInList()
NavigationView {
List {
ForEach(anotherData, id: \.self) { data in
Text(editMode?.wrappedValue.isEditing == true ? "EditMode" : "Non-EditMode")
}
.onDelete { _ in } // remove
.onMove { _, _ in } // reorder
}
.toolbar {
EditButton()
}
}
}
}
}
struct TextNotInList: View {
@Environment(\.editMode) var editMode
var body: some View {
Text(editMode?.wrappedValue.isEditing == true ? "EditMode" : "Non-EditMode")
}
}
struct TextInList: View {
@Environment(\.editMode) var editMode
var body: some View {
Text(editMode?.wrappedValue.isEditing == true ? "EditMode" : "Non-EditMode")
}
}
Background(my assumption)
After this investigation, I believe followings are the
NavigationView/ListView has local state for editMode. They will give local state as environment for their child view.
But only for views which will be affected by editMode.
# I believe this can be true not only for NavigationView/ListView but also for the view which is supported by EditButton.
As usual :(, Apple documents does not say anything. but I start to consider this would be reasonable implementation for this.
how to reflect editMode to NavigationView/List
In addition, I’ll try to explain how to reflect editMode change to NavigationView/List.
as mentioned above, there is no straight way.
for this, we need to pass data from child view to parent view.
In SwiftUI, there is Preference for supporting such request.
in following example, I used Preference to pass editMode value from child to parent, then NavigationViewTitle is changed accordingly.
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2022/04/11
// © 2022 SmallDeskSoftware
//
import SwiftUI
struct ContentView: View {
@State private var data = ["One", "Two", "Three"]
@State private var title = "List"
var body: some View {
NavigationView {
List {
ForEach(data, id: \.self) { data in
TextInList()
.onPreferenceChange(EditModePreferenceKey.self) { newValue in
title = newValue ? "List under editing" : "List"
}
}
.onDelete { _ in } // remove
.onMove { _, _ in } // reorder
}
.toolbar {
EditButton()
}
.navigationBarTitle(title)
}
}
}
struct EditModePreferenceKey: PreferenceKey {
typealias Value = Bool
static var defaultValue: Bool = false
static func reduce(value: inout Bool, nextValue: () -> Bool) {
value = nextValue()
}
}
struct TextInList: View {
@Environment(\.editMode) var editMode
var body: some View {
Text(editMode?.wrappedValue.isEditing == true ? "EditMode" : "Non-EditMode")
.preference(key: EditModePreferenceKey.self, value: editMode?.wrappedValue.isEditing ?? false)
}
}
if you put above into ViewModifier, it will looks like .navigationTitle.
[SwiftUI] ViewModifierの作り方
Issue
In above example, parent view get the info from child view.
but in other words, without child view parent view can not get any info.
how to reflect editMode to NavigationView/List (cheating…)
With considering above issue, in some cases, implementing own EditButton would be better than using EditButton.
Reference
For this post, I learned a lot from followings. (sorry in Japanese)
参考
【SwiftUI】編集モードの取得に関する問題カピ通信
Summary
In this post, I explained “How to access .editMode”.
- editMode will be updated only in target external child view.
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link