iOS SwiftUI: Data Flow Flow in Subviews #ObservedObject - ios

I'm having a problem of updates in my application.
I can't understand very well what is going on with the data flow between the subviews.
This is my current structure
ViewModel: ObsebsrvableObject
MainView with ObservedObject (viewModel)
ChildView with a list from MainView observed object (just the list is passed as a normal array - not bindable)
NephewView with the list passed to the childView, still as a normal array
What is happening:
Every time I modify the list, the MainView updates triggering a new rebuild of ChildView, but the NephewView does not update
What I would like to have:
I would like to update the Main, the Child and the NephewView views every time the observedObject get an update
Problem:
I can't understand why if the Child View rebuild, the nephew doesn't.
Example Code
class ViewModel: ObservableObject {
let userData = UserData.shared
var canceller: AnyCancellable?
#Published var items: [Items]
init() {
items = []
canceller = userData.objectWillChange
.throttle(for: 5, scheduler: RunLoop.main, latest: true)
.sink(receiveCompletion: { data in
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.items = data.items
}
},
receiveValue: { _ in
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.items = data.items
}
})
}
}
struct MainView: View {
#ObservedObject var model = ViewModel()
var body: some View {
ChildView(items: model.items)
}
}
struct ChildView: View {
let items: [Items]
var body: some View {
NephewView(items: items)
}
}
struct NephewView: View {
let items: [Items]
var body: some View {
List...
}
}
The sink works properly, it's just the UI part that does not get updates.
Maybe the way I'm updating the publisher is wrong?

Your MainView
ChildView(items: $model.items)
Your ChildView
struct ChildView: View {
#Binding let items: [Items]
var body: some View {
List {
ForEach(self.items) { item in
NephewView(items: item)
}
}
}
}
Your NephewView
struct NephewView: View {
#Binding let items: item
var body: some View {
// Your view logic goes here with single item
}
}

Related

SwiftUI: Issue with data binding for passing back the updated value to caller

I have 2 views where the
first view passes list of items and selected item in that to second view and
second view returns the updated selected item if user changes.
I am getting error 'Type of expression is ambiguous without more context' when i am sending the model property 'idx'.
//I cant make any changes to this model so cant confirm it with ObservableObject or put a bool property like 'isSelected'
class Model {
var idx: String?
....
}
class FirstViewModel: ObservableObject {
var list: [Model]
#Published var selectedModel: Model?
func getSecondViewModel() -> SecondViewModel {
let vm2 = SecondViewModel( //error >> Type of expression is ambiguous without more context
list: list,
selected: selectedModel?.idx // >> issue might be here but showing at above line
)
return vm2
}
}
struct FirstView: View {
#ObservableObject firstViewModel: FirstViewModel
var body: some View {
..
.sheet(isPresented: $showView2) {
NavigationView {
SecondView(viewModel: firstViewModel.getSecondViewModel())
}
}
..
}
}
class SecondViewModel: ObservableObject {
var list: [Model]
#Published var selected: String?
init(list: [Model], selected: Published<String?>) {
self.list = list
_selected = selected
}
func setSelected(idx: String) {
self.selected = idx
}
}
struct SecondView: View {
#ObservableObject secondViewModel: SecondViewModel
#Environment(\.presentationMode) var presentationMode
var body: some View {
...
.onTapGesture {
secondViewModel.setSelected(idx: selectedIndex)
presentationMode.wrappedValue.dismiss()
}
...
}
}
In case if I am sending 'Model' object directly to the SecondViewModel its working fine. I need to make changes the type and couple of other areas and instantiate the SecondViewModel as below
let vm2 = SecondViewModel(
list: list,
selected: _selectedModel
)
Since I need only idx I don't want to send entire model.
Also the reason for error might be but not sure the Model is #Published and the idx is not.
Any help is appreciated
Here is some code, in keeping with your original code that allows you to
use the secondViewModel as a nested model.
It passes firstViewModel to the SecondView, because
secondViewModel is contained in the firstViewModel. It also uses
firstViewModel.objectWillChange.send() to tell the model to update.
My comment is still valid, you need to create only one SecondViewModel that you use. Currently, your func getSecondViewModel() returns a new SecondViewModel every time you use it.
Re-structure your code so that you do not need to have nested ObservableObjects.
struct Model {
var idx = ""
}
struct ContentView: View {
#StateObject var firstMdl = FirstViewModel()
var body: some View {
VStack (spacing: 55){
FirstView(firstViewModel: firstMdl)
Text(firstMdl.secondViewModel.selected ?? "secondViewModel NO selected data")
}
}
}
class FirstViewModel: ObservableObject {
var list: [Model]
#Published var selectedModel: Model?
let secondViewModel: SecondViewModel // <-- here only one source of truth
// -- here
init() {
self.list = []
self.selectedModel = nil
self.secondViewModel = SecondViewModel(list: list, selected: nil)
}
// -- here
func getSecondViewModel() -> SecondViewModel {
secondViewModel.selected = selectedModel?.idx
return secondViewModel
}
}
class SecondViewModel: ObservableObject {
var list: [Model]
#Published var selected: String?
init(list: [Model], selected: String?) { // <-- here
self.list = list
self.selected = selected // <-- here
}
func setSelected(idx: String) {
selected = idx
}
}
struct FirstView: View {
#ObservedObject var firstViewModel: FirstViewModel // <-- here
#State var showView2 = false
var body: some View {
Button("click me", action: {showView2 = true}).padding(20).border(.green)
.sheet(isPresented: $showView2) {
SecondView(firstViewModel: firstViewModel)
}
}
}
struct SecondView: View {
#ObservedObject var firstViewModel: FirstViewModel // <-- here
#Environment(\.dismiss) var dismiss
#State var selectedIndex = "---> have some data now"
var body: some View {
Text("SecondView tap here to dismiss").padding(20).border(.red)
.onTapGesture {
firstViewModel.objectWillChange.send() // <-- here
firstViewModel.getSecondViewModel().setSelected(idx: selectedIndex) // <-- here
// alternatively
// firstViewModel.secondViewModel.selected = selectedIndex
dismiss()
}
}
}

Type 'Void' cannot conform to 'View' | Swift

I'm trying to create a List with data from my firebase reali-time database but i'm getting this error on the List line:
The error:
Type 'Void' cannot conform to 'View'
My code:
struct ActiveGoalsView: View {
#State var goals = ["finish this project"]
#State var ref = Database.database().reference()
var body: some View {
NavigationView {
List {
ref.child("users").child(Auth.auth().currentUser?.uid ?? "noid").child("goals").observeSingleEvent(of: .value) { snapshot in
for snap in snapshot.children {
Text(snap.child("title").value)
}
}
}.navigationBarHidden(true)
}
}
}
struct ActiveGoalsView_Previews: PreviewProvider {
static var previews: some View {
ActiveGoalsView()
}
}
You can't use imperative code like observeSingleEvent in the middle of your view hierarchy that doesn't return a View. As a commenter suggested, you'd be better off moving your asynchronous code outside of the body (I'd recommend to an ObservableObject). Here's one solution (see inline comments):
class ActiveGoalsViewModel : ObservableObject {
#Published var children : [String] = []
private var ref = Database.database().reference()
func getChildren() {
ref.child("users").child(Auth.auth().currentUser?.uid ?? "noid").child("goals").observeSingleEvent(of: .value) { snapshot in
self.children = snapshot.children.map { snap in
snap.child("title").value //you may need to use ?? "" if this returns an optional
}
}
}
}
struct ActiveGoalsView: View {
#State var goals = ["finish this project"]
#StateObject private var viewModel = ActiveGoalsViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.children, id: \.self) { child in //id: \.self isn't a great solution here -- you'd be better off returning an `Identifiable` object, but I have no knowledge of your data structure
Text(child)
}
}.navigationBarHidden(true)
}.onAppear {
viewModel.getChildren()
}
}
}

SwiftUI NavigationView pops by itself

it's very strange issue as I cannot reproduce in isolated code, but I hope that someone may think about the reason. I have a view, let's say ContentView that has its ContentViewModel that is ObservedObject, and then there's another View ContentView2. And we have NavigationView in ContentView that wraps navigation link to ContentView2. And it's a bit weird, but when we do some changes that affect ContentViewModel, then NavigationView pops ContentView2 so that we end up in initial ContentView, but we didn't do anything like dismissing ContentView2 or tapping back button. I have a similar code to the one used in my project, but please note that in this code everything works fine:
func qrealm() -> Realm {
return try! Realm(configuration: .init( inMemoryIdentifier: "yw"))
}
class SomeRObj: Object {
#objc dynamic var name: String = ""
convenience init(name: String) {
self.init()
self.name = name
}
static var instance: SomeRObj {
return qrealm().objects(SomeRObj.self).first!
}
}
struct SomeRObjWrapped: Hashable {
var obj: SomeRObj
var prop: Int
}
class ContentViewModel: ObservableObject {
#Published var someRObj: [SomeRObjWrapped] = []
var any: Any?
init() {
let token = qrealm().objects(SomeRObj.self).observe { changes in
switch changes {
case let .initial(data), let .update(data, deletions: _, insertions: _, modifications: _):
let someObjs = data.map { SomeRObjWrapped(obj: $0, prop: Int.random(in: 1..<50)) }
self.someRObj = Array(someObjs)
default: break
}
}
self.any = token
}
}
struct ContentView: View {
#ObservedObject var model: ContentViewModel = ContentViewModel()
var body: some View {
NavigationView {
VStack {
ForEach(model.someRObj, id: \.self) { obj in
Heh(obj: obj.obj, pr: obj.prop)
}
NavigationLink(destination: ContentView2()) {
Text("Link")
}
}
}
}
}
struct Heh: View {
var obj: SomeRObj
var pr: Int
var body: some View {
Text("\(obj.name) \(pr)")
}
}
struct ContentView2: View {
var body: some View {
Button(action: { try! qrealm().write {
let elem = qrealm().objects(SomeRObj.self).randomElement()
elem?.name = "jotaro kujo"
}
}, label: { Text("Toggle") })
}
}
You can replace \.self with \.id:
ForEach(model.someRObj, id: \.id) { obj in
Heh(obj: obj.obj, pr: obj.prop)
}
Then every object will be identified by id and the ForEach loop will only refresh when the id is changed.
Thanks to pawello2222, I found the real reason behind it. I had a NavigationLink inside my List, so that each time there was a change NavigationLink is redrawn and it's state refreshed. I hope that it will be helpfull to someone, and the solution as pawello2222 wrote before is to identify view by id parameter.

ForEach loop inside a button action in SwiftUI?

I understand that a ForEach loop is typically used to display a view. When I put a ForEach loop inside the action button it pretty much tells me Button action cannot conform to the view protocol. So how can I use a loop to make the button carry out multiple actions?
struct SomeView: View {
var newExercises = [NewExercise]()
var finalExercises = [Exercise]()
var body: some View {
Button(action: {
ForEach(newExercises) { newExercise in
//.getExercise() returns an Exercise object
finalExercises.append(newExercise.getExercise())
}
}) {
Text("Done")
}
}
}
I want the button to add an Exercise (by calling .getExercise()) to the finalExercises array for each newExercise in the newExercises array.
How can I go about doing this?
The new SwiftUI ForEach statement returns a View for each Element of an Array. For your code, you simply need to run a Void, Array<Exercise>.append(newElement: Exercise) not get multiple View's, so you can use a for loop, map, or Array.forEach(body: (_) throws -> Void).
If the order in which the newExercises are appended matters, the most elegant solution will be mapping each NewExercise of finalExercises to a Exercise, and appending the resulting Array<Exercise>, with Array<Exercise>.append(contentsOf: Sequence).
struct SomeView: View {
#State var newExercises = [NewExercise]()
#State var finalExercises = [Exercise]()
var body: some View {
Button(action: {
self.finalExercises.append(contentsOf:
self.newExercises.map { newExercise -> Exercise in
newExercise.getExercise()
}
)
}) {
Text("Done")
}
}
}
If the order in which the newExercises are appended does not matter, you can call Array<Exercise>.append(newElement: Exercise) from newExcercises.forEach, which is different than a SwiftUI ForEach statement:
struct SomeView: View {
#State var newExercises = [NewExercise]()
#State var finalExercises = [Exercise]()
var body: some View {
Button(action: {
self.newExercises.forEach { newExercise in
self.finalExercises.append(newExercise.getExercise())
}
}) {
Text("Done")
}
}
}
The way to complete what you want with a for loop would be simple, but less elegant:
struct SomeView: View {
#State var newExercises = [NewExercise]()
#State var finalExercises = [Exercise]()
var body: some View {
Button(action: {
for newExercise in self.newExercises {
self.finalExercises.append(newExercise.getExercise())
}
}) {
Text("Done")
}
}
}

SwiftUI View - viewDidLoad()?

Trying to load an image after the view loads, the model object driving the view (see MovieDetail below) has a urlString. Because a SwiftUI View element has no life cycle methods (and there's not a view controller driving things) what is the best way to handle this?
The main issue I'm having is no matter which way I try to solve the problem (Binding an object or using a State variable), my View doesn't have the urlString until after it loads...
// movie object
struct Movie: Decodable, Identifiable {
let id: String
let title: String
let year: String
let type: String
var posterUrl: String
private enum CodingKeys: String, CodingKey {
case id = "imdbID"
case title = "Title"
case year = "Year"
case type = "Type"
case posterUrl = "Poster"
}
}
// root content list view that navigates to the detail view
struct ContentView : View {
var movies: [Movie]
var body: some View {
NavigationView {
List(movies) { movie in
NavigationButton(destination: MovieDetail(movie: movie)) {
MovieRow(movie: movie)
}
}
.navigationBarTitle(Text("Star Wars Movies"))
}
}
}
// detail view that needs to make the asynchronous call
struct MovieDetail : View {
let movie: Movie
#State var imageObject = BoundImageObject()
var body: some View {
HStack(alignment: .top) {
VStack {
Image(uiImage: imageObject.image)
.scaledToFit()
Text(movie.title)
.font(.subheadline)
}
}
}
}
We can achieve this using view modifier.
Create ViewModifier:
struct ViewDidLoadModifier: ViewModifier {
#State private var didLoad = false
private let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content.onAppear {
if didLoad == false {
didLoad = true
action?()
}
}
}
}
Create View extension:
extension View {
func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
Use like this:
struct SomeView: View {
var body: some View {
VStack {
Text("HELLO!")
}.onLoad {
print("onLoad")
}
}
}
I hope this is helpful. I found a blogpost that talks about doing stuff onAppear for a navigation view.
Idea would be that you bake your service into a BindableObject and subscribe to those updates in your view.
struct SearchView : View {
#State private var query: String = "Swift"
#EnvironmentObject var repoStore: ReposStore
var body: some View {
NavigationView {
List {
TextField($query, placeholder: Text("type something..."), onCommit: fetch)
ForEach(repoStore.repos) { repo in
RepoRow(repo: repo)
}
}.navigationBarTitle(Text("Search"))
}.onAppear(perform: fetch)
}
private func fetch() {
repoStore.fetch(matching: query)
}
}
import SwiftUI
import Combine
class ReposStore: BindableObject {
var repos: [Repo] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject<ReposStore, Never>()
let service: GithubService
init(service: GithubService) {
self.service = service
}
func fetch(matching query: String) {
service.search(matching: query) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let repos): self?.repos = repos
case .failure: self?.repos = []
}
}
}
}
}
Credit to: Majid Jabrayilov
Fully updated for Xcode 11.2, Swift 5.0
I think the viewDidLoad() just equal to implement in the body closure.
SwiftUI gives us equivalents to UIKit’s viewDidAppear() and viewDidDisappear() in the form of onAppear() and onDisappear(). You can attach any code to these two events that you want, and SwiftUI will execute them when they occur.
As an example, this creates two views that use onAppear() and onDisappear() to print messages, with a navigation link to move between the two:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Hello World")
}
}
}.onAppear {
print("ContentView appeared!")
}.onDisappear {
print("ContentView disappeared!")
}
}
}
ref: https://www.hackingwithswift.com/quick-start/swiftui/how-to-respond-to-view-lifecycle-events-onappear-and-ondisappear
I'm using init() instead. I think onApear() is not an alternative to viewDidLoad(). Because onApear is called when your view is being appeared. Since your view can be appear multiple times it conflicts with viewDidLoad which is called once.
Imagine having a TabView. By swiping through pages onApear() is being called multiple times. However viewDidLoad() is called just once.

Resources