Skip to content

Commit

Permalink
The first version.
Browse files Browse the repository at this point in the history
  • Loading branch information
Iaenhaall committed Mar 19, 2021
1 parent a6bb7d9 commit 7623af5
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 3 deletions.
Binary file added Images/Example.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
98 changes: 97 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,97 @@
# UndoController
# UndoController

The controller that allows a user to undo an action within seconds. It looks like the system HUDs.

![Example](Images/Example.gif)



## Installation

1. Add **UndoController.swift** file to your project.

That's all. There are no dependencies.



## Usage

You can download and run the project to explore the `UndoController` features.

These are the main points to pay attention to.

1. Create an `UndoController` instance.

```swift
@StateObject private var undoController: UndoController = UndoController()
```

If you are not satisfied with the standard indents, use the `UndoController(indents:)` initializer.

```swift
@StateObject private var undoController: UndoController = UndoController(indents: EdgeInsets(top: 0, leading: 20, bottom: 20, trailing: 20))
```

2. Add `UndoController` to the desired View.

It is recommended to add to the view that occupies the entire screen area. For example, to `NavigationView`.

```swift
NavigationView {
List {
ForEach(names, id: \.self) { name in
NavigationLink(destination: Text(name).padding().font(.largeTitle)) {
Text(name)
.padding()
}
}
.onDelete(perform: delete)
}
// .add(undoController) // The next View in the NavigationView stack overlaps UndoController when the NavigationLink is activated.
}
.add(undoController) // Stays above NavigationView when NavigationLink is activated.
```

To access `UndoController` from nested Views, `EnvironmentObject` wrapper can be used.

In this case, the `UndoController`, will be available from any application View.

```swift
struct Application: App {
@StateObject private var undoController: UndoController = UndoController()

var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(undoController)
.add(undoController)
}
}
}
```

**Note**, in this case the `UndoController` will overlap Views such as `TabView`. To avoid this, add the controller only to necessary Views (`NavigationView`, `List`, etc.) or increase its indentation.

3. Call the `UndoController` when needed. In this case, it happens when user deletes a name from the `List`.

Please note that when the **Show** method is called multiple times, all closures (except the last one) required to undo user actions are removed.

```swift
func delete(at offsets: IndexSet) {
...
// Shows UndoController after name is deleted.
undoController.show(message: "After deletion, the name cannot be restored!") {
// Actions required to undo name deletion.
}
}
```

To undo actions you can write your own code or use the system [UndoManager](https://developer.apple.com/documentation/foundation/undomanager).

The **Show** function accepts the following parameters:

* **message**: The message text that is displayed on the `UndoController`.
* **time**: The `UndoController` lifetime. *The default value is 5 sec.*
* **timerAction**: The action that will be performed if the `UndoController`'s life time has expired. *It can be nil.*
* **undoAction**: The action that will be performed if a user undoes the action.

12 changes: 12 additions & 0 deletions UndoController.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@
objects = {

/* Begin PBXBuildFile section */
BD0F1664260397E20057B61E /* ListExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0F1663260397E20057B61E /* ListExample.swift */; };
BD42E9BB260278A500E66A85 /* UndoControllerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD42E9BA260278A500E66A85 /* UndoControllerApp.swift */; };
BD42E9BD260278A500E66A85 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD42E9BC260278A500E66A85 /* ContentView.swift */; };
BD42E9BF260278A800E66A85 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BD42E9BE260278A800E66A85 /* Assets.xcassets */; };
BD42E9C2260278A800E66A85 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BD42E9C1260278A800E66A85 /* Preview Assets.xcassets */; };
BD42E9CB2602796F00E66A85 /* UndoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD42E9CA2602796F00E66A85 /* UndoController.swift */; };
BD77FC3926046F1000586808 /* AppExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD77FC3826046F1000586808 /* AppExample.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
BD0F1663260397E20057B61E /* ListExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListExample.swift; sourceTree = "<group>"; };
BD42E9B7260278A500E66A85 /* UndoController.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UndoController.app; sourceTree = BUILT_PRODUCTS_DIR; };
BD42E9BA260278A500E66A85 /* UndoControllerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UndoControllerApp.swift; sourceTree = "<group>"; };
BD42E9BC260278A500E66A85 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
BD42E9BE260278A800E66A85 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
BD42E9C1260278A800E66A85 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
BD42E9C3260278A800E66A85 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BD42E9CA2602796F00E66A85 /* UndoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UndoController.swift; sourceTree = "<group>"; };
BD77FC3826046F1000586808 /* AppExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExample.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -53,7 +59,10 @@
isa = PBXGroup;
children = (
BD42E9BA260278A500E66A85 /* UndoControllerApp.swift */,
BD42E9CA2602796F00E66A85 /* UndoController.swift */,
BD42E9BC260278A500E66A85 /* ContentView.swift */,
BD0F1663260397E20057B61E /* ListExample.swift */,
BD77FC3826046F1000586808 /* AppExample.swift */,
BD42E9BE260278A800E66A85 /* Assets.xcassets */,
BD42E9C3260278A800E66A85 /* Info.plist */,
BD42E9C0260278A800E66A85 /* Preview Content */,
Expand Down Expand Up @@ -139,7 +148,10 @@
buildActionMask = 2147483647;
files = (
BD42E9BD260278A500E66A85 /* ContentView.swift in Sources */,
BD77FC3926046F1000586808 /* AppExample.swift in Sources */,
BD42E9BB260278A500E66A85 /* UndoControllerApp.swift in Sources */,
BD42E9CB2602796F00E66A85 /* UndoController.swift in Sources */,
BD0F1664260397E20057B61E /* ListExample.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
38 changes: 38 additions & 0 deletions UndoController/AppExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// AppExample.swift
// UndoController
//
// Created by Никита Белокриницкий on 19.03.2021.
//

import SwiftUI

struct AppExample: View {
@State private var text: String = ""
@EnvironmentObject private var undoController: UndoController

var body: some View {
VStack {
Button("Show UndoController") {
undoController.show(message: "UndoController message!",
time: 3,
timerAction: {
print("Timer")
},
undoAction: {
print("Undo!")
})
}

TextField("Enter text", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
}
}

struct AppExample_Previews: PreviewProvider {
static var previews: some View {
AppExample()
}
}
13 changes: 11 additions & 2 deletions UndoController/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,17 @@ import SwiftUI

struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
TabView {
ListExample()
.tabItem {
Label("List", systemImage: "list.bullet")
}

AppExample()
.tabItem {
Label("App", systemImage: "app")
}
}
}
}

Expand Down
56 changes: 56 additions & 0 deletions UndoController/ListExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// ListExample.swift
// UndoController
//
// Created by Никита Белокриницкий on 18.03.2021.
//

import SwiftUI

struct ListExample: View {
@State private var names: [String] = [
"Elizabeth", "James", "Jennifer", "John", "Linda", "Mary", "Michael", "Patricia", "Robert", "William"
]
@StateObject private var undoController: UndoController = UndoController()

var body: some View {
NavigationView {
List {
ForEach(names, id: \.self) { name in
NavigationLink(destination: Text(name).padding().font(.largeTitle)) {
Text(name)
.padding()
}
}
.onDelete(perform: delete)
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Names")
// .add(undoController) // The next View in the NavigationView stack overlaps UndoController when the NavigationLink is activated.
}
.navigationViewStyle(StackNavigationViewStyle())
.add(undoController) // Stays above NavigationView when NavigationLink is activated.
}

func delete(at offsets: IndexSet) {
var removedNames: [Int: String] = [:]

for index in offsets {
removedNames.updateValue(names[index], forKey: index)
names.remove(at: index)
}

// Shows UndoController after name is deleted.
undoController.show(message: "After deletion, the name cannot be restored!", time: 5) {
for index in removedNames.keys.sorted() {
names.insert(removedNames[index]!, at: index)
}
}
}
}

struct ListExample_Previews: PreviewProvider {
static var previews: some View {
ListExample()
}
}
118 changes: 118 additions & 0 deletions UndoController/UndoController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//
// UndoController.swift
// UndoController
//
// Created by Никита Белокриницкий on 17.03.2021.
//

import SwiftUI

/// The controller that allows a user to undo an action within a few seconds. It looks like the system HUDs.
final public class UndoController: ObservableObject {
/// Boolean variable indicating whether the `UndoController` is currently displayed or not.
@Published fileprivate(set) var isPresented: Bool = false
/// Time in seconds remaining before the `UndoController` disappears.
@Published private(set) var seconds: Double = 0
/// The message text that is displayed on the `UndoController`.
private(set) var message: String = ""
/// The action that will be performed if the `UndoController`'s life time has expired.
private(set) var timerAction: (() -> ())?
/// The action that will be performed if a user undoes the action.
private(set) var undoAction: (() -> ())?
fileprivate let indents: EdgeInsets
private var timer: Timer?

/**
Creates an UndoController instance.

- parameter padding: An optional parameter that allows to set indents for UndoController from the boundaries of the view to which it is added.
*/
public init(indents: EdgeInsets = EdgeInsets(top: 0, leading: 20, bottom: 20, trailing: 20)) {
self.indents = indents
}

/**
Displays the `UndoController` with the specified parameters.

- parameter message: The message text that is displayed on the `UndoController`.
- parameter seconds: The `UndoController` lifetime.
- parameter timerAction: The action that will be performed if the `UndoController`'s life time has expired.
- parameter undoAction: The action that will be performed if a user undoes the action.
*/
public func show(message: String, time seconds: UInt = 5, timerAction: (() -> ())? = nil, undoAction: @escaping () -> ()) {
DispatchQueue.main.async {
self.reset()
self.message = message
self.seconds = Double(seconds > 99 ? 99 : seconds)
self.timerAction = timerAction
self.undoAction = undoAction

self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.seconds -= 1
if self.seconds == 0 {
withAnimation { self.isPresented = false }
if let timerAction = timerAction { timerAction() }
self.reset()
}
}

withAnimation { self.isPresented = true }
}
}

/// Resets the controller parameters to their initial values.
fileprivate func reset() {
self.timer?.invalidate()
self.timer = nil
self.seconds = 0
self.message = ""
self.timerAction = nil
self.undoAction = nil
}
}

extension View {
/**
Adds the `UndoController` to the view.

- parameter undoController: `UndoController` with the specified parameters.
- returns: ZStack that contains the view and UndoController.

# Notes: #
1. It is recommended to embed in a view that takes up the entire screen area.
2. To access `UndoController` from nested views, `EnvironmentObject` wrapper can be used.
*/
func add(_ undoController: UndoController) -> some View {
ZStack(alignment: .bottom) {
self

if undoController.isPresented {
HStack(alignment: .center, spacing: 10) {
Text(String(Int8(undoController.seconds)))
.font(.title)
.frame(width: 36) // The fixed width prevents the jerking of the controller during timer operation.

Text(undoController.message)

Button(action: {
withAnimation { undoController.isPresented = false }
undoController.undoAction!()
undoController.reset()
}, label: {
Text("Undo", comment: "The undo button text on the UndoController.")
.font(.headline)
})
}
.padding(EdgeInsets(top: 16, leading: 23, bottom: 16, trailing: 23))
.background(
Capsule()
.foregroundColor(Color(UIColor.tertiarySystemBackground))
.shadow(color: Color(.gray).opacity(0.35), radius: 12, x: 0, y: 5)
)
.padding(undoController.indents)
.transition(AnyTransition.move(edge: .bottom).combined(with: .opacity))
.zIndex(.infinity)
}
}
}
}
7 changes: 7 additions & 0 deletions UndoController/UndoControllerApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ import SwiftUI

@main
struct UndoControllerApp: App {
@StateObject private var undoController: UndoController = UndoController()

var body: some Scene {
WindowGroup {
ContentView()
// Example of using the UndoController in nested views using EnvironmentObject.
// Note, in this case the UndoController will overlap the TabView.
// This happens because UndoController is added to the view that contains the TabView.
.environmentObject(undoController)
.add(undoController)
}
}
}

0 comments on commit 7623af5

Please sign in to comment.