#State and #Published properties in child views, SwiftUI - ios

In MyController, I have the following property which is updated in other methods that are called:
#Published public var data = [Glucose]()
I also have a function, which limits this Published property by a given limit:
public func latestReadings(limit: Int = 5) -> [Glucose] {
// return latests results
}
In a SwiftUI View, I consume this data by the following, which works fine and updates when MyController's data changes:
#EnvironmentObject var data: MyController
var body: Some View {
ForEach(self.data.latestReadings(limit: 11), id: \.self) {
/// Display Text etc.
}
}
But, I want to call the following here, which converts the Glucose readings into a DataPoint array which the Chart consumes:
Chart(
data: self.data.latestReadings(limit: 37),
formattedBy: { (readings) -> [DataPoint] in
var result = [DataPoint]()
var i = 0
for reading in readings {
result.append(DataPoint(x: Double(i), y: reading.mmol))
i += 1
}
return result
}
)
...Which refers to another SwiftUI View defined as:
struct Chart: View {
// Properties
#State var data: [DataPoint] // I asusme this should be #State
var opt: ChartOptions
// Formatters
private var fmt: Formatting = Formatting.shared
// Init
public init(data: [Glucose], formattedBy:ChartDataFormatter) {
_data = State(wrappedValue: formattedBy(data)) // Again I assume this is probably wrong..
}
...draw views etc.
}
This all works on the first time the Chart is drawn, but the data property on the Chart view doesn't re-draw as the MyController data property changes. I assume I'm doing something wrong with state and observing changes here?

If I understood your workflow correctly you don't need state wrapper in Chart, because it prevents value update... so try without it, like
struct Chart: View {
// Properties
var data: [DataPoint]
// ...
// Init
public init(data: [Glucose], formattedBy:ChartDataFormatter) {
self.data = formattedBy(data)
}
// ...

#State breaks the connection with your Controller. Per the documentation #State should always be private.
Pass the data using #EnvironmentObject and manipulate it within the view or in the Controller.

Related

How to use constant variable in SwiftUI Core Data #FetchRequest?

How can I use #FetchRequest in SwiftUI with a fetch request based on a variable being passed in from a parent view, while also ensuring that the view updates based on Core Data changes?
I have this issue where using #FetchRequest in SwiftUI while passing in a variable to the fetch request doesn't work. It results in the following error:
Cannot use instance member 'name' within property initializer; property initializers run before 'self' is available
struct DetailView: View {
#FetchRequest(fetchRequest: Report.fetchRequest(name: name), animation: .default)
private var data: FetchedResults<Report>
let name: String
var body: some View {
Text("\(data.first?.summary.text ?? "")")
.navigationTitle(name)
.onAppear {
ReportAPI.shared.fetch(for: name) // Make network request to get Report data then save to Core Data
}
}
}
extension Report {
#nonobjc public class func fetchRequest(name: String) -> NSFetchRequest<Report> {
let fetchRequest = NSFetchRequest<Report>(entityName: "Report")
fetchRequest.fetchLimit = 1
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Report.createdAt, ascending: true)]
fetchRequest.predicate = NSPredicate(format: "details.name == %#", name)
return fetchRequest
}
}
This view is pushed using a NavigationLink from a previous view. Like: NavigationLink(destination: DetailView(name: item.name)).
I've looked at this answer and I don't think that it will work for my use case since normally the #FetchRequest property wrapper automatically listens for data changes and updates automatically. Since I'm doing an async call to make a network request and update Core Data, I need the data variable to update dynamically once that is complete.
I've also looked at this answer but I think it has some of the same problems as mentioned above (although I'm not sure about this). I'm also getting an error using that code on the self.data = FetchRequest(fetchRequest: Report.fetchRequest(name: name), animation: .default) line. So I'm not sure how to test my theory here.
Cannot assign to property: 'data' is a get-only property
struct DetailView: View {
#FetchRequest
private var data: FetchedResults<Report>
let name: String
var body: some View {
Text("\(data.first?.summary.text ?? "")")
.navigationTitle(name)
.onAppear {
ReportAPI.shared.fetch(for: name) // Make network request to get Report data then save to Core Data
}
}
init(name: String) {
self.name = name
self.data = FetchRequest(fetchRequest: Report.fetchRequest(name: name), animation: .default)
}
}
Therefore, I don't think that question is the same as mine, since I need to be sure that the view updates dynamically based on Core Data changes (ie. when the fetch method saves the new data to Core Data.

displays values of array in ForEach Swift UI

I am using SwiftUI and I want to simply display array values in a Text form.
Here is my code:
ForEach(0..<returnCont().count) {
Text("\(returnCont()[$0]),")
}
I also tried this:
ForEach(returnCont().indices) {
Text("\(return()[index]),")
}
Where returnCont() is a function returning an array.
The array displays elements that are initialised, but when the array is empty and then appended through user inputs, it only displays values in the terminal, but not in the Text form on the View.
No error is displayed either, just empty text.
Try something like below-:
import SwiftUI
class Model:ObservableObject{
var dataArray = ["1","2","3"]
func returnCont() -> Int{
return dataArray.count
}
}
struct myView:View{
#ObservedObject var model = Model()
var body: some View{
ForEach(0..<model.returnCont()) { index in
Text("\(model.dataArray[index]),")
}
}
}
I don’t know how your model class looks like, so this is just a demo to give you correct idea.

How to pass an EnvironmentObject to an ObservedObject within that EnvironmentObject?

I have an EnvironmentObject called GameManager() that is basically the root of my app:
class GameManager: ObservableObject {
#ObservedObject var timeManager = TimeManager()
Because there is a lot of code in it, I want to delegate certain tasks into seperate classes/files.
For example, I want to create a timer running every second. This could easily run inside GameManager(), but I want to delegate this to a seperate class called TimeManager():
class TimeManager: ObservableObject {
#EnvironmentObject var gameManager: GameManager
var activeGameTimer: Timer?
#Published var activeGameSeconds: Int = 0
func start(){
self.activeGameTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ _ in
self.activeGameSeconds += 1
}
}
}
Only problem is, TimeManager needs to have a reference to GameManager() - in order to react to certain events in my game.
However, there doesn't seem to be a way to pass GameManager() into TimeManager().
Is there a smooth way to achieve this? If not, is there another way I should arrange what I'm trying to do?
If the solution is hacky, I would rather not do it.
First of all #ObservedObject and #EnvironmentObject are property wrappers designed for SwiftUI View not for other else, so using them in classes might be harmful or at least useless - they do not functioning as you'd expected.
Now the solution for your scenario is dependency injection (both types are reference-types so instances are injected as a references):
class GameManager: ObservableObject {
var timeManager: TimeManager!
init() {
timeManager = TimeManager(gameManager: self)
}
// ... other code
}
class TimeManager: ObservableObject {
var gameManager: GameManager
var activeGameTimer: Timer?
#Published var activeGameSeconds: Int = 0
init(gameManager: GameManager) {
self.gameManager = gameManager
}
// ... other code
}

Using Kotlin mulitplatform classes in SwiftUI

I am building a small project using the Kotlin Mulitplatform Mobile (KMM) framework and am using SwiftUI for the iOS application part of the project (both of which I have never used before).
In the boilerplate for the KMM application there is a Greeting class which has a greeting method that returns a string:
package com.example.myfirstapp.shared
class Greeting {
fun greeting(): String {
return "Hello World!"
}
}
If the shared package is imported into the iOS project using SwiftUI, then the greeting method can be invoked and the string that's returned can put into the View (e.g. Text(Greeting().greeting()))
I have implemented a different shared Kotlin class whose properties are modified/fetched using getters and setters, e.g. for simplicity:
package com.example.myfirstapp.shared
class Counter {
private var count: Int = 0
getCount() {
return count
}
increment() {
count++
}
}
In my Android app I can just instantiate the class and call the setters to mutate the properties of the class and use it as application state. However, I have tried a number of different ways but cannot seem to find the correct way to do this within SwiftUI.
I am able to create the class by either creating a piece of state within the View that I want to use the Counter class in:
#State counter: Counter = shared.Counter()
If I do this then using the getCount() method I can see the initial count property of the class (0), but I am not able to use the setter increment() to modify the property the same way that I can in the Android Activity.
Any advice on the correct/best way to do this would be greatly appreciated!
Here's an example of what I'd like to be able to do just in case that helps:
import shared
import SwiftUI
struct CounterView: View {
#State var counter: shared.Counter = shared.Counter() // Maybe should be #StateObject?
var body: some View {
VStack {
Text("Count: \(counter.getCount())")
Button("Increment") { // Pressing this button updates the
counter.increment() // UI state on the previous line
}
}
}
}
I believe the fundamental issue is that there isn't anything that's notifying SwiftUI layer when the count property is changed in the shared code (when increment is called). You can at least verify that value is being incremented by doing something like following (where we manually retrieve updated count after incrementing it)
struct ContentView: View {
#ObservedObject var viewModel = ViewModel(counter: Counter())
var body: some View {
VStack {
Text("Count: \(viewModel.count)")
Button("Increment") {
viewModel.increment()
}
}
}
}
class ViewModel: ObservableObject {
#Published var count: Int32 = 0
func increment() {
counter.increment()
count = counter.getCount()
}
private let counter: Counter
init(counter: Counter) {
self.counter = counter
}
}

SwiftUI - data source as struct - performance and redundancy through copies - why still use it?

I have a minimal working example of something I'm still not sure about:
import SwiftUI
struct Car {
var name: String
}
class DataModel: ObservableObject {
#Published var cars: [Car]
init(_ cars: [Car]) {
self.cars = cars
}
}
struct TestList: View {
#EnvironmentObject var dataModel: DataModel
var body: some View {
NavigationView {
List(dataModel.cars, id: \.name) { car in
NavigationLink(destination: TestDetail(car: car).environmentObject(self.dataModel)) {
Text("\(car.name)")
}
}
}
}
}
struct TestDetail: View {
#EnvironmentObject var dataModel: DataModel
var car: Car
var carIndex: Int {
dataModel.cars.firstIndex(where: {$0.name == self.car.name})!
}
var body: some View {
Text(car.name)
.onTapGesture {
self.dataModel.cars[self.carIndex].name = "Changed Name"
}
}
}
struct TestList_Previews: PreviewProvider {
static var previews: some View {
TestList().environmentObject(DataModel([.init(name: "A"), .init(name: "B")]))
}
}
It's about the usage of structs as data models. The example is similar to the official SwiftUI tutorial by Apple (https://developer.apple.com/tutorials/swiftui/handling-user-input).
Basically, we have a DataModel class that is passed down the tree as EnvironmentObject. The class wraps the basic data types of our model. In this case, it's an array of the struct Car:
class DataModel: ObservableObject {
#Published var cars: [Car]
...
}
The example consists of a simple list that shows the names of all cars. When you tap on one, you get to a detail view. The detail view is passed the car as property (while the dataModel is passed as EnvironmentObject):
NavigationLink(destination: TestDetail(car: car).environmentObject(self.dataModel)) {
Text("\(car.name)")
}
The property car of the detail view is used to populate it. However, if you want to e.g. change the name of the car from within the detail view you have to go through the dataModel because car is just a copy of the original instance found in dataModel. Thus, you first have to find the index of the car in the dataModel's cars array and then update it:
struct TestDetail: View {
...
var carIndex: Int {
dataModel.cars.firstIndex(where: {$0.name == self.car.name})!
}
...
self.dataModel.cars[self.carIndex].name = "Changed Name"
This doesn't feel like a great solution. Searching for the index is a linear operation you have to do whenever you want to change something (the array could change at any time, so you have to constantly repeat the index search).
Also, this means that you have duplicate data. The car property of the detail view exactly mirrors the car of the viewModel. This separates the data. It doesn't feel right.
If car was a class instead of a struct, this would no be a problem because you pass the instance as reference. It would be much simpler and cleaner.
However, it seems that everyone wants to use structs for these things. Sure, they are safer, there can't be reference cycles with them but it creates redundant data and causes more expensive operations. At least, that's what it looks like to me.
I would love to understand why this might not be a problem at all and why it's actually superior to classes. I'm sure I'm just having trouble understanding this as a new concept.

Resources