SwiftUI: How to pass arguments to class from view? - database

I have a class that conforms to ObservableObject, which takes some arguments. When I then use #ObservedObject var someName = className() in my view to access all the functions and data in the class, I get an error saying:
Missing arguments for parameters 'pickedVideo', 'pickedImage', 'retrievedImages', 'retrievedVideos' in call
I am aware that I somehow have to pass the arguments from my view to the class.
But how do I pass variables from my view to my class?
Class:
class DBFunctions : ObservableObject {
init(pickedVideo: [String], pickedImage: [UIImage], retrievedImages: [UIImage], retrievedVideos: [AVPlayer]) {
self.pickedVideo = pickedVideo
self.pickedImage = pickedImage
self.retrievedImages = retrievedImages
self.retrievedVideos = retrievedVideos
}
var pickedVideo : [String]
var pickedImage : [UIImage]
var retrievedImages : [UIImage]
var retrievedVideos : [AVPlayer]
func somefunc() {
}
}
View:
struct ContentView: View {
#ObservedObject var helpFuncs = DBFunctions()
#State var showPicker: Bool = false
#State var pickedImage: [UIImage] = []
#State var retrievedImages = [UIImage]()
#State var player : AVPlayer?
#State var width : CGFloat = 0
#State var retrievedVideos = [AVPlayer]()
#State var pickedVideo: [String] = []
#State var isPaused = false
var body: some View {
VStack{
Button(action: {
helpFuncs.uploadImage()
}) {
Text("Upload Image")
}
}
}

What you are doing is something like an anti-pattern... The ObservableObject should be getting their "truth" from the Model, and not from the view. So, normally speaking, or those values initialize themselves, or you get their from the Model.
To be clear, you should create an object first, from model data, then pass its instance to the View. That's what MVVM is all about.
Fell free to increase your question if something that I answered was not clear enough.
Edit1: If you need to communicate something (like a choice) from the View to the ObservableObject, you should do this via "Intents"... that is normally a func in the ObservableObject class. Those Intents (in my way to architect this is failable), if the Intent goes well, ObservableObject change the Model and Publish their new results to the View, which redraws itself. If the "Intent" not go through, or I launch an Alert (if it's critical) or I simply ignore.

As mentioned, you are not using ObservableObject as it is meant to be used.
Look at this link, it gives you some good examples of how to use ObservableObject and manage data in your app
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
Try something like this example code:
class DBFunctions : ObservableObject {
#Published var pickedVideo : [String] // <-- here #Published
#Published var pickedImage : [UIImage]
#Published var retrievedImages : [UIImage]
#Published var retrievedVideos : [AVPlayer]
init() {
self.pickedVideo = []
self.pickedImage = []
self.retrievedImages = []
self.retrievedVideos = []
}
func uploadImage() { }
}
struct ContentView: View {
#StateObject var helpFuncs = DBFunctions() // <-- here
#State var showPicker: Bool = false
#State var player : AVPlayer?
#State var width : CGFloat = 0
#State var isPaused = false
// do not use these, use the ones in your helpFuncs model directly
// like: helpFuncs.pickedImage = ...
// #State var pickedImage: [UIImage] = []
// #State var retrievedImages = [UIImage]()
// #State var retrievedVideos = [AVPlayer]()
// #State var pickedVideo: [String] = []
var body: some View {
VStack{
Button(action: {
helpFuncs.uploadImage()
}) {
Text("Upload Image")
}
}
.onAppear {
// here do some initialisation of your helpFuncs, for example
}
}
}
With your original code (and question), You can of course pass some initial values, like this:
#ObservedObject var helpFuncs = DBFunctions(pickedVideo: [], pickedImage: [], retrievedImages: [], retrievedVideos: [])

Related

Assign value from array to a variable outside its constructor in Swift

I am trying to get variables in my view structure which will be updated from Firestore every time the user submits a rating so that a new currentRating can be determined. (currentRating is the average of all currentRatings, but I'm not storing individual ratings - I'm just trying to have my currentRating for each of the schools keep being updated after each new rating). I have created the class, FireStoreManager which creates an array which should be reading data to an array. The base of my array is from my identifiable structure RatingSubmission. I then have the first part of my final view structure where I've referenced FirestoreManager() because I'm trying to then be able to reference the different items in the array, Rating Submission, but I keep getting errors.
class FirestoreManager: ObservableObject {
#Published var ratingSubmission = [RatingSubmission]()
var currentRating: CGFloat = 0.0
func fetchRatingSubmission() {
let db = Firestore.firestore()
let docRef = db.collection("RatingInformation").document()
docRef.getDocument{(document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ??
"nil"
print("Document data: \(dataDescription)")
let data = document.data()
self.currentRating = data!["RatingInformation"]! as! CGFloat
print("after update: \(self.currentRating)")
}
}
}
init() {
fetchRatingSubmission()
}
}
struct RatingSubmission: Identifiable {
var id: String = UUID().uuidString
var location: String
var currentRating: Float
var usualRating: Float
var totalRatings: Float
}
struct NewRatingView: View {
var model = FirestoreManager()
var schools = ["North Avenue", "West Village", "Brittain"]
#State private var location = "West Village"
var currentRating: CGFloat {
didSet {
model.fetchRatingSubmission(currentRating)
}
}

Give #State private var a variable Array as initial value

I have this #State private var which is an Array of strings and changes values when buttons are tapped. But as an initial value, I want it to take an array of strings that is variable.
#ObservedObject var News = getNews()
#State private var newssitearray : Array<String>
init() {
_newssitearray = State(initialValue: News.data.map {$0.newsSite})
}
What I did above gives the error:
self was used before all stored properties are initialized.
Do everything in init, like
#ObservedObject var News: YourTypeOfNewsContainer // << declare only
#State private var newssitearray : Array<String>
init() {
let news = getNews() // << assuming this is synchronous
// use local var for both properties initialization
self.News = news
self._newssitearray = State(initialValue: news.data.map {$0.newsSite})
}
Important: if you getNews is asynchronous, then it cannot be used either in property initialisation or init itself - think to do this in .onAppear

How to create subarrays without duplication Swift

I have a Meal structure in my SwiftUI project
struct Meal: Identifiable, Codable, Equatable {
var id = UUID().uuidString
var name: String
var time: String
var type: String
var recommendation: Bool
}
I also have the ContentViewModel class
class ContentViewModel: ObservableObject {
init() {
let allItemsInit = Bundle.main.decode([Meal].self, from: "menu.json")
self.allItems = allItemsInit
self.recomendationItems = allItemsInit.filter {$0.recommendation == true}
}
#Published var allItems: [Meal] = []
#Published var recomendationItems: [Meal] = []
}
Is it a correct approach that I just assign certain elements to the new array of recomendationItems, thereby duplicating them.
recomendationItems - just example, there will be a large number of such subarrays.
You don't need "subarrays" -- your View will get updated whenever allItems changes, so you can use other computed properties to provide the subarrays rather than making them actual separate containers.
For example:
class ContentViewModel: ObservableObject {
init() {
self.allItems = Bundle.main.decode([Meal].self, from: "menu.json")
}
#Published var allItems: [Meal] = []
var recommendedItems: [Meal] {
return allItems.filter {$0.recommendation == true}
}
}

Using an array within a view model

I have an array within a view model. I cannot work out how to initialise it with dummy data so that I can access any element of the array later. The number of elements in the array changes each time the view loads so I cannot initialise it in the view model with "repeating".
I tried calling a function from .onAppear to append elements to the array, but .onAppear seems to run after my view has loaded so I get an error trying to access the array.
I'm obviously doing this wrong,
My view model is:
class DemoViewModel: ObservableObject {
#Published var array = [0]
}
My view is:
#ObservedObject private var demoViewModel = DemoViewModel()
// This changes every time the view is called from it's parent
var numberOfItemsInArray = 10
var body: some View {
List(0..<numberOfItemsInArray) { index in
Text("Hello, World! - \(index)")
// demoViewModel.array[index] = 1
// causes an error
}
}
You can pass the numberOfItemsInArray variable to DemoViewModel in init, so you can use Array(repeating:count:):
class DemoViewModel: ObservableObject {
#Published var array: [Int]
init(numberOfItemsInArray: Int) {
array = Array(repeating: 0, count: numberOfItemsInArray)
}
}
and initialise DemoViewModel like this:
struct ContentView: View {
#ObservedObject private var demoViewModel: DemoViewModel
init(numberOfItemsInArray: Int = 10) {
demoViewModel = DemoViewModel(numberOfItemsInArray: numberOfItemsInArray)
}
var body: some View {
List(0 ..< demoViewModel.array.count) { index in
Text("Hello, World! - \(index)")
}
}
}
Also, consider using #StateObject instead of #ObservedObject if you're using SwiftUI 2.

How do I make the Observable Object update the list?

So I know my items are being added to the 'vitallist'(through printing the list in the terminal), but I am not seeing them appear on list view. I think it has something to do with the 'ObservedObject' not being linked correctly. Any suggestions?
struct Vital: Identifiable {
let id = UUID()
var name: String
}
class VitalList:ObservableObject {
#Published var vitallist = [Vital]()
}
struct Row: View {
var vital: Vital
#State var completed:Bool = false
var body: some View {
HStack{
Image(systemName: completed ? "checkmark.circle.fill" : "circle").onTapGesture {
self.completed.toggle()
}
Text(vital.name)
}
}
}
struct Lists: View {
#ObservedObject var vitallist = VitalList()
var body: some View {
NavigationView{
List{
Section(header: Text("Vital")){
ForEach(vitallist.vitallist){ item in
Row(vital: item)
}
}
}
}
}
}
I also had same problem.
I am not sure why, but it works that creating a new element in the array, not changing the element itself. I confirmed directly updating works only in data, but not for binding UI.
In my code, element change in TobuyData class.
class Tobuy: Identifiable {
let id = UUID()
var thing: String
var isDone = false
init(_ thing: String, isDone: Bool = false) {
self.thing = thing
self.isDone = isDone
}
}
class TobuyData: ObservableObject {
#Published var tobuys: [Tobuy]
init() {
self.tobuys = [
Tobuy("banana"),
Tobuy("bread"),
Tobuy("pencil"),
]
}
func toggleDone(_ tobuy: Tobuy) {
if let j = self.tobuys.firstIndex(where: { $0.id == tobuy.id }) {
self.tobuys[j] = Tobuy(self.tobuys[j].thing, isDone: !self.tobuys[j].isDone)
// self.tobuys[j].isDone.toggle() // this works only in data, but not for binding UI
}
}
}
In View
struct ContentView: View {
#EnvironmentObject var tobuyData: TobuyData
var body: some View {
List {
ForEach(tobuyData.tobuys) { tobuy in
Text(tobuy.thing)
.strikethrough(tobuy.isDone)
.onTapGesture { self.tobuyData.toggleDone(tobuy) }
...
p.s.
Changing Tobuy Class to Struct made direct element updating work, the comment out part above. This referenced to Apple's official tutorial: "Handling User Input"
change
#ObservedObject var vitallist = VitalList()
to
#EnvironmentObject var vitallist = VitalList()
The code seems fine. I added a simple add method to VitalList
class VitalList:ObservableObject {
#Published var vitallist = [Vital]()
func addVital(){
self.vitallist.append(Vital(name: UUID().description))
}
}
And a Button to the body
var body: some View {
NavigationView{
VStack{
Button(action: {self.vitallist.addVital()}, label: {Text("add-vital")})
List{
Section(header: Text("Vital")){
ForEach(vitallist.vitallist){ item in
Row(vital: item)
}
}
}
}
}
}
The list updates as expected. check your code that adds your items to
#Published var vitallist = [Vital]()
Are you using the same instance of VitalList? A singleton might help.
https://developer.apple.com/documentation/swift/cocoa_design_patterns/managing_a_shared_resource_using_a_singleton

Resources