SwiftUI store View as a variable while passing it some bindings - swift

I want to store a View as a variable for later use, while passing that View some Bindings.
Here's what I've tried:
struct Parent: View {
#State var title: String? = ""
var child: Child!
init() {
self.child = Child(title: self.$title)
}
var body: some View {
VStack {
child
//...
Button(action: {
self.child.f()
}) {
//...
}
}
}
}
struct Child: View {
#Binding var title: String?
func f() {
// complex work from which results a string
self.title = <that string>
}
var body: some View {
// ...
}
}
It compiles correctly and the View shows as expected, however when updating from the child the passed Binding from the parent, the variable never gets updated. You can even do something like this (from the child):
self.title = "something"
print(self.title) // prints the previous value, in this case nil
I don't know if this is a bug or not, but directly initializing the child in the body property does the trick. However, I need that child as a property to access its methods.

If you want to change something from Parent for the child, binding is the right way. If that's complicated, you have to use DataModel.
struct Parent: View {
#State var title: String? = ""
var body: some View {
VStack {
Child(title: $title)
Button(action: {
self.title = "something"
}) {
Text("click me")
}
}
}
}
struct Child: View {
#Binding var title: String?
var body: some View {
Text(title ?? "")
}
}

This is counter to the design of the SwiftUI framework. You should not have any persistent view around to call methods on. Instead, views are created and displayed as needed in response to your app's state changing.
Encapsulate your data in an ObservableObject model, and implement any methods you need to call on that model.
Update
It is fine to have such a function defined in Child, but you should only be calling it from within the Child struct definition. For instance, if your child view contains a button, that button can call the child's instance methods. For example,
struct Parent: View {
#State private var number = 1
var body: some View {
VStack {
Text("\(number)")
Child(number: $number)
}
}
}
struct Child: View {
#Binding var number: Int
func double() {
number *= 2
}
var body: some View {
HStack {
Button(action: {
self.double()
}) {
Text("Double")
}
}
}
}
But you wouldn't try to call double() from outside the child struct. If you wanted a function that can be called globally, put it in a data model. This is especially true if the function call is making network requests, as the model will stick around outside your child view, even if it is recreated due to layout changing.
class NumberModel: ObservableObject {
#Published var number = 1
func double() {
number *= 2
}
}
struct Parent: View {
#ObservedObject var model = NumberModel()
var body: some View {
VStack {
Text("\(model.number)")
Button(action: {
self.model.double()
}) {
Text("Double from Parent")
}
Child(model: model)
}
}
}
struct Child: View {
#ObservedObject var model: NumberModel
var body: some View {
HStack {
Button(action: {
self.model.double()
}) {
Text("Double from Child")
}
}
}
}

Related

SwiftUI: How to update element in ForEach without necessity to update all elements?

Imagine that you have some parent view that generate some number of child views:
struct CustomParent: View {
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index)
}
}
}
}
struct CustomChild: View {
#State var index: Int
#State private var text: String = ""
var body: some View {
Button(action: {
// Here should be some update of background/text/opacity or whatever.
// So how can I update background/text/opacity or whatever for button with index for example 3 from button with index for example 1?
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Question is included in the code as comment.
Thanks!
UPDATE:
First of all really thanks for all of your answers, but now imagine that you use mentioned advanced approach.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
If I use let _ = Self._printChanges() method in CustomChildView, to catch UI updates/changes, it'll print that every element in ForEach was updated/changed on button action.
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
let _ = Self._printChanges() // This have been added to code
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}
And now imagine that you have for example 1000 custom elements which have some background, opacity, shadow, texts, fonts and so on. Now I change text in any of the elements.
Based on log from let _ = Self._printChanges() method, it goes through all elements, and all elements are updated/changed what can cause delay.
Q1: Why did update/change all elements, if I change text in only one element?
Q2: How can I prevent update/change all elements, if I change only one?
Q3: How to update element in ForEach without necessity to update all elements?
Simpler Approach:
Although child views cannot access things that the host views have, it's possible to declare the child states in the host view and pass that state as a binding variable to the child view. In the code below, I have passed the childTexts variable to the child view, and (for your convenience) initialized the text so that it binds to the original element in the array (so that your onAppear works properly). Every change performed on the text and childTexts variable inside the child view reflects on the host view.
I strongly suggest not to do this though, as more elegant approaches exist.
struct CustomParent: View {
#State var childTexts = [String](repeating: "", count: 10)
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, childTexts: $childTexts)
}
}
}
}
struct CustomChild: View {
let index: Int
#Binding private var text: String
#Binding private var childTexts: [String]
init(index: Int, childTexts: Binding<[String]>) {
self.index = index
self._childTexts = childTexts
self._text = childTexts[index]
}
var body: some View {
Button(action: {
//button behaviors goes here
//for example
childTexts[index + 1] = "A"
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Advanced Approach:
By using the Combine framework, all your logics can be moved into an ObservableObject view model. This is much better as the button logic is no longer inside the view. In simplest terms, the #Published variable in the ObservableObject will publish a change when it senses its own mutation, while the #StateObjectand the #ObservedObject will listen and recalculate the view for you.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}

How to pass a FocusState variable to other views and allow those views to update the variable? [Swift]

Currently, I have something like the following:
struct ViewA: View {
#FocusState private var focusedField: Bool
var body: some View {
ViewB(focusedField: $focusedField)
// some other views that read focusedField...
}
}
struct ViewB: View {
#State var focusedField: FocusState<Bool>.Binding
var body: some View {
Button(action: {
focusedField = true // ERROR: Cannot assign value of type 'Bool' to type 'FocusState<Bool>'
})
// ...
}
}
Seems like I can pass down focusedField with no problem but unable to update its value. How to solve this?
For consistency, maybe it's better to use the property wrapper #FocusedState.Binding instead.
struct ViewA: View {
#FocusState private var focusedField: Bool
var body: some View {
ViewB(focusedField: $focusedField)
// some other views that read focusedField...
}
}
struct ViewB: View {
#FocusState.Binding var focusedField: Bool
var body: some View {
Button(action: {
focusedField = true
}) {
Text("Tap me")
}
}
}
This allows to do focusedField = true instead of focusedField.wrappedValue = true
Instead of directly setting focusedField, use the wrappedValue property.
struct ViewA: View {
#FocusState private var focusedField: Bool
var body: some View {
ViewB(focusedField: $focusedField)
// some other views that read focusedField...
}
}
struct ViewB: View {
var focusedField: FocusState<Bool>.Binding
var body: some View {
Button(action: {
focusedField.wrappedValue = true /// assign `wrappedValue`
}) {
Text("Click me!") /// Side note: you need a label for the button too, otherwise your code won't compile
}
// ...
}
}

I'm trying to implement a view stack in swiftui and my #State objects are being reset for reasons that are unclear to me

I'm new to swiftui and doing an experiment with pushing and popping views with a stack. When I pop a view off the stack, the #State variable of the prior view has been reset and I don't understand why.
This demo code was tested on macos.
import SwiftUI
typealias Push = (AnyView) -> ()
typealias Pop = () -> ()
struct PushKey: EnvironmentKey {
static let defaultValue: Push = { _ in }
}
struct PopKey: EnvironmentKey {
static let defaultValue: Pop = {() in }
}
extension EnvironmentValues {
var push: Push {
get { self[PushKey.self] }
set { self[PushKey.self] = newValue }
}
var pop: Pop {
get { self[PopKey.self] }
set { self[PopKey.self] = newValue }
}
}
struct ContentView: View {
#State private var stack: [AnyView]
var body: some View {
currentView()
.environment(\.push, push)
.environment(\.pop, pop)
.frame(width: 600.0, height: 400.0)
}
public init() {
_stack = State(initialValue: [AnyView(AAA())])
}
private func currentView() -> AnyView {
if stack.count == 0 {
return AnyView(Text("stack empty"))
}
return stack.last!
}
public func push(_ content: AnyView) {
stack.append(content)
}
public func pop() {
stack.removeLast()
}
}
struct AAA : View {
#State private var data = "default text"
#Environment(\.push) var push
var body: some View {
VStack {
TextEditor(text: $data)
Button("Push") {
self.push(AnyView(BBB()))
}
}
}
}
struct BBB : View {
#Environment(\.pop) var pop
var body: some View {
VStack {
Button("Pop") {
self.pop()
}
}
}
}
If I type some text into the editor then hit Push, then Pop out of that view, I was expecting the text editor to maintain my changes but it reverts to the default text.
What am I missing?
Edit:
I guess this is really a question of how are NavigationView and NavigationLink implemented. This simple code does the what I'm trying to do:
import SwiftUI
struct MyView: View {
#State var text = "default text"
var body: some View {
VStack {
TextEditor(text: $text)
NavigationLink(destination: MyView()) {
Text("Push")
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MyView()
}
}
}
run that on iOS so you get a nav stack. edit the text, then push. Edit again if you want, then go back and see state is retained.
My code is trying to do the same thing in principle.
I'll share this attempt maybe it will help you create your version of this.
This all started with an attempt to create something like NavigationView and NavigationLink but being able to back track to a random View in the stack
I have a protocol where an object returns a View. Usually it is an enum. The view() references a View with a switch that provides the correct child View. The ContentView/MainView works almost like a storyboard and just presents whatever is designated in the current or path variables.
//To make the View options generic
protocol ViewOptionsProtocol: Equatable {
associatedtype V = View
#ViewBuilder func view() -> V
}
This is the basic navigation router that keep track of the main view and the NavigationLink/path. Which looks similar to what you want to do.
//A generic Navigation Router
class ViewNavigationRouter<T: ViewOptionsProtocol>: ObservableObject{
//MARK: Variables
var home: T
//Keep track of your current screen
#Published private (set) var current: T
//Keep track of the path
#Published private (set) var path: [T] = []
//MARK: init
init(home: T, current: T){
self.home = home
self.current = current
}
//MARK: Functions
//Control how you get to the screen
///Navigates to the nextScreen adding to the path/cookie crumb
func push(nextScreen: T){
//This is a basic setup just going forward
path.append(nextScreen)
}
///Goes back one step in the path/cookie crumb
func pop(){
//Use the stored path to go back
_ = path.popLast()
}
///clears the path/cookie crumb and goes to the home screen
func goHome(){
path.removeAll()
current = home
}
///Clears the path/cookie crumb array
///sets the current View to the desired screen
func show(nextScreen: T){
goHome()
current = nextScreen
}
///Searches in the path/cookie crumb for the desired View in the latest position
///Removes the later Views
///sets the nextScreen
func dismissTo(nextScreen: T){
while !path.isEmpty && path.last != nextScreen{
pop()
}
if path.isEmpty{
show(nextScreen: nextScreen)
}
}
}
It isn't an #Environment but it can easily be an #EnvrionmentObject and all the views have to be in the enum so the views are not completely unknown but it is the only way I have been able to circumvent AnyView and keep views in an #ViewBuilder.
I use something like this as the main portion in the main view body
router.path.last?.view() ?? router.current.view()
Here is a simple implementation of your sample
import SwiftUI
class MyViewModel: ViewNavigationRouter<MyViewModel.ViewOptions> {
//In some view router concepts the data that is /preserved/shared among the views is preserved in the router itself.
#Published var preservedData: String = "preserved"
init(){
super.init(home: .aaa ,current: .aaa)
}
enum ViewOptions: String, ViewOptionsProtocol, CaseIterable{
case aaa
case bbb
#ViewBuilder func view() -> some View{
ViewOptionsView(option: self)
}
}
struct ViewOptionsView: View{
let option: ViewOptions
var body: some View{
switch option {
case .aaa:
AAA()
case .bbb:
BBB()
}
}
}
}
struct MyView: View {
#StateObject var router: MyViewModel = .init()
var body: some View {
NavigationView{
ScrollView {
router.path.last?.view() ?? router.current.view()
}
.toolbar(content: {
//Custom back button
ToolbarItem(placement: .navigationBarLeading, content: {
if !router.path.isEmpty {
Button(action: {
router.pop()
}, label: {
HStack(alignment: .center, spacing: 2, content: {
Image(systemName: "chevron.backward")
if router.path.count >= 2{
Text(router.path[router.path.count - 2].rawValue)
}else{
Text(router.current.rawValue)
}
})
})
}
})
})
.navigationTitle(router.path.last?.rawValue ?? router.current.rawValue)
}.environmentObject(router)
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}
struct AAA : View {
//This will reset because the view is cosmetic. the data needs to be preserved somehow via either persistence or in the router for sharing with other views.
#State private var data = "default text"
#EnvironmentObject var vm: MyViewModel
var body: some View {
VStack {
TextEditor(text: $data)
TextEditor(text: $vm.preservedData)
Button("Push") {
vm.push(nextScreen: .bbb)
}
}
}
}
struct BBB : View {
#EnvironmentObject var vm: MyViewModel
var body: some View {
VStack {
Button("Pop") {
vm.pop()
}
}
}
}

How to update an element of an array in an Observable Object

Sorry if my question is silly, I am a beginner to programming. I have a Navigation Link to a detail view from a List produced from my view model's array. In the detail view, I want to be able to mutate one of the tapped-on element's properties, but I can't seem to figure out how to do this. I don't think I explained that very well, so here is the code.
// model
struct Activity: Identifiable {
var id = UUID()
var name: String
var completeDescription: String
var completions: Int = 0
}
// view model
class ActivityViewModel: ObservableObject {
#Published var activities: [Activity] = []
}
// view
struct ActivityView: View {
#StateObject var viewModel = ActivityViewModel()
#State private var showingAddEditActivityView = false
var body: some View {
NavigationView {
VStack {
List {
ForEach(viewModel.activities, id: \.id) {
activity in
NavigationLink(destination: ActivityDetailView(activity: activity, viewModel: self.viewModel)) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
}
}
.navigationBarItems(trailing: Button("Add new"){
self.showingAddEditActivityView.toggle()
})
.navigationTitle(Text("Activity List"))
}
.sheet(isPresented: $showingAddEditActivityView) {
AddEditActivityView(copyViewModel: self.viewModel)
}
}
}
// detail view
struct ActivityDetailView: View {
#State var activity: Activity
#ObservedObject var viewModel: ActivityViewModel
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
activity.completions += 1
updateCompletionCount()
}
Text("\(activity.completeDescription)")
}
}
func updateCompletionCount() {
var tempActivity = viewModel.activities.first{ activity in activity.id == self.activity.id
}!
tempActivity.completions += 1
}
}
// Add new activity view (doesn't have anything to do with question)
struct AddEditActivityView: View {
#ObservedObject var copyViewModel : ActivityViewModel
#State private var activityName: String = ""
#State private var description: String = ""
var body: some View {
VStack {
TextField("Enter an activity", text: $activityName)
TextField("Enter an activity description", text: $description)
Button("Save"){
// I want this to be outside of my view
saveActivity()
}
}
}
func saveActivity() {
copyViewModel.activities.append(Activity(name: self.activityName, completeDescription: self.description))
print(copyViewModel.activities)
}
}
In the detail view, I am trying to update the completion count of that specific activity, and have it update my view model. The method I tried above probably doesn't make sense and obviously doesn't work. I've just left it to show what I tried.
Thanks for any assistance or insight.
The problem is here:
struct ActivityDetailView: View {
#State var activity: Activity
...
This needs to be a #Binding in order for changes to be reflected back in the parent view. There's also no need to pass in the entire viewModel in - once you have the #Binding, you can get rid of it.
// detail view
struct ActivityDetailView: View {
#Binding var activity: Activity /// here!
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
activity.completions += 1
}
Text("\(activity.completeDescription)")
}
}
}
But how do you get the Binding? If you're using iOS 15, you can directly loop over $viewModel.activities:
/// here!
ForEach($viewModel.activities, id: \.id) { $activity in
NavigationLink(destination: ActivityDetailView(activity: $activity)) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
And for iOS 14 or below, you'll need to loop over indices instead. But it works.
/// from https://stackoverflow.com/a/66944424/14351818
ForEach(Array(zip(viewModel.activities.indices, viewModel.activities)), id: \.1.id) { (index, activity) in
NavigationLink(destination: ActivityDetailView(activity: $viewModel.activities[index])) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
You are changing and increment the value of tempActivity so it will not affect the main array or data source.
You can add one update function inside the view model and call from view.
The view model is responsible for this updation.
class ActivityViewModel: ObservableObject {
#Published var activities: [Activity] = []
func updateCompletionCount(for id: UUID) {
if let index = activities.firstIndex(where: {$0.id == id}) {
self.activities[index].completions += 1
}
}
}
struct ActivityDetailView: View {
var activity: Activity
var viewModel: ActivityViewModel
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
updateCompletionCount()
}
Text("\(activity.completeDescription)")
}
}
func updateCompletionCount() {
self.viewModel.updateCompletionCount(for: activity.id)
}
}
Not needed #State or #ObservedObject for details view if don't have further action.

Why doesn't calling method of child view from parent view update the child view?

I'm trying to call a method of a child view which includes clearing some of its fields. When the method is called from a parent view, nothing happens. However, calling the method from the child view will clear its field. Here is some example code:
struct ChildView: View {
#State var response = ""
var body: some View {
TextField("", text: $response)
}
func clear() {
self.response = ""
}
}
struct ParentView: View {
private var child = ChildView()
var body: some View {
HStack {
self.child
Button(action: {
self.child.clear()
}) {
Text("Clear")
}
}
}
}
Can someone tell me why this happens and how to fix it/work around it? I can't directly access the child view's response because there are too many fields in my actual code and that would clutter it up too much.
SwiftUI view is not a reference-type, you cannot create it once, store in var, and then access it - SwiftUI view is a struct, value type, so storing it like did you work with copies it values, ie
struct ParentView: View {
private var child = ChildView() // << original value
var body: some View {
HStack {
self.child // created copy 1
Button(action: {
self.child.clear() // created copy 2
}) {
Here is a correct SwiftUI approach to construct parent/child view - everything about child view should be inside child view or injected in it via init arguments:
struct ChildView: View {
#State private var response = ""
var body: some View {
HStack {
TextField("", text: $response)
Button(action: {
self.clear()
}) {
Text("Clear")
}
}
}
func clear() {
self.response = ""
}
}
struct ParentView: View {
var body: some View {
ChildView()
}
}
Try using #Binding instead of #State. Bindings are a way of communicating state changes down to children.
Think of it this way: #State variables are used for View specific state. They are usually made private for this reason. If you need to communicate anything down, then #Binding is the way to do it.
struct ChildView: View {
#Binding var response: String
var body: some View {
TextField("", text: $response)
}
}
struct ParentView: View {
#State private var response = ""
var body: some View {
HStack {
ChildView(response: $response)
Button(action: {
self.clear()
}) {
Text("Clear")
}
}
}
private func clear() {
self.response = ""
}
}