How to use #FocusState with view models? - swiftui

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
}
}
}

Related

Infinite loop when setting a #Published property

I have a simple view that shows some photos, through a list. Clicking on any row should display a detailed view of that photo. I'm using the MVVM pattern. However, an infinite loop occurs when I try to set the “selectedPhoto” property of the view model. Is there any way to avoid this loop without having to create a property in the detailed view itself?
Here is the Photo struct:
struct Photo: Identifiable {
var id = UUID()
var name: String
}
Here is the ContentView with an extension (the “updatePhoto” method is causing the infinite loop):
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.photos) { selectedPhoto in
showDetailView(with: selectedPhoto)
}
}
.navigationTitle("Favorite Photo")
}
}
}
extension ContentView {
func showDetailView(with selectedPhoto: Photo?) -> some View {
if let selectedPhoto = selectedPhoto {
viewModel.updatePhoto(selectedPhoto)
}
return DetailView(viewModel: viewModel)
}
}
Here is the view model:
class ViewModel: ObservableObject {
#Published var photos = [
Photo(name: "Photo 1"),
Photo(name: "Photo 2"),
Photo(name: "Photo 3")
]
#Published var selectedPhoto: Photo?
func updatePhoto(_ selectedPhoto: Photo?) {
self.selectedPhoto = selectedPhoto
}
}
And here is the DetailView:
struct DetailView: View {
#ObservedObject private var viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
var body: some View {
Text(viewModel.selectedPhoto?.name ?? "Unknown photo name")
}
}
Try this approach, using a NavigationLink to present the DetailView,
and passing the selectedPhoto to it using #State var selectedPhoto: Photo.
struct Photo: Identifiable {
let id = UUID()
var name: String
}
class ViewModel: ObservableObject {
#Published var photos = [Photo(name: "Photo 1"),Photo(name: "Photo 2"),Photo(name: "Photo 3")]
}
struct DetailView: View {
#State var selectedPhoto: Photo
var body: some View {
Text(selectedPhoto.name)
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.photos) { selectedPhoto in
NavigationLink(selectedPhoto.name, destination: DetailView(selectedPhoto: selectedPhoto))
}
}
.navigationTitle("Favorite Photo")
}
}
}
Note that NavigationView is being deprecated and you will have to use NavigationStack instead.

SwiftUI - Binding in ObservableObject

Let's say we have a parent view like:
struct ParentView: View {
#State var text: String = ""
var body: some View {
ChildView(text: $text)
}
}
Child view like:
struct ChildView: View {
#ObservedObject var childViewModel: ChildViewModel
init(text: Binding<String>) {
self.childViewModel = ChildViewModel(text: text)
}
var body: some View {
...
}
}
And a view model for the child view:
class ChildViewModel: ObservableObject {
#Published var value = false
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
...
}
Making changes on the String binding inside the child's view model makes the ChildView re-draw causing the viewModel to recreate itself and hence reset the #Published parameter to its default value. What is the best way to handle this in your opinion?
Cheers!
The best way is to use a custom struct as a single source of truth, and pass a binding into child views, e.g.
struct ChildViewConfig {
var value = false
var text: String = ""
// mutating funcs for logic
mutating func reset() {
text = ""
}
}
struct ParentView: View {
#State var config = ChildViewConfig()
var body: some View {
ChildView(config: $config)
}
}
struct ChildView: View {
#Binding var config: ChildViewConfig
var body: some View {
TextField("Text", text: $config.text)
...
Button("Reset") {
config.reset()
}
}
}
"ViewConfig can maintain invariants on its properties and be tested independently. And because ViewConfig is a value type, any change to a property of ViewConfig, like its text, is visible as a change to ViewConfig itself." [Data Essentials in SwiftUI WWDC 2020].

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")
}
}
}

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 = ""
}

UndoManager's canUndo property not updating in 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
)
...