SwiftUI macOS Edit Mode

So I really wanted to have the same EditButton when we develop our iOS apps in SwiftUI. To reuse as much as possible between both platforms I got motivated to make my own implementation of of the EditButton.

Fully functioning code could be found here.

iOS EditMode usage

This is what we want to implement for macOS and is the current way of how we use it on iOS.

struct ContentView: View {
    /// Environment key to get the current 
    /// EditMode state
    @Environment(\.editMode) var editMode: EditMode

    var body: some View {
        /// Button to toggle the edit mode
        EditButton()
    }
}

EditMode model

Let's first start of with implementing the EditMode model. The EditMode model represents the current state of editing.

enum EditMode {
    case active
    case inactive

    var isEditing: Bool {
        self == .active
    }
}

Making EditMode accessible from the environment

To make our EditMode state accessible from the @Environment property wrapper we need to create our own EnvironmentKey to reference and modify the current edit mode. The static property defaultValue is required for the EnvironmentKey to conform to the protocol.

struct EditModeKey: EnvironmentKey {
    static let defaultValue: EditMode = .inactive
}

Next we need to extend EnvironmentValues with and edit mode key. This how later on access it using the @Environment property wrapper using a key path.

extension EnvironmentValues {
    var editMode: EditMode {
        get { self[EditModeKey.self] }
        set { self[EditModeKey.self] = newValue }
    }
}

Updating the edit mode state

From the ContentView the editMode state is just a regular State property we always knew and we can edit it's value like we always did. One thing that might be new is using the environment view modifier.

Everytime we modify the editMode State property the changes reflect to the child views that are accessing the @Environment property wrapper of editMode.

struct ContentView: View {
    @State private var editMode: EditMode = .inactive

    var body: some View {
        VStack {
            Button(action: {
                editMode = editMode.isEditing ? .inactive : .active
            }) {
                Text(editMode.isEditing ? "Done" : "Edit")
            }
            ChildView()
        }
        // Setting the state value the environment
        .environment(\.editMode, editMode)
    }
}

struct ChildView: View {
    // This value behaves like a property value that has been passed
    // through by it's parent
    @Environment(\.editMode) var editMode

    var body: some View {
        Text("I'm \(editMode.isEditing ? "editing" : "not editing")")
    }
}

Bonus: Implementing EditButton

EditMode view modifier

To make it easier for ourself let's create a view modifier to modify edit mode in a uniform way.

I decided to use NotificationCenter to loosely couple updating the EditMode. So we can post a new EditMode state to this view modifier and the state will get handled accordingly.

struct EditModeViewModifier: ViewModifier {
    @State private var editMode: EditMode = .inactive

    func body(content: Content) -> some View {
        content
            // Setting the state value the environment
            .environment(\.editMode, editMode)
            .onReceive(NotificationCenter.default
                // Using the Notification.Name "change_edit_mode" to recieve edit mode changes
                .publisher(for: Notification.Name(rawValue: "change_edit_mode")), perform: { output in
                    guard let newEditMode = output.object as? EditMode, newEditMode != editMode else { return }
                    // Updating the edit mode state when publisher has recieved a new value.
                    editMode = newEditMode
                })
    }
}

extension View {
    // Our uniform view modifier
    func withEditMode() -> some View {
        self
            .modifier(EditModeViewModifier())
    }
}

EditButton

This loosely coupled view can toggle between EditMode's by posting to NotificationCenter.

struct EditButton: View {
    @Environment(\.editMode) var editMode

    var body: some View {
        Button(action: {
            NotificationCenter.default.post(
                // Using the Notification.Name "change_edit_mode" to post new edit mode changes
                name: Notification.Name(rawValue: "change_edit_mode"),
                object: editMode.isEditing ? EditMode.inactive : EditMode.active,
                userInfo: nil)
        }) {
            Text(editMode.isEditing ? "Done" : "Edit")
        }
    }
}

Usage

Now let's use our convient EditButton with our also very convinient view modifier.

struct ContentView: View {
    var body: some View {
        VStack {
            EditButton()
            ChildView()
        }
        .withEditMode()
    }
}

🎉 That's all, now we have EditMode when developing our apps for macOS. 🎉