Using onAppear on ForEach - ios

I created View which contains List and under List is Section:
struct SwiftUIView: View {
var body: some View {
List {
Section {
EmptyView()
} header: {
Text("Some header text")
}
NewView()
}
}
}
NewView and SomeViewModel code:
class SomeViewModel: ObservableObject {
#Published var data: [String] = []
func onAppear(){
data = ["One", "Two", "Three"]
}
}
struct NewView: View {
#State private var viewModel = SomeViewModel()
var body: some View {
ForEach(viewModel.data, id: \.self){
Text($0)
}
.onAppear(perform: viewModel.onAppear)
}
}
Problem here is that onAppear modifier is not called on ForEach. When I added Text and use onAppear modifier on it, then it is working, but I don't need one more View. I tried to use EmptyView also, but again, onAppear is not called. Is there any way to call modifiers on ForEach?

Try this approach to call onAppear()
struct ContentView: View {
var body: some View {
NewView()
}
}
class SomeViewModel: ObservableObject {
#Published var data: [String] = []
func onAppear() {
data = ["One", "Two", "Three"]
print("----> in SomeViewModel onAppear")
}
}
struct NewView: View {
#StateObject var viewModel = SomeViewModel() // <-- here
var body: some View {
ForEach(viewModel.data, id: \.self){
Text($0)
}
.onAppear{
viewModel.onAppear() // <-- here
}
}
}

I think you should use init instead of onAppear to init the model
class SomeViewModel: ObservableObject {
#Published var data: [String] = []
init() {
data = ["One", "Two", "Three"]
}
}
struct NewView: View {
#State private var viewModel = SomeViewModel()
var body: some View {
ForEach(viewModel.data, id: \.self){
Text($0)
}
}
}

Related

How to pass data between ViewModels in SwiftUI

I have this use case where I have a parent view and a child view. Both of the views have their own corresponding ViewModels.
ParentView:
struct ParentView: View {
#StateObject var parentViewModel = ParentViewModel()
var body: some View {
NavigationView {
List {
TextField("Add Name", text: $parentViewModel.newListName)
NavigationLink(destination: ChildView()) {
Label("Select Products", systemImage: K.ListIcons.productsNr)
}
}
}
}
ParentViewModel:
class ParentViewModel: ObservableObject {
#Published var newListName: String = ""
func saveList() {
// some logic to save to CoreData, method would be called via a button
// how do I reference "someString" from ChildViewModel in this ViewModel?
}
}
ChildView:
struct ChildView: View {
#StateObject var childViewModel = ChildViewModel()
var body: some View {
NavigationView {
List{
Text("Some element")
.onTapGesture {
childViewModel.alterData()
}
}
}
}
}
ChildViewModel:
class ChildViewModel: ObservableObject {
#Published var someString: String = ""
func alterData() {
someString = "Toast"
}
}
My question now is, how do I pass the new value of "someString" from ChildViewModel into the ParentViewModel, in order to do some further stuff with it?
I've tried to create a #StateObject var childViewModel = ChildViewModel() reference in the ParentViewModel, but that does obviously not work, as this will create a new instance of the ChildViewModel and therefore not know of the changes made to "someString"
Solution:
As proposed by Josh, I went with the approach to use a single ViewModel instead of two. To achieve this, the ParentView needs a .environmentObject(T) modifier.
ParentView:
struct ParentView: View {
#StateObject var parentViewModel = ParentViewModel()
var body: some View {
NavigationView {
List {
TextField("Add Name", text: $parentViewModel.newListName)
NavigationLink(destination: ChildView()) {
Label("Select Products", systemImage: K.ListIcons.productsNr)
}
}
}.environmentObject(parentViewModel)
}
The ChildView then references that environment Object via #EnvironmentObject without an initializer:
struct ChildView: View {
#EnvironmentObject var parentViewModel: ParentViewModel
var body: some View {
NavigationView {
List{
Text("Some element")
.onTapGesture {
parentViewModel.alterData()
}
}
}
}
}
Most likely you would use a binding for this situation:
struct ChildView: View {
#Binding var name: String
var body: some View {
NavigationView {
List{
Text("Some element")
.onTapGesture {
name = "Altered!"
}
}
}
}
}
And in the parent:
struct ParentView: View {
#StateObject var parentViewModel = ParentViewModel()
var body: some View {
NavigationView {
List {
TextField("Add Name", text: $parentViewModel.newListName)
NavigationLink(destination: ChildView(name: $parentViewModel.newListName)) {
Label("Select Products", systemImage: K.ListIcons.productsNr)
}
}
}
}
Also, I think you can remove the NavigationView view from ChildView. Having it ParentView is enough.

SwiftUI MVVM Binding List Item

I am trying to create a list view and a detailed screen like this:
struct MyListView: View {
#StateObject var viewModel: MyListViewModel = MyListViewModel()
LazyVStack {
// https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/
ForEach(viewModel.items.identifiableIndicies) { index in
MyListItemView($viewModel.items[index])
}
}
}
class MyListViewModel: ObservableObject {
#Published var items: [Item] = []
...
}
struct MyListItemView: View {
#Binding var item: Item
var body: some View {
NavigationLink(destination: MyListItemDetailView(item: $item), label: {
...
})
}
}
struct MyListItemDetailView: View {
#Binding var item: Item
#StateObject var viewModel: MyListViewItemDetailModel
init(item: Binding<Item>) {
viewModel = MyListViewItemDetailModel(item: item)
}
var body: some View {
...
}
}
class MyListViewItemDetailModel: ObservableObject {
var item: Binding<Item>
...
}
I am not sure what's wrong with it, but I found that item variables are not synced with each other, even between MyListItemDetailView and MyListItemDetailViewModel.
Is there anyone who can provide the best practice and let me know what's wrong in my implmentation?
I think you should think about a minor restructure of your code, and use only 1
#StateObject/ObservableObject. Here is a cut down version of your code using
only one StateObject source of truth:
Note: AFAIK Binding is meant to be used in View struct not "ordinary" classes.
PS: what is identifiableIndicies?
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct Item: Identifiable {
let id = UUID().uuidString
var name: String = ""
}
struct MyListView: View {
#StateObject var viewModel: MyListViewModel = MyListViewModel()
var body: some View {
LazyVStack {
ForEach(viewModel.items.indices) { index in
MyListItemView(item: $viewModel.items[index])
}
}
}
}
class MyListViewModel: ObservableObject {
#Published var items: [Item] = [Item(name: "one"), Item(name: "two")]
}
struct MyListItemView: View {
#Binding var item: Item
var body: some View {
NavigationLink(destination: MyListItemDetailView(item: $item)){
Text(item.name)
}
}
}
class MyAPIModel {
func fetchItemData(completion: #escaping (Item) -> Void) {
// do your fetching here
completion(Item(name: "new data from api"))
}
}
struct MyListItemDetailView: View {
#Binding var item: Item
let myApiModel = MyAPIModel()
var body: some View {
VStack {
Button(action: fetchNewData) {
Text("Fetch new data")
}
TextField("edit item", text: $item.name).border(.red).padding()
}
}
func fetchNewData() {
myApiModel.fetchItemData() { itemData in
item = itemData
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MyListView()
}.navigationViewStyle(.stack)
}
}
EDIT1:
to setup an API to call some functions, you could use something like this:
class MyAPI {
func fetchItemData(completion: #escaping (Item) -> Void) {
// do your stuff
}
}
and use it to obtain whatever data you require from the server.
EDIT2: added some code to demonstrate the use of an API.

SwiftUI is it possible to pass range of binding in ForEach?

I'd like to pass a range of an array in a model inside ForEach.
I recreated an example:
import SwiftUI
class TheModel: ObservableObject {
#Published var list: [Int] = [1,2,3,4,5,6,7,8,9,10]
}
struct MainView: View {
#StateObject var model = TheModel()
var body: some View {
VStack {
ForEach (0...1, id:\.self) { item in
SubView(subList: $model.list[0..<5]) <-- error if I put a range
}
}
}
}
struct SubView: View {
#Binding var subList: [Int]
var body: some View {
HStack {
ForEach (subList, id:\.self) { item in
Text("\(item)")
}
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}
The work around
I found is to pass all the list and perform the range inside the subView. But I'd like don't do this because the array is very big:
struct MainView: View {
#StateObject var model = TheModel()
var body: some View {
VStack {
ForEach (0...1, id:\.self) { i in
SubView(subList: $model.list, number: i, dimension: 5)
}
}
}
}
struct SubView: View {
#Binding var subList: [Int]
var number: Int
var dimension: Int
var body: some View {
HStack {
ForEach (subList[number*dimension..<dimension*(number+1)].indices, id:\.self) { idx in
Button(action: {
subList[idx] += 1
print(subList)
}, label: {
Text("num: \(subList[idx])")
})
}
}
}
}
I would pass the model to the subview since it is a class and will be passed by reference and then pass the range as a separate parameter.
Here is my new implementation of SubView
struct SubView: View {
var model: TheModel
var range: Range<Int>
var body: some View {
HStack {
ForEach (model.list[range].indices, id:\.self) { idx in
HStack {
Button(action: {
model.list[idx] += 1
print(model.list)
}, label: {
Text("num: \(model.list[idx])")
})
}
}
}
}
}
Note that I added indices to the ForEach header to make sure we access the array using an index and not with a value from the array.
The calling view would then look like
var body: some View {
VStack {
SubView(model: model, range: (0..<5))
Text("\(model.list.map(String.init).joined(separator: "-"))")
}
The extra Text is just there for testing purposes

Why my SwiftUI List is not getting updated?

I use a SwiftUI List and pass a String to a different view via a Binding.
But the list get not updated when I went back.
Here is my example:
struct ContentView: View {
#State private var list = ["a", "b", "c"]
#State private var item: String?
#State private var showSheet = false
var body: some View {
List {
ForEach(list.indices) { i in
Button(action: {
item = list[i]
showSheet.toggle()
}) {
Text(list[i])
}
}
}
.sheet(isPresented: $showSheet, content: {
DetailView(input: $item)
})
}
}
And the detail page:
struct DetailView: View {
#Binding var input: String?
var body: some View {
Text(input ?? "")
.onDisappear{
print("changed to changed")
input = "changed"
}
}
}
What I want to achieve is, that on every Item I click, I see the detail page. After that the element should change to "changed". But this does not happen. Why?
You update item but not list, so don't see any result. Here is corrected variant - store selected index and pass binding to list by index.
Tested with Xcode 12.1 / iOS 14.1
struct ContentView: View {
#State private var list = ["a", "b", "c"]
#State private var item: Int?
var body: some View {
List {
ForEach(list.indices) { i in
Button(action: {
item = i
}) {
Text(list[i])
}
}
}
.sheet(item: $item, content: { i in
DetailView(input: $list[i])
})
}
}
extension Int: Identifiable {
public var id: Self { self }
}
struct DetailView: View {
#Binding var input: String
var body: some View {
Text(input)
.onDisappear{
print("changed to changed")
input = "changed"
}
}
}
I recommend you use .sheet(item:content:) instead of .sheet(isPresented:content:)
struct ContentView: View {
#State private var items = ["a", "b", "c"]
#State private var selectedIndex: Int?
var body: some View {
List {
ForEach(items.indices) { index in
Button(action: {
selectedIndex = index
}) {
Text(items[index])
}
}
}
.sheet(item: $selectedIndex) { index in
DetailView(item: $items[index])
}
}
}
struct DetailView: View {
#Binding var item: String
var body: some View {
Text(item)
.onDisappear {
print("changed to changed")
item = "changed"
}
}
}
This will, however, require the selectedIndex to conform to Identifiable.
You can either create an Int extension:
extension Int: Identifiable {
public var id: Int { self }
}
or create a custom struct for your data (and conform to Identifiable).

objectWillChange do not update view

I have ViewModel which looks like this:
class ItemsListViewModel : ObservableObject{
var response : ItemsListResponse? = nil
var itemsList : [ListItem] = []
var isLoading = true
let objectWillChange = PassthroughSubject<Void, Never>()
func getItems() {
self.isLoading = true
ApiManager.shared.getItems()
.sink(receiveCompletion: {completion in
}, receiveValue: {
self.response = data
self.isLoading = false
self.objectWillChange.send()
})
}
}
When I receive data from network request I use self.objectWillChange.send() to notify view, but view do not react to this.
My views :
ItemsView
struct ItemsView: View {
var body: some View {
VStack{
Text("Some Title")
ItemsListView()
}
}
}
ItemsListView
struct ItemsListView: View {
#ObservedObject var myViewModel = ItemsListViewModel()
var body: some View {
VStack{
Text("\(self.myViewModel.response?.total")
}.onAppear{
self.myViewModel.getItems()
}
}
}
But the interesting thing, that if I use ItemsListView not inside
ItemsView everything works perfectly. How can i solve this problem?
try this ( i simplified just your model to test in playground)
you can copy directly the code in playground and check
struct Model {
var items : [String]
}
class ItemsListViewModel : ObservableObject {
#Published var items : [String] = ["Test 1", "Test2"]
}
let myViewModel = ItemsListViewModel()
struct ItemsView: View {
var body: some View {
VStack{
Text("Some Title")
ItemsListView().environmentObject(myViewModel)
}
}
}
struct ItemsListView: View {
#EnvironmentObject private var model : ItemsListViewModel
var body: some View {
VStack{
Text("\(model.items.count)")
}
}
}
struct ContentView: View {
var body: some View {
ItemsView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(myViewModel)
}
}
Using #Published for properties of ObservableObject does fix your issue. See simplified below demo:
import SwiftUI
import Combine
class ItemsListViewModel : ObservableObject{
#Published var response = ""
var isLoading = true
func getData() {
self.isLoading = true
DispatchQueue.main.async {
Just("test")
.sink(receiveCompletion: {completion in
}, receiveValue: { data in
self.response = data
self.isLoading = false
})
}
}
}
struct ItemsListView: View {
#ObservedObject var myViewModel = ItemsListViewModel()
var body: some View {
VStack{
Text("\(self.myViewModel.response)")
}.onAppear{
self.myViewModel.getData()
}
}
}
struct ItemsView: View {
var body: some View {
VStack{
Text("Some Title")
ItemsListView()
}
}
}
struct TestPublished: View {
var body: some View {
ItemsView()
}
}
struct TestPublished_Previews: PreviewProvider {
static var previews: some View {
TestPublished()
}
}

Resources