2

How do I pass incoming data from a method triggered by a delegate in a Swift class to an EnvironmentObject?

I am aware that for this to work my Swift class needs to be called/initialized from the SwiftUI struct (be a child of the parent SwiftUI struct). However, I initialise my Swift class in the ExtensionDelegate of the Apple Watch app. I would like to see the UI Text element change when the name is updated.

The following code runs on the Apple Watch:

class User: ObservableObject {
    
    @Published var id: UUID?
    @Published var name: String?
}
//SwiftUI struct
struct UI: View {

@EnvironmentObject var userEnv: User

var body: some View {
   Text(userEnv.name)
 }
}
// Swift class
class WatchConnectivityProvider: NSObject, WCSessionDelegate {

 static let shared = WatchConnectivityProvider()
 private let session: WCSession
    
    init(session: WCSession = .default) {
        self.session = session
        super.init()
    }
    
    func activateSession() {
        if WCSession.isSupported() {
            session.delegate = self
            session.activate()
        }
    }

    //This func gets triggered when data is sent from the iPhone
    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        
        let list = message["list"]
        
        let jsonDecoder = JSONDecoder()
        if let data = try? jsonDecoder.decode(User.self, from: list as! Data) {
            
        // !!! This is where I would like to update the EnvironmentObject userEnv !!!   
        // What is the best way to do this? Remember, this class has already been initialised within the ExtensionDelegate.
        }
    }
}
//ExtensionDelegate of Apple Watch app, initialising the WatchConnectivityProvider
class ExtensionDelegate: NSObject, WKExtensionDelegate {

    func applicationDidFinishLaunching() {
        // Perform any final initialization of your application.
        WatchConnectivityProvider.shared.activateSession()
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
Peanutsmasher
  • 220
  • 3
  • 13

2 Answers2

0

Dependency Injection

One of the solutions could be to store the reference to your @EnvironmentObject globally, eg. in some dependency container.

enum Dependencies {
    struct Name: Equatable {
        let rawValue: String
        static let `default` = Name(rawValue: "__default__")
        static func == (lhs: Name, rhs: Name) -> Bool { lhs.rawValue == rhs.rawValue }
    }

    final class Container {
        private var dependencies: [(key: Dependencies.Name, value: Any)] = []

        static let `default` = Container()

        func register(_ dependency: Any, for key: Dependencies.Name = .default) {
            dependencies.append((key: key, value: dependency))
        }

        func resolve<T>(_ key: Dependencies.Name = .default) -> T {
            return (dependencies
                .filter { (dependencyTuple) -> Bool in
                    return dependencyTuple.key == key
                        && dependencyTuple.value is T
                }
                .first)?.value as! T
        }
    }
}

Then you create your object like this:

Dependencies.Container.default.register(User())

And you can access it from anywhere in your code:

let user: User = Dependencies.Container.default.resolve()
user.modify()

A more detailed explanation of Dependency Injection for Swift can be found here.

Singleton

Alternatively you can use standard Singleton pattern to make your User data available globally. A more detailed explanation can be found here.

Final thoughts

Clean Architecture for SwiftUI is a good example (in my opinion at least) of how to write an iOS app in the clean way. It's a little bit complicated, but you can pick up some parts.

pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • 1
    This seems awfully complicated. Especially, when the custom objects are complex. My understanding of EnvironmentObject is to allow app-wide sharing of data and its updates. This should not require Dependencies on top. While I am sure that this is a way to do it, it does not feel SwiftUI-like. – Peanutsmasher Jun 02 '20 at 07:28
  • 1
    True, it is complicated. The problem with `EnvironmentObject` is that it's a kind of singleton but for SwiftUI `Views` only. If you want a simple solution, try using a normal singleton. An similar question is [here](https://stackoverflow.com/questions/59379898/app-delegate-accessing-environment-object). – pawello2222 Jun 02 '20 at 07:51
  • 1
    So the only way to use Swift Classes with SwiftUI is to call the class & method from within SwiftUI, such as in .onAppear{ self.userEnv.name = WatchConnectivityProvider.shared.getData() }? If I want to get regular updates, I have to call the method on a Timer interval. This feels like an immature design. Don't I want my application logic to reside in Swift Classes? – Peanutsmasher Jun 02 '20 at 08:10
  • If you want to follow `MVVM` pattern, you can have a `ViewModel` class with your business logic residing there and have it as an `ObservedObject` in your `View`. You can use it like `.onAppear{ self.viewModel.load() }`. For the problem of accessing the env object in the `ViewModel` I recommend using the `Dependency Injection`. Otherwise there's only a `Singleton` left from what I know. Remember, it's still the early stage of SwiftUI. There might be better solutions in the future. – pawello2222 Jun 02 '20 at 08:34
  • [Here](https://nalexn.github.io/clean-architecture-swiftui/) is a good article of how to write an iOS app in the *clean* way. – pawello2222 Jun 02 '20 at 09:20
0

Here bad Jumbo code:

class ExtensionDelegate: ObservableObject, NSObject, WCSessionDelegate, WKExtensionDelegate {
var session: WCSession?
@Published var model = Model

func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
    print(#function)
    var replyValues = Dictionary<String, Any>()
    replyValues["status"] = "failed"
    
    // 2442 Bytes
    if let data = message["data"] as? Data {
        // push that work back to the main thread
        DispatchQueue.main.async {
            self.model = try? JSONDecoder().decode(Model.self, from: data)
        }
        if let vm = vm {
            replyValues["status"] = "ok"
            replyValues["id"] = vm.id.uuidString
        }
    }
    replyHandler(replyValues)
}
...
William Choy
  • 1,291
  • 9
  • 7