-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
339 additions
and
3 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters