11

Ok,

so this might be trivial, but I am not sure how to go about this.

I have a UIViewController that gets created when the SwiftUI view calls:

func makeUIViewController(context: Context) -> MyViewController

The View that makes that call was given an environment object in the SceneDelegate like we have seen in the tutorials:

window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(MyData()))

What I am trying to do is to use that environment object (MyData()) within my UIViewController logic. The ViewController would read/write on MyData's instance as needed, and from what I understand that should cause the SwiftUI view to react accordingly since MyData conforms to BindableObject...

So in the makeUIViewController call I get the UIViewControllerRepresentableContext. I can see the environment in the context:

context.environment

and if I print it in the console during debug I see this:

context.environment: [EnvironmentPropertyKey<PreferenceBridgeKey> = Value(value: Optional(SwiftUI.PreferenceBridge)), EnvironmentPropertyKey<FontKey> = Optional(SwiftUI.Font(provider: SwiftUI.(unknown context at $1c652cbec).FontBox<SwiftUI.Font.(unknown context at $1c656e2cc).TextStyleProvider>)), .......

In the print I see the MyData environmentObject instance:

EnvironmentPropertyKey<StoreKey<MyData>> = Optional(MyApp.MyData), ...

I am not sure how to get MyData out of the environment values given to me in the context.environment....

I have tried to figure out how to get the proper EnvironmentKey for MyData so I could try access it view subscript ... context.environment[myKey...]

How can I get MyData back from the environment values given to me by the context?

zumzum
  • 17,984
  • 26
  • 111
  • 172
  • 2
    `UIViewControllerRepresentable` is still a `View` of sort, so I tried to use `@EnvironmentObject` inside it, and made sure the superview has an environment object passed. It gets passed to the view controller, BUT if you access it the app crashes, because you're using an environment object OUTSIDE a body. I think it's best to pass `Bindings`, unless someone finds a better way – Matteo Pacini Jun 14 '19 at 17:04
  • interesting. I wondered also about this approach being good or bad etc... then I thought why wouldn't I be ok using MyData in my UIViewController is the context passed to me has that in the environments values... still trying to get all this worked out in my head. – zumzum Jun 14 '19 at 17:30
  • It's too early to develop good practices, with a frameworks that lacks items and proper documentation. At the moment, it's mostly... "workarounds". I used `Bindings` with success in my answers concerning`UIVIewRepresentable`, and it seemed like a good call: please check https://stackoverflow.com/a/56564377/2890168 and https://stackoverflow.com/a/56568715/2890168 to see some examples. – Matteo Pacini Jun 14 '19 at 17:34
  • 1
    I'm trying to do the same thing with UIViewRepresentable. As of beta 3, I've tried all you have with no success. I've worked around it by passing the data needed by injecting it through initializers. Works fine, but it would be nicer to get it straight out of context.environment. Perhaps in beta 4? – Jim Marquardt Jul 08 '19 at 18:44

2 Answers2

2

Using @EnvironmentObject now works (but not in Xcode Preview). Used Xcode 11.1/Swift 5.1. For simplicity it was used UIViewRepresentable, but the same should work for UIViewControllerRepresentable, because it is also SwiftUI View

Here is complete demo

Change @EnvironmentObject from UIButton

import SwiftUI
import Combine
import UIKit

class AppState: ObservableObject {
    @Published var simpleFlag = false
}

struct CustomUIView: UIViewRepresentable {
    typealias UIViewType = UIButton

    @EnvironmentObject var settings: AppState

    func makeUIView(context: Context) -> UIButton {
        let button = UIButton(type: UIButton.ButtonType.roundedRect)
        button.setTitle("Tap UIButton", for: .normal)
        button.actionHandler(controlEvents: UIControl.Event.touchUpInside) {
            self.settings.simpleFlag.toggle()
        }
        return button
    }

    func updateUIView(_ uiView: UIButton, context: UIViewRepresentableContext<CustomUIView>) {
    }

}

struct ContentView: View {
    @ObservedObject var settings: AppState = AppState()

    var body: some View {
        VStack(alignment: .center) {
            Spacer()
            CustomUIView()
                .environmentObject(self.settings)
                .frame(width: 100, height: 40)
                .border(Color.blue)
            Spacer()
            if self.settings.simpleFlag {
                Text("Activated").padding().background(Color.red)
            }
            Button(action: {
                self.settings.simpleFlag.toggle()
            }) {
                Text("SwiftUI Button")
            }
            .padding()
            .border(Color.blue)
        }
        .edgesIgnoringSafeArea(.all)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(AppState())
    }
}

/// Just utility below
extension UIButton {
    private func actionHandler(action:(() -> Void)? = nil) {
        struct __ { static var action :(() -> Void)? }
        if action != nil { __.action = action }
        else { __.action?() }
    }
    @objc private func triggerActionHandler() {
        self.actionHandler()
    }
    func actionHandler(controlEvents control :UIControl.Event, for action:@escaping () -> Void) {
        self.actionHandler(action: action)
        self.addTarget(self, action: #selector(triggerActionHandler), for: control)
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
0

I had the same question and it was answered by Apples excellent tutorial https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit.

What you do basically is pass a binding into your ViewController.


import SwiftUI
import UIKit

struct PageViewController<Page: View>: UIViewControllerRepresentable {
    var pages: [Page]
    @Binding var currentPage: Int
    ...

Which is then passed the binding like so:


import SwiftUI

struct PageView<Page: View>: View {
    var pages: [Page]
    @State private var currentPage = 0

    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            PageViewController(pages: pages, currentPage: $currentPage)
            //                               Here is the binding -^
            PageControl(numberOfPages: pages.count, currentPage: $currentPage)
                .frame(width: CGFloat(pages.count * 18))
                .padding(.trailing)
        }
    }
}

This also works of course with @EnvironmentObject instead of @State

bnassler
  • 591
  • 6
  • 15