Using Kotlin mulitplatform classes in SwiftUI - ios

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
}
}

Related

NSKeyValueCoding.setValue(_:forKey:) not working in SwiftUI

I am unable to produce a proper minimal working example, mainly due to my novice level understanding of iOS development, but I do have a simple SwiftUI project that may help.
In my ContentView.swift:
import SwiftUI
struct ContentView: View {
#State var viewText :String
var myClass :MyClass
var body: some View {
VStack{
Text(viewText)
.padding()
Button("Update Text", action: {
myClass.update()
viewText = myClass.txt
})
}
}
}
class MyClass: NSObject {
var txt :String = ""
var useSetVal :Bool = false
func update(){
if(useSetVal){
setValue("used set val", forKey: "txt")
} else {
txt = "used ="
}
useSetVal = !useSetVal
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let mc = MyClass()
ContentView(viewText: "", myClass: mc)
}
}
and in my PracticeApp.swift
import SwiftUI
#main
struct PracticeApp: App {
var body: some Scene {
WindowGroup {
let mc = MyClass()
ContentView(viewText: "", myClass: mc)
}
}
}
In this app, I expect to see the text toggle between "used =" and "used setVal" as I push the button. Instead, I get an exception when I call setValue:
Thread 1: "[<Practice.MyClass 0x60000259dc20> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key txt."
I've been reviewing the answers here but since most answers refer to xib and storyboard files (which I either don't have, or don't know how to find), I don't see how they relate.
By the way, even though the app I'm actually trying to fix doesn't use SwiftUI and the issue with setValue is different, it's still true that I either don't have .xib or .storyboard files or I just don't know where to find them.
I'd appreciate help from any one who could either help me figure out the issue with my example, or who can get me closer to solving the issue with my actual app (including how to produce a proper MWE).
I believe what I've already written is sufficient for the issue (at least for a start), but for those interested, I thought I'd add the full story.
The Full Story
I'm new to iOS development, and I've just taken ownership of an old iOS app. It hasn't really been touched since 2017. I noticed an animation that is not working. Though I cannot verify that it ever did work, I have good reason to assume that it once did, but I can't say when it stopped working.
One issue I noticed is that animated properties are supposed to be updated with the NSKeyValueCoding.setValue(_:forKey:) function, but nothing seems to happen when the function is called.
I was able to work around the issue by overriding the setValue function with my own which basically uses a switch statement to map each key to its corresponding value. However, this did not fix the animation or explain why the setValue function isn't working.
Because both the setValue function and the CABasicAnimation.add(_:forKey:) rely on the same keyPath, I wonder if solving one issue might help me solve the other. I've decided to focus on the setValue issue (at least for now).
When I went to work starting a new project to use as an MWE, I noticed that neither the Storyboard nor the SwiftUI interface options provided by Xcode 13.0 (13A233) started me out with a project structure that matched my existing project. It was clear to me that SwiftUI was new and very different from my existing project, but the Storyboard interface wasn't familiar either and after several minutes a reading tutorials, I failed to build a storyboard app that would respond to button presses at all (all the storyboard app tutorials I found seemed to be set up for older versions of Xcode).
SwiftUI will require that you use #ObservedObject to react to changes in an object. You can make this compliant with both observedobject and key-value manipulation as follows:
struct ContentView: View {
#State var viewText :String
#ObservedObject var myClass :MyClass
var body: some View {
VStack{
Text(viewText)
.padding()
Button("Update Text", action: {
myClass.update()
viewText = myClass.txt
})
}
}
}
class MyClass: NSObject, ObservableObject {
#objc dynamic var txt: String = ""
#Published var useSetVal: Bool = false
func update(){
if(useSetVal){
setValue("used set val", forKey: "txt")
} else {
txt = "used ="
}
useSetVal = !useSetVal
}
}
You need to make the txt property available to Objective-C, in order to make it work with KVO/KVC. This is required as the Key-Value Observing/Coding mechanism is an Objective-C feature.
So, either
class MyClass: NSObject {
#objc var txt: String = ""
, or
#objcMembers class MyClass: NSObject {
var txt: String = ""
This will fix the error, and should make your app behave as expected. However, as others have said, you need to make more changes to the code in order to adhere to the SwiftUI paradigms.

Handle Auth state in WindowGroup

I'm new to SwiftUI still and don't really know how to handle best the auth state. If a user is logged in for example i want to redirect him to home screen if not to a certain screen.
I have a service that will tell me if the user is authenticated like: self.authService.isAuthenticated but in my App in WindowGroup i cannot use my service since this is all a struct and i get Cannot use mutating getter on immutable value: 'self' is immutable
I would appreciate a little snippet that can help me solve this here.
My code:
#main
struct MyApp: App, HasDependencies {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// MARK: Services
private lazy var authService: AuthService = dependencies.authService()
var body: some Scene {
WindowGroup {
if !self.authService.isAuthenticated {
WelcomeView()
} else {
MainView()
}
}
}
}
I suppose you want to handle it just for this time, but i'm proposing you look deeper in SwiftUI bindings and state handlings.
So here we just save the value in a variable in the init since this is getting loaded first.
#main
struct MainApp: App, HasDependencies {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// MARK: Services
private lazy var authService: AuthService = dependencies.authService()
var isAuth: Bool = false
init() {
isAuth = self.authService.isAuthenticated
}
var body: some Scene {
WindowGroup {
if isAuth {
MainView()
} else {
WelcomeView()
}
}
}
}
The problem is
private lazy var authService: AuthService = dependencies.authService()
(A) SwiftUI rebuilds views in response to, for example, a #StateObject's ObjectWillChangePublisher. Changes an unwatched variable fall silently in a forest without participating in this UI framework, but would be read if you trigger a state change by some other object. Also, I'd guess that service will be rebuilt every time the struct is first built, but I haven't had a use case for this scenario yet, so I don't know.
(B) You've got a mutating variable holding a reference type stored in a value type. As above, store your service as an #StateObject, which is one way SwiftUI gets around this problem of lifetime management.
To get "lazy" loading, call .onAppear { service.load() }.
That said, you have a services / factory container you probably already want to be an #StateObject and injected into the environment. If you store an ObservableObject inside an ObservableObject, the View will react to the outer object only. That object does not link its ObjectWillChangePublisher to inner objects. You will need to either:
(a) individually inject select services into the environment for children to observe
(b) pass those into observable view models that use Combine to subscribe to specific states
(c) use .onReceive and .onChange APIs on Views to link to specific state changes
(C) Conditionals evaluated in App can cause objects stored in that struct to be rebuilt. Good practice is to keep App super clean, like always. Move any conditional logic to a "Root" View for that Scene.

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
}

#State and #Published properties in child views, SwiftUI

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.

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