How to initialise one property with another during initialisation process - SwiftUI - ios

I'm building an app that fetches data from the The Movie Database API and presents a List of movies by genre.
In my view model I have a function that calls the api and makes the data available via an observable object.
import Foundation
class MovieListViewModel: ObservableObject {
#Published var movies = [Movie]()
private var fetchedMovies = [MovieList]()
var page = 1
func fetchMovies(genre: Int) {
WebService().getMoviesByGenre(genre: genre, page: page) { movie in
if let movie = movie {
self.fetchedMovies.append(movie)
for movie in movie.movies {
self.movies.append(movie)
}
}
}
page += 1
print(page)
}
}
Inside the view, I'm using onAppear to call the function and pass the genre id (Int) received from the previous view. This all works as expected.
import SwiftUI
struct MovielistView: View {
#ObservedObject private var movielistVM = MovieListViewModel()
var genre: GenreElement
var body: some View {
List {
...
}
}.onAppear {
self.movielistVM.fetchMovies(genre: self.genre.id)
}
.navigationBarTitle(genre.name)
}
}
The problem is because I'm using onAppear, the api is being hit/the view refreshed every time the user navigates back to the view.
Ideally, I'd like to call the api once during the initialising of the view model but because the genre property hasn't been initialised yet, it can't pass the parameter to the view model.
I'v tried this (below) but get the error 'self' used before all stored properties are initialized
import SwiftUI
struct MovielistView: View {
#ObservedObject private var movielistVM = MovieListViewModel()
var genre: GenreElement
init() {
movielistVM.fetchMovies(genre: genre.id) // Error here
}
var body: some View {
List {
...
}
.navigationBarTitle(genre.name)
}
}
Any help would be appreciated.

Add a genre parameter for the init and initialize the genre property of MovielistView before using it:
struct MovielistView: View {
var genre: GenreElement
//...
init(genre: GenreElement) {
self.genre = genre
movielistVM.fetchMovies(genre: genre.id)
}
//...
}

Related

How to pass by reference in Swift - number incrementing app using MVVM

I've just started learning swift and was going to build this number-incrementing sample app to understand MVVM. I don't understand why is my number on the view not updating upon clicking the button.
I tried to update the view everytime user clicks the button but the count stays at zero.
The View
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("\(viewModel.model.count)")
Button(action: {
self.viewModel.increment()
}) {
Text("Increment")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The ViewModel
import SwiftUI
class CounterViewModel: ObservableObject {
#ObservedObject var model = Model()
func increment() {
self.model.count += 1
}
}
The Model
import Foundation
class Model : ObservableObject{
#Published var count = 0
}
Following should work:
import SwiftUI
struct Model {
var count = 0
}
class CounterViewModel: ObservableObject {
#Published var model = Model()
func increment() {
self.model.count += 1
}
}
struct ContentView: View {
#ObservedObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("\(viewModel.model.count)")
Button(action: {
self.viewModel.increment()
}) {
Text("Increment")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Please note:
ObservableObject and #Published are designed to work together.
Only a value, that is in an observed object gets published and so the view updated.
A distinction between model and view model is not always necessary and the terms are somewhat misleading. You can just put the count var in the ViewModel. Like:
#Published var count = 1
It makes sense to have an own model struct (or class), when fx you fetch a record from a database or via a network request, than your Model would take the complete record.
Something like:
struct Adress {
let name: String
let street: String
let place: String
let email: String
}
Please also note the advantages (and disadvantages) of having immutable structs as a model. But this is another topic.
Hi it's a bad idea to use MVVM in SwiftUI because Swift is designed to take advantage of fast value types for view data like structs whereas MVVM uses slow objects for view data which leads to the kind of consistency bugs that SwiftUI's use of value types is designed to eliminate. It's a shame so many MVVM UIKit developers (and Harvard lecturers) have tried to push their MVVM garbage onto SwiftUI instead of learning it properly. Fortunately some of them are changing their ways.
When learning SwiftUI I believe it's best to learn value semantics first (where any value change to a struct is also a change to the struct itself), then the View struct (i.e. when body is called), then #Binding, then #State. e.g. have a play around with this:
// use a config struct like this for view data to group related vars
struct ContentViewConfig {
var count = 0 {
didSet {
// could do validation here, e.g. isValid = count < 10
}
}
// include other vars that are all related, e.g. you could have searchText and searchResults.
// use mutating func for logic that affects multiple vars
mutating func increment() {
count += 1
//othervar += 1
}
}
struct ContentView: View {
#State var config = ContentViewConfig() // normally structs are immutable, but #State makes it mutable like magic, so its like have a view model object right here, but better.
var body: some View {
VStack {
ContentView2(count: config.count)
ContentView3(config: $config)
}
}
}
// when designing a View first ask yourself what data does this need to do its job?
struct ContentView2: View {
let count: Int
// body is only called if count is different from the last time this was init.
var body: some View {
Text(count, format: .number)
}
}
struct ContentView3: View {
#Binding var config: ContentViewConfig
var body: some View {
Button(action: {
config.increment()
}) {
Text("Increment")
}
}
}
}
Then once you are comfortable with view data you can move on to model data which is when ObservableObject and singletons come into play, e.g.
struct Item: Identifiable {
let id = UUID()
var text = ""
}
class MyStore: ObservableObject {
#Published var items: [Item] = []
static var shared = MyStore()
static var preview = MyStore(preview: true)
init(preview: Bool = false) {
if preview {
items = [Item(text: "Test Item")]
}
}
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(MyStore.shared)
}
}
}
struct ContentView: View {
#EnvironmentObject store: MyStore
var body: some View {
List($store.items) { $item in
TextField("Item", $item.text)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(MyStore.preview)
}
}
Note we use singletons because it would be dangerous to use #StateObject for model data because its lifetime is tied to something on screen we could accidentally lose all our model data which should have lifetime tied to the app running. Best to think of #StateObject when you need a reference type in a #State, i.e. involving view data.
When it comes to async networking use the new .task modifier and you can avoid #StateObject.

How can I prevent a View model from being recreated on multiple views

DISCLAIMER: I'm a newbie in swift
I'm trying to set an MVVM app in such a way that multiple screens can access a single View Model but for some reason, everytime I navigate away from the home page, the ViewModel get re-created.
The ViewModel is set up this way:
extension ContentView {
//view model
class MyViewModel: ObservableObject {
let sdk: mySdk
#Published var allProducts = [ProductItem]()
#Published var itemsArray = [Item]() //This gets updated with content later on
...
init(sdk: mySdk) {
self.sdk = sdk
self.loadProds(forceReload: false)
...
func loadProds(forceReload: Bool){
sdk.getProducts(forceReload: forceReload) { products, error in
if let products = products {
self.allProducts = products
} else {
self.products = .error (error?.localizedDescription ?? "error")
print(error?.localizedDescription)
}
}
...
//itemsArray gets values appended to it as follows:
itemsArray.append(Item(productUid: key, quantity: Int32(value)))
}
}
}
}
The rest of the code is set up like:
struct ContentView: View { // Home Screen content
#ObservedObject var viewmodel: MyViewModel
var body: some View {
...
}
}
The SecondView that should get updated based on the state of the itemsArray is set up like so:
struct SecondView: View {
#ObservedObject var viewModel: ContentView.MyViewModel //I have also tried using #StateObject
init(sdk: mySdk) {
_viewModel = ObservedObject(wrappedValue: ContentView.MyViewModel(sdk: sdk))
}
var body: some View {
ScrollView {
LazyVStack {
Text("Items array count is \(viewModel.itemsArray.count)")
Text("All prods array count is \(viewModel.allProducts.count)")
if viewModel.itemsArray.isEmpty{
Text ("Items array is empty")
}
else {
Text ("Items array is not empty")
...
}
}
}
}
}
The Main View that holds the custom TabView and handles Navigation is set up like this:
struct MainView: View {
let sdk = mySdk(dbFactory: DbFactory())
#State private var selectedIndex = 0
let icons = [
"house",
"cart.fill",
"list.dash"
]
var body: some View{
VStack {
//Content
ZStack {
switch selectedIndex {
case 0:
NavigationView {
ContentView(viewmodel: .init(sdk: sdk))
.navigationBarTitle("Home")
}
case 1:
NavigationView {
SecondView(sdk: sdk)
.navigationBarTitle("Cart")
}
...
...
}
}
}
}
}
Everytime I navigate away from the ContentView screen, any updated content of the viewmodel gets reset. For example, on navigating the SecondView screen itemsArray.count shows 0 but allProducts Array shows the correct value as it was preloaded.
The entire content of ContentView gets recreated on navigating back as well.
I would love to have the data in the ViewModel persist on multiple views unless explicitly asked to refresh.
How can I go about doing that please? I can't seem to figure out where I'm doing something wrong.
Any help will be appreciated.
Your call to ContentView calls .init on your view model, so every time SwiftUI's rendering system needs to redraw itself, you'll get a new instance of the view model created. Similarly, the init() method on SecondView also calls the init method, in its ContentView.MyViewModel(sdk: sdk) form.
A better approach would be to create a single instance further up the hierarchy, and store it as a #StateObject so that SwiftUI knows to respond to changes to its published properties. Using #StateObject once also shows which view "owns" the object; that instance will stick around for as long as that view is in the hierarchy.
In your case, I'd create your view model in MainView – which probably means the view model definition shouldn't be namespaced within ContentView. Assuming you change the namespacing, you'd have something like
struct MainView: View {
#StateObject private var viewModel: ViewModel
init() {
let sdk = mySdk(dbFactory: DbFactory())
let viewModel = ViewModel(sdk: sdk)
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View{
VStack {
//Content
ZStack {
switch selectedIndex {
case 0:
NavigationView {
ContentView(viewModel: viewModel)
.navigationBarTitle("Home")
}
case 1:
NavigationView {
SecondView(viewModel: viewModel)
.navigationBarTitle("Cart")
}
...
...
}
}
}
}
}
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
// etc
}
}
struct SecondView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
// etc
}
}
One of the key things is that ObservedObject is designed to watch for changes on an object that a view itself doesn't own, so you should never be creating objects and assigning them directly to an #ObservedObject property. Instead they should receive references to objects owned by a view higher up, such as those that have been declared with a #StateObject.
First of all, let sdk = mySdk(dbFactory: DbFactory()) should be #StateObject var sdk = mySdk(dbFactory: DbFactory()).
To continue, SecondView & ContentView should have the same ViewModel, hence they should be like this:
ContentView(viewmodel: sdk)
SecondView(sdk: sdk)
Also use #StateObject instead of #ObservedObject

how do I get a binding of computed property in SwiftUI?

I have a search bar in a list that filters an array of students and stores them in a computed property called searchResults of type [Student], I want to pass a binding of each filtered student in the list to another view that accepts a binding.
If I tried to pass a binding of searchResults in the list, it says: Cannot find '$searchResults' in scope, I think because searchResults is not a state, and I cannot make it a state since it is a computed property.
struct StudentsSection: View {
#Binding var course: Course
#State var searchText = ""
var searchResults: [Student] { course.students.filter { $0.fullName.contains(searchText) } }
var body: some View {
List($searchResults) { $student in // error: Cannot find '$searchResults' in scope
StudentRow(student: $student, course: $course)
}
.searchable(text: $searchText)
}
}
I want to achieve the same result as the code below:
struct StudentsSection: View {
#Binding var course: Course
var body: some View {
List($course.students) { $student in // no errors
StudentRow(student: $student, course: $course)
}
}
}

SwiftUI Published variable doesn't trigger UI update

I have an app with a navigation view list that doesn't update when new elements get added later on in the app. the initial screen is fine and everything get triggered at this moment no matter how I code them, but beyond that, it stays that way. At some point I had my "init" method as an .onappear, and dynamic elements wouldn't come in, but the static ones would get added multiple times when I would go back and forth in the app, this is no longer part of my code now though.
here what my content view look like, I tried to move the navigation view part to the class that has the published var, in case it help, visually it dint change anything, dint help either.
struct ContentView: View {
#ObservedObject var diceViewList = DiceViewList()
var body: some View {
VStack{
Text("Diceimator").padding()
diceViewList.body
Text("Luck Selector")
}
}
}
and the DiceViewList class
import Foundation
import SwiftUI
class DiceViewList: ObservableObject {
#Published var list = [DiceView]()
init() {
list.append(DiceView(objectID: "Generic", name: "Generic dice set"))
list.append(DiceView(objectID: "Add", name: "Add a new dice set"))
// This insert is a simulation of what add() does with the same exact values. it does get added properly
let pos = 1
let id = 1
self.list.insert(DiceView(objectID: String(id), dice: Dice(name: String("Dice"), face: 1, amount: 1), name: "Dice"), at: pos)
}
var body: some View {
NavigationView {
List {
ForEach(self.list) { dView in
NavigationLink(destination: DiceView(objectID: dView.id, dice: dView.dice, name: dView.name)) {
HStack { Text(dView.name) }
}
}
}
}
}
func add(dice: Dice) {
let pos = list.count - 1
let id = list.count - 1
self.list.insert(DiceView(objectID: String(id), dice: dice, name: dice.name), at: pos)
}
}
I'm working on the latest Xcode 11 in case it matter
EDIT: Edited code according to suggestions, problem didnt change at all
struct ContentView: View {
#ObservedObject var vm: DiceViewList = DiceViewList()
var body: some View {
NavigationView {
List(vm.customlist) { dice in
NavigationLink(destination: DiceView(dice: dice)) {
Text(dice.name)
}
}
}
}
}
and the DiceViewList class
class DiceViewList: ObservableObject {
#Published var customlist: [Dice] = []
func add(dice: Dice) {
self.customlist.append(dice)
}
init() {
customlist.append(Dice(objectID: "0", name: "Generic", face: 1, amount: 1))
customlist.append(Dice(objectID: "999", name: "AddDice", face: 1, amount: 1))
}
}
SwiftUI is a paradigm shift from how you would build a UIKit app.
The idea is to separate the data that "drives" the view - which is the View model, from the View presentation concerns.
In other words, if you had a ParentView that shows a list of ChildView(foo:Foo), then the ParentView's view model should be an array of Foo objects - not ChildViews:
struct Foo { var v: String }
class ParentVM: ObservableObject {
#Published let foos = [Foo("one"), Foo("two"), Foo("three")]
}
struct ParentView: View {
#ObservedObject var vm = ParentVM()
var body: some View {
List(vm.foos, id: \.self) { foo in
ChildView(foo: foo)
}
}
}
struct ChildView: View {
var foo: Foo
var body = Text("\(foo.v)")
}
So, in your case, separate the logic of adding Dice objects from DiceViewList (I'm taking liberties with your specific logic for brevity):
class DiceListVM: ObservableObject {
#Published var dice: [Dice] = []
func add(dice: Dice) {
dice.append(dice)
}
}
struct DiceViewList: View {
#ObservedObject var vm: DiceListVM = DiceListVM()
var body: some View {
NavigationView {
List(vm.dice) { dice in
NavigationLink(destination: DiceView(for: dice)) {
Text(dice.name)
}
}
}
}
If you need more data than what's available in Dice, just create a DiceVM with all the other properties, like .name and .dice and objectId.
But the takeaway is: Don't store and vend out views. - only deal with the data.
While testing stuff I realized the problem. I Assumed declaring #ObservedObject var vm: DiceViewList = DiceViewList() in every other class and struct needing it would make them find the same object, but it doesn't! I tried to pass the observed object as an argument to my subview that contain the "add" button, and it now work as intended.

How to use BindableObjects (EnviromentObject)?

Im using the new SwiftUI. I have a UserUpdate class which is a Bindable Object and I want to modify these variables and automatically Update the UI.
I update these Values successfully but the views in my UI struct isn't updating when I change the variable in the UserUpdate class.
It only changes when I modify the #EnviromentObject variable in the UI struct itself.
That's my Bindable Object Class:
final class UserUpdate: BindableObject {
let didChange = PassthroughSubject<Any, Never>()
var allUsers: [User] = [] {
didSet {
print(allUsers)
didChange.send(allUsers)
}
}
var firstName: String = "" {
didSet {
didChange.send(firstName)
}
}
var lastName: String = "" {
didSet {
didChange.send(lastName)
}
}
}
That's my User class:
struct User: Identifiable {
let id: Int
let firstName, lastName: String
}
Here's how I configure my UI:
struct ContentView : View {
#EnvironmentObject var bindableUser: UserUpdate
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text("All Users:").bold().padding(.leading, 10)
List {
ForEach(bindableUser.allUsers) { user in
Text("\(user.firstName) \(user.lastName)")
}
}
}
}
}
}
Here I modify the variables in UserUpdate:
class TestBind {
static let instance = TestBind()
let userUpdate = UserUpdate()
func bind() {
let user = User(id: userUpdate.allUsers.count, firstName: "Heyy", lastName: "worked")
userUpdate.allUsers.append(user)
}
}
I found out that I had to call the method from my UI to get it working so its on the same stream.
For example by this:
struct ContentView : View {
#EnvironmentObject var networkManager: NetworkManager
var body: some View {
VStack {
Button(action: {
self.networkManager.getAllCourses()
}, label: {
Text("Get All Courses")
})
List(networkManager.courses.identified(by: \.name)) {
Text($0.name)
}
}
}
}
If I'm not wrong, you should inject the UserUpdate instance in your ContentView, probably in the SceneDelegate, using ContentView().environmentObject(UserUpdate()).
In that case, you have 2 different instance of the UserUpdate class, the first one created in the SceneDelegate, and the second created in the TestBind class.
The problem is that you have one instance that is bond to view (and will trigger view reload on update), and the one that you actually modify (in TestBind class) which is totally unrelated to the view.
You should find a way to use the same instance in the view and in the TestBind class (for example by using ContentView().environmentObject(TestBind.instance.userUpdate)

Resources