UndoManager's canUndo property not updating in SwiftUI - swiftui

Why does the #Environment UndoManager not update its canUndo property when it has actions in its stack? I have a view that has a child that can utilize the un/redo functionality, but for some reason I can't disable the undo button based on the manager.
struct MyView: View {
#Environment(\.undoManager) var undoManager: UndoManager?
var body: some View {
Button("Undo") { ... }
.disabled(!self.undoManager!.canUndo)
}
}

UndoManager.canUndo is not KVO compliant, so use some notification publisher to track state, like below
struct MyView: View {
#Environment(\.undoManager) var undoManager
#State private var canUndo = false
// consider also other similar notifications
private let undoObserver = NotificationCenter.default.publisher(for: .NSUndoManagerDidCloseUndoGroup)
var body: some View {
Button("Undo") { }
.disabled(!canUndo)
.onReceive(undoObserver) { _ in
self.canUndo = self.undoManager!.canUndo
}
}
}

When it comes to canRedo I tried multiple things, and what I ended up with is this - so observing viewModel (or document or any other undo-supporting data source) and updating canUndo/canRedo in reaction to it's change:
struct MyView: View {
#ObservedObject var viewModel: ViewModel
#Environment(\.undoManager) private var undoManger: UndoManager!
#State private var canUndo = false
#State private var canRedo = false
var body: some View {
RootView()
.onReceive(viewModel.objectWillChange) { _ in
canUndo = undoManger.canUndo
canRedo = undoManger.canRedo
}
if canUndo {
Button(
action: { undoManger?.undo() },
label: { Text("Undo") }
)
}
if canRedo {
Button(
action: { undoManger?.redo() },
label: { Text("Redo") }
)
}
...
I also wrapped it in a standalone button (without overgeneralizing the implementation above my own needs) that eliminates the boilerplate from my view and keeps complexity more private so it ends up like this for me:
struct MyView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
RootView()
UndoManagerActionButton(
.undo,
willChangePublisher: viewModel.objectWillChange
)
UndoManagerActionButton(
.redo,
willChangePublisher: viewModel.objectWillChange
)
...

Related

How to establish communication between ViewModels of the same screen in SwiftUI MVVM

In my app, I have a Screen with Toolbar and Main View.
VStack {
ToolbarView()
MainView()
}
Think of it like this:
Toolbar has its own View and ToolbarViewModel where we can select “Tools”
struct ToolbarView: View {
#StateObject private var VM = ToolbarViewModel()
var body: some View {
Text("Toolbar View")
}
}
#MainActor final class ToolbarViewModel: ObservableObject {
var selectedTool: Int = 1
func selectTool() {
//We select a new tool
selectedTool = 2
}
}
Main view has its own View and MainViewModel
struct MainView: View {
#StateObject private var VM = MainViewModel()
var body: some View {
Text("Main View")
}
}
#MainActor final class MainViewModel: ObservableObject {
var selectedTool: Int = 1
}
Now, when I tap a button in the ToolbarView and call a function in ToolbarViewModel to select a new tool, the tool must change in the MainViewModel too.
What would be the correct way of implementing this?
In the screen with the MainView and ToolbarView instances, create #StateObject(s) for both
#StateObject private var mainVM = MainViewModel()
#StateObject private var toolbarVM = ToolbarViewModel()
Then, create Observed objects in both your instances:
ToolbarView:
struct ToolbarView: View {
#ObservedObject var VM: ToolbarViewModel
var body: some View {
Text("Toolbar View")
}
}
MainView:
struct MainView: View {
#ObservedObject var VM: MainViewModel
var body: some View {
Text("Main View")
}
}
Then pass your objects in the screen than you created your instances:
VStack {
ToolbarView(VM: toolbarVM)
MainView(VM: mainVM)
}
Finally, whenever you make a change you can just listen to it like:
VStack {
ToolbarView(VM: toolbarVM)
MainView(VM: mainVM)
}
.onChange(of: toolbarVM.isDrawing) { newValue in {
mainVM.isDrawing = newValue
}
.onChange(of: mainVM.isDrawing) { newValue in {
toolbarVM.isDrawing = newValue
}
We don't use view model objects in SwiftUI. The View data struct is already the view model that SwiftUI uses to create/update/remove UIView objects automatically for us. The property wrappers give the struct reference semantics giving us the best of both worlds. You'll have to learn #State and #Binding and put the shared state in a parent View, then pass it down as a let for read access or #Bindng var for write access, e.g.
#State var tools = Tools()
...
VStack {
ToolbarView(tools: $tools)
MainView(tools: tools)
}
struct Tools {
var selectedTool: Int = 1
mutating func selectTool() {
//We select a new tool
selectedTool = 2
}
}
struct MainView: View {
let tools: Tools
var body: some View {
Text("Main View \(tools.selected)")
}
}
struct ToolbarView: View {
#Binding var tools: Tools
var body: some View {
Text("Toolbar View")
Button("Select") {
tools.selectTool()
}
}
}

SwiftUI Control Flow and ViewBuilder

I want to have a mother view that displays a different child view based on a #State bool.
However I get two errors when doing that.
On the start of the body closure:
Struct 'ViewBuilder' requires that 'EmptyCommands' conform to 'View'
Return type of property 'body' requires that 'EmptyCommands' conform
to 'View'
And inside the control flow statement:
Closure containing control flow statement cannot be used with result
builder 'CommandsBuilder'
struct ResultView: View {
#State var resultViewSuccess = false
let resultViewModel: ResultViewModel
var body: some View {
Group {
if let showresultView = resultViewSuccess {
ViewOne(viewModel: resultViewModel)
} else {
ViewTwo(
resultViewSuccess: $resultViewSuccess,
viewModel: resultViewModel,
)
}
}
}
}
struct ViewTwo: View {
#Binding var resultViewSuccess: Bool
#StateObject var viewModel: ResultViewModel
var body: some View {
NavigationView {
ButtonResult(
resultViewSuccess: $resultViewSuccess,
viewModel: viewModel)
}
}
struct ButtonResult: View {
#Binding var resultViewSuccess: Bool
#StateObject var viewModel: ResultViewModel
var body: some View {
Button(action: {
self.resultViewSuccess = true
}) {
Text("View Results")
}
}
}

How to use #FocusState with view models?

I'm using view models for my SwiftUI app and would like to have the focus state also in the view model as the form is quite complex.
This implementation using #FocusState in the view is working as expected, but not want I want:
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
Button("Set Focus") {
hasFocus = true
}
}
}
}
class ViewModel: ObservableObject {
#Published var textField: String = ""
}
How can I put the #FocusState into the view model?
Assuming you have in ViewModel as well
class ViewModel: ObservableObject {
#Published var hasFocus: Bool = false
...
}
you can use it like
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
}
.onChange(of: hasFocus) {
viewModel.hasFocus = $0 // << write !!
}
.onAppear {
self.hasFocus = viewModel.hasFocus // << read !!
}
}
}
as well as the same from Button if any needed.
I faced the same problem and ended up writing an extension that can be reused to sync both values. This way the focus can also be set from the view model side if needed.
class ViewModel: ObservableObject {
#Published var hasFocus: Bool = false
}
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
}
.sync($viewModel.hasFocus, with: _hasFocus)
}
}
extension View {
func sync<T: Equatable>(_ binding: Binding<T>, with focusState: FocusState<T>) -> some View {
self
.onChange(of: binding.wrappedValue) {
focusState.wrappedValue = $0
}
.onChange(of: focusState.wrappedValue) {
binding.wrappedValue = $0
}
}
}

Missing argument for parameter 'View Call' in call

I am struggle with understanding about why i have to give Popup view dependency named vm while calling this view since it is observable
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView() /// this line shows error
}
}
}
struct DetailView:View {
#ObservedObject var vm:ViewModel
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
You have to set your vm property when you init your View. Which is the usual way.
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView(vm: ViewModel()) // Initiate your ViewModel() and pass it as DetailView() parameter
}
}
}
struct DetailView:View {
var vm: ViewModel
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
Or you could use #EnvironmentObject. You have to pass an .environmentObject(yourObject) to the view where you want to use yourObject, but again you'll have to initialize it before passing it.
I'm not sure it's the good way to do it btw, as an environmentObject can be accessible to all childs view of the view you declared the .environmentObject on, and you usually need one ViewModel for only one View.
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView().environmentObject(ViewModel()) // Pass your ViewModel() as an environmentObject
}
}
}
struct DetailView:View {
#EnvironmentObject var vm: ViewModel // you can now use your vm, and access it the same say in all childs view of DetailView
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}

How to swap my #State of my SwiftUI view for my view model #Published variable?

I have a button that triggers my view state. As I have now added a network call, I would like my view model to replace the #State with its #Publihed variable to perform the same changes.
How to use my #Published in the place of my #State variable?
So this is my SwiftUI view:
struct ContentView: View {
#ObservedObject var viewModel = OnboardingViewModel()
// This is the value I want to use as #Publisher
#State var isLoggedIn = false
var body: some View {
ZStack {
Button(action: {
// Before my #State was here
// self.isLoggedIn = true
self.viewModel.login()
}) {
Text("Log in")
}
if isLoggedIn {
TutorialView()
}
}
}
}
And this is my model:
final class OnboardingViewModel: ObservableObject {
#Published var isLoggedIn = false
private var subscriptions = Set<AnyCancellable>()
func demoLogin() {
AuthRequest.shared.login()
.sink(
receiveCompletion: { print($0) },
receiveValue: {
// My credentials
print("Login: \($0.login)\nToken: \($0.token)")
DispatchQueue.main.async {
// Once I am logged in, I want this
// value to change my view.
self.isLoggedIn = true } })
.store(in: &subscriptions)
}
}
Remove state and use view model member directly, as below
struct ContentView: View {
#ObservedObject var viewModel = OnboardingViewModel()
var body: some View {
ZStack {
Button(action: {
self.viewModel.demoLogin()
}) {
Text("Log in")
}
if viewModel.isLoggedIn { // << here !!
TutorialView()
}
}
}
}
Hey Roland I think that what you are looking for is this:
$viewMode.isLoggedIn
Adding the $ before the var will ensure that SwiftUI is aware of its value changes.
struct ContentView: View {
#ObservedObject var viewModel = OnboardingViewModel()
var body: some View {
ZStack {
Button(action: {
viewModel.login()
}) {
Text("Log in")
}
if $viewMode.isLoggedIn {
TutorialView()
}
}
}
}
class OnboardingViewModel: ObservableObject {
#Published var isLoggedIn = false
func login() {
isLoggedIn = true
}
}