Swift can't update view from parent - ios

So I have a SwiftUI view that I instantiate in a parents ViewControllers viewDidLoad() method.
It looks something like this:
class ChildViewController: UIViewController {
private var historyView: HistoryView?
private var models: [HistoryModel]?
...
override func viewDidLoad() {
super.viewDidLoad()
historyView = HistoryView(models: models)
let historyViewHost = UIHostingController(rootView: historyView)
view.addSubview(historyViewHost.view)
historyViewHost.didMove(toParent: self)
historyViewHost.view.frame = view.frame
}
...
}
At some point i need to update the models in the historyView and I do so like this:
func updateHistory() {
let updatedModels = requests.map({ HistoryModel.fromRequest(request: $0) })
historyView!.historyCellModels = updatedModels
}
The problem is the models in the view do not actually get updated. And I don't mean that the view doesn't display the new models, but it actually doesn't even get the new list of models.
My HistoryView also updates everytime I hit a button, when I hit that button I have a breakpoint set in the construction of the view, and from the debugger I can see that the models do not get updated. I also have a breakpoint in my updateHistory method, where i can see that the models in the parent ARE updated, its just not getting passed down to the child.
I had the idea that maybe 2 instances of the view were being created and I was just updating the wrong one. So I viewed the memory of the historyView when I had a breakpoint inside it, and I wrote down where it was in memory. Then at the breakpoint I have in the parent view, i went to look at the memory of historyView and it pointed to 0x00! But whats even stranger is that historyView is not nil! I even did a force cast of it to a non-optional and the program had no issues.
So I figured the debugger must be lying to me and just not giving the right info. So I went to some old trusty print statements. When I added print statements like this:
func updateHistory() {
let updatedModels = requests.map({ HistoryCellModel.fromRequest(request: $0) })
print(updatedModels.count)
historyView!.historyCellModels = updatedModels
print(historyView?.historyCellModels.count)
}
And then I create a new model, and call the update function it will output:
11
Optional(10)
How is that possible!
My HistoryView looks something like this:
struct HistoryView: View {
#State var historyCellModels: [HistoryModel]
var body: some View {
List(historyCellModels) { ... }
}
}
I clearly set the two variables to be equal, the view is non-null, there is only one instance of it... I'm really not sure what the next step to hunting this bug is. Is there something obvious I could be missing?

#State is designed to be used only inside SwiftUI view itself (and recommended always to be declared as private).
So here is a way...
Use instead
struct HistoryView: View {
#ObservedObject var historyCellModels: HistoryViewModel
init(models: HistoryViewModel) {
historyCellModels = models
}
...
where
import Combine
class HistoryViewModel: ObservableObject {
#Published var historyCellModels: [HistoryModel]
}
and now it can be
class ChildViewController: UIViewController {
private var models: HistoryViewModel = HistoryViewModel()
...
and
func updateHistory() {
let updatedModels = requests.map({ HistoryModel.fromRequest(request: $0) })
models.historyCellModels = updatedModels
}
and all should work.

Related

Can't show view in Swift UI with layers of views and onAppear

There is a strange case where if you show a view through another view the contents (list of 3 items) of the second view won't show when values are set using onAppear. I'm guessing SwiftUI gets confused since the second views onAppear is called prior to the first views onAppear, but I still think this is weird since both of the views data are only used in their own views. Also, there is no problem if I don't use view models and instead have the data being set using state directly in the view, but then there is yet another problem that the view model declaration must be commented out otherwise I get "Thread 1: EXC_BAD_ACCESS (code=1, address=0x400000008)". Furthermore, if I check for nil in the first view on the data that is set there before showing the second one, then the second view will be shown the first time you navigate (to the first containing the second), but no other times. I also tried removing content view and starting directly at FirstView and then the screen is just black. I want to understand why these problems happen, setting data through init works but then the init will be called before it's navigated to since that's how NavigationView works, which in turn I guess I could work around by using a deferred view, but there are cases where I would like to do stuff in the background with .task as well and it has the same problem as .onAppear. In any case I would like to avoid work arounds and understand the problem. See comments for better explanation:
struct ContentView: View {
var body: some View {
NavigationView {
// If I directly go to SecondView instead the list shows
NavigationLink(destination: FirstView()) {
Text("Go to first view")
}
}
}
}
class FirstViewViewModel: ObservableObject {
#Published var listOfItems: [Int]?
func updateList() {
listOfItems = []
}
}
struct FirstView: View {
#ObservedObject var viewModel = FirstViewViewModel()
// If I have the state in the view instead of the view model there is no problem.
// Also need to comment out the view model when using the state otherwise I get Thread 1: EXC_BAD_ACCESS runtime exception
//#State private var listOfItems: [Int]?
var body: some View {
// Showing SecondView without check for nil and it will never show
SecondView()
// If I check for nil then the second view will show the first time its navigated to, but no other times.
/*Group {
if viewModel.listOfItems != nil {
SecondView()
} else {
Text("Loading").hidden() // Needed in order for onAppear to trigger, EmptyView don't work
}
}*/
// If I comment out onAppear there is no problem
.onAppear {
print("onAppear called for first view after onAppear in second view")
viewModel.updateList()
// listOfItems = []
}
}
}
class SecondViewViewModel: ObservableObject {
#Published var listOfItems = [String]()
func updateList() {
listOfItems = ["first", "second", "third"]
}
}
struct SecondView: View {
#ObservedObject var viewModel = SecondViewViewModel()
// If I set the items through init instead of onAppear the list shows every time
init() {
// viewModel.updateList()
}
var body: some View {
Group {
List {
ForEach(viewModel.listOfItems, id: \.self) { itemValue in
VStack(alignment: .leading, spacing: 8) {
Text(itemValue)
}
}
}
}
.navigationTitle("Second View")
.onAppear {
viewModel.updateList()
// The items are printed even though the view don't show
print("items: \(viewModel.listOfItems)")
}
}
}
We don't use view model objects in SwiftUI. For data transient to a View we use #State and #Binding to make the View data struct behave like an object.
And FYI initing an object using #ObservedObject is an error causing a memory leak, it will be discarded every time the View struct is init. When we are creating a Combine loader/fetcher object that we want to have a lifetime tied to the view we init the object using #StateObject.
Also you must not do id: \.self with ForEach for an array of value types cause it'll crash when the data changes. You have to make a struct for your data that conforms to Identifiable to be used with ForEach. Or if you really do want a static ForEach you can do ForEach(0..<5) {

Changes to a struct not being published in SwiftUI

I'm loading data from an API, and expecting my app to show the data once it's loaded.
In my View Model file, here's the code:
It calls a WeatherService to get the data, and populates the weather property. Weather is a struct in this case.
class WeatherViewModel: ObservableObject {
let webService = WeatherService.shared
#Published var weather:Weather?
init() {
}
func getWeather() {
webService.getWeather { weather in
if let weather = weather {
self.weather = weather
}
}
}
}
In my SwiftUI view, here's the code:
I instantiate an instance of the View Model as an ObservedObject
In the inAppear, I call the method in the view model to get the data
The first time the screen launches (using a tab bar), I see "Loading weather..." and it never goes away
If I navigate to a different tab and back, I see the weather. I can't tell if this is data from the old API call, or from the new one.
struct WeatherView: View {
#ObservedObject var weatherViewModel = WeatherViewModel()
#State var areDetailsHidden = true
var body: some View {
VStack(alignment: .leading, spacing: 0) {
if(weatherViewModel.weather == nil) {
Text("Loading weather...")
} else {
Text("Display the weather here")
}
}
.onAppear{
self.weatherViewModel.getWeather()
}
}
}
The weird thing is, if I remove the getWeather() from the onAppear and add it to the init() of the View Model, it works (although for some reason getWeather() gets called twice...). However, I want the weather info to be refreshed every time the screen is loaded.
This is caused by the:
#ObservedObject var weatherViewModel = WeatherViewModel()
being owned by the WeatherView itself.
So what happens is the weather view model changes which forces a re-render of the view which creates a new copy of the weather view model, which changes forces a re-render...
So you end up with an endless loop.
To fix it you need to move the weather view model out of the view itself so either use an #Binding and pass it in or an #EnvironmentObject and access it that way.

iOS - Sharing viewModel between views

I have a view whose ViewModel configures the view. The user can update the ViewModel and this object is later passed onto another view which will reflect the state of the preview view. Here is an example.
struct ViewSettings {
var btn1Selected: Bool
var btn2Selected: Bool
var btn3Selected: Bool
init() {
btn1Selected = true
btn2Selected = true
btn3Selected = true
}
}
class ViewOne: UIView {
var settings: ViewSettings
init(settings: ViewSettings) {
self.settings = settings
}
func configureView() {
btn1.isSelected = settings.btn1Selected
btn2.isSelected = settings.btn2Selected
btn3.isSelected = settings.btn3Selected
}
#objc func tapBtn1(_ sender: UIButton) {
btn1.isSelected = btn1.isSelected.toggle()
settings.btn1Selected.toggle()
}
#objc func tapBtn2(_ sender: UIButton) {
btn2.isSelected = btn2.isSelected.toggle()
settings.btn2Selected.toggle()
}
#objc func tapBtn3(_ sender: UIButton) {
btn3.isSelected = btn3.isSelected.toggle()
settings.btn3Selected.toggle()
}
}
This setting is later used inside another view. If btn1 is selected in ViewOne and when ViewTwo uses that setting, btn1 in ViewTwo is selected too.
Question:
I'm doing a direct mutation on the settings to achieve this. Is there a better design pattern that would let me arrive at the same solution?
your viewModel is struct that means its a value type, even when you think you are passing the same viewModel to other views, in reality you send a different copy of viewModel not the same instance.
So when you mutate value in view1 and pass the copy of it to view2, if view2 changes the value again, your viewModel in view1 will not be updated, its not passed by reference its passed by value.
so If your question was that I am mutating value directly will it cause side effects no because they are different copies, but if you want the changes to be reflected in all the views that holds this viewModel then it won't.
Finally answering your question
Question: I'm doing a direct mutation on the settings to achieve this.
Is there a better design pattern that would let me arrive at the same
solution?
At very first, sharing viewModel across view itself is arguable. Should this be done or not, as such the whole idea is opinion based. Some might say its fine some might say its not!
In general, I have seen people sharing viewModel across views but I personally refrain from doing so, but that doesn't mean that either of the approach is the only right way. Its best left to developers judgement.
Few clarifications:
struct ViewSettings {
var btn1Selected: Bool
var btn2Selected: Bool
var btn3Selected: Bool
init() {
btn1Selected = true
btn2Selected = true
btn3Selected = true
}
}
This looks more like DataModel and less of ViewModel isn't it? Its just a data container, no business logic, no data presentation/modification mechanisms nothing in it, its a plain data model. You are sharing DataModel across view and you wanna mutate and pass it on to next view I think its fine to go ahead with it.

How to re-initialise classes in a SwiftUI NavigationView

I have two views - a MasterView and DetailView. When opening the DetailView, I initialise a new class that tracks data about the view (in the real implementation, the detail view involves a game).
However, when I press the back button from the DetailView to return to the MasterView, and then press the button to return to the DetailView, my class is unchanged. However, I would like to re-initialise a new copy of this class (in my case to re-start the game) whenever I move from the MasterView to the DetailView.
I have condensed the problem to this code:
import SwiftUI
import Combine
class Model: ObservableObject {
#Published var mytext: String = "mytext"
}
struct MasterView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailView(model: Model())) {
Text("press me")
}
}
}
}
struct DetailView: View {
#ObservedObject var model: Model = Model()
var body: some View {
TextField("Enter here", text: $model.mytext)
}
}
struct MasterView_Previews: PreviewProvider {
static var previews: some View {
MasterView()
}
}
I would like to create a new instance of Model every time I click the NavigationLink to the detail view, but it seems like it always refers back to the same original instance - I can see this by typing a change into the text field of the DetailView, which persists if I go back and forward again.
Is there any way of doing this?
Based on your comments - and correct me where wrong - here's how I'd set things up.
Your needs are:
A "base" class. Call it MasterView, "settings", "view state", whatever. This is where everything starts.
A "current game".... well, it could be a struct, a class, even properties in an ObservableObject.
I think that's about it. Hierarchically, your model could be:
ViewState
...Player
......Properties, including ID and history
...Current Game
...... Properties, including difficulty
Please note, I've changed some names and am being very vague on properties. The point is, you can encapsulate all of this in an ObservableObject, create an `EnvironmentObject of it, and have all your SwiftUI views "react" to changes in it.
Leaving out views, hopefully you can see where this "model" can contain just about all the Swift code you wish to do everything - now all you need is to tie in your views.
(1) Create your ObservableObject. It needs to (a) be a class object and (b) conform to the ObservableObject protocol. Here's a simple one:
class ViewState : ObservableObject {
var objectWillChange = PassthroughSubject<Void, Never>()
#Published var playerID = "" {
willSet {
objectWillChange.send()
}
}
}
You can create more structs/classes and instantiate them as needed in your model.
(2) Instantiate ViewState once min your environment. In SceneDelegate:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView()
.environmentObject(ViewState())
)
self.window = window
window.makeKeyAndVisible()
}
}
Note that there's a single line added here and that ViewState is instantiated a single time.
(3) Finally, in any SwiftUI view that needs to know your view state, bind it by adding one line of code:
#EnvironmentObject var model: ViewState
If you want, you can do virtually anything in your model (ViewState) from instantiating a new game, flag something to result in a modal popup, add a player to an array, whatever.
The main thing I hope I'm explaining is there's no need to instantiate a second view state - rather instantiate a second game instance inside your single view state.
Again, if I'm way off from your needs, let me know - I'll gladly delete my answer. Good luck!

How to create an instance of an object in SwiftUI without duplication?

This is the next part of that question.
I've got the follow code.
The initial view of the app:
struct InitialView : View {
var body: some View {
Group {
PresentationButton(destination: ObjectsListView()) {
Text("Show ListView")
}
PresentationButton(destination: AnotherObjectsListView()) {
Text("Show AnotherListView")
}
}
}
}
The list view of the objects:
struct ObjectsListView : View {
#Environment(\.myObjectsStore.objects) var myObjectsStores: Places
var body: some View {
Group {
Section {
ForEach(myObjectsStore.objects) { object in
NavigationLink(destination: ObjectDetailView(object: object)) {
ObjectCell(object: object)
}
}
}
Section {
// this little boi
PresentationButton(destination: ObjectDetailView(objectToEdit: MyObject(store: myObjectsStore))) {
Text("Add New Object")
}
}
}
}
}
The detail view:
struct ObjectsDetailView : View {
#Binding var myObject: MyObject
var body: some View {
Text("\(myObject.title)")
}
}
So the problem is quite complex.
The ObjectsListView creates instance of the MyObject(store: myObjectsStore) on itself initialization while computing body.
The MyObject object is setting its store property on itself initialization, since it should know is it belongs to myObjectsStore or to anotherMyObjectsStore.
The myObjectsStore are #BindableObjects since their changes are managing by SwiftUI itself.
So this behavior ends up that I've unexpected MyObject() initializations since the Views are computing itself. Like:
First MyObject creates on the ObjectsListView initialization.
Second MyObject creates on its PresentationButton pressing (the expected one).
Third (any sometimes comes even fourth) MyObject creates on dismissing ObjectsDetailView.
So I can't figure what pattern should I use this case to create only one object?
The only thing that I'd come to is to make the follow code:
struct ObjectsListView : View {
#Environment(\.myObjectsStore.objects) var myObjectsStores: Places
#State var buttonPressed = false
var body: some View {
Group {
if buttonPressed {
ObjectDetailView(objectToEdit: MyObject(store: myObjectsStore))
} else {
Section {
ForEach(myObjectsStore.objects) { object in
NavigationLink(destination: ObjectDetailView(object: object)) {
ObjectCell(object: object)
}
}
}
Section {
Button(action: {
self.buttonPressed.toggle()
}) {
Text("Add New Object")
}
}
}
}
}
}
Which simply redraw ObjectsListView to detail view conditionally. But it's completely out of iOS guidelines. So how to create the Only One object for another view in SwiftUI?
UPD:
Here's the project that represents the bug with Object duplication.
I'm still have no idea why the objects are duplicating in this case. But at least I know the reason yet. And the reason is this line:
#Environment(\.myObjectsStore.objects) var myObjectsStores: Places
I've tried to share my model with this wrapper to make it available in every single view (including Modal one) without passing them as an arg to the new view initializer, which are unavailable by the other ways, like #EnvironmentObject wrapper. And for some reason #Environment(\.keyPath) wrapper makes duplications.
So I'd simply replace all variables from Environment(\.) to ObjectBinding and now everything works well.
I've found the solution to this.
Here's the project repo that represents the bug with Object duplication and the version that fix this. I'm still have no idea how objects have been duplicate in that case. But I figured out why. It happens because this line:
#Environment(\.myObjectsStore.objects) var myObjectsStores: MyObjectsStore
I've used #Environment(\.key) to connect my model to each view in the navigation stack including Modal one, which are unavailable by the other ways provided in SwiftUI, e.g.: #State, #ObjectBinding, #EnvironmentObject. And for some reason #Environment(\.key) wrapper produce these duplications.
So I'd simply replace all variables from #Environment(\.) to #ObjectBinding and now almost everything works well.
Note: The code is in the rep is still creates one additional object by each workflow walkthrough. So it creates two objects totally instead of one. This behavior could be fixed by way provided in this answer.

Resources