Change SwiftUI Text from UIViewController - ios

I'm quite new to Swift and absolute new to SwiftUI.
I'm trying to combine UIViewController Google Maps and modern SwiftUI.
In SwiftUI i have a few Text objects, and I want my GmapsController class to be able to modify these Text values, and redraw SwiftUI struct.
My main struct:
var _swiftUIStruct : swiftUIStruct = swiftUIStruct() // to have access in GmapsController
struct GoogMapView: View {
var body: some View {
let gmap = GmapsControllerRepresentable()
return ZStack {
gmap
_swiftUIStruct
}
}
}
UIViewControllerRepresentable wrapper of GmapsController :
struct GmapsControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<GmapsControllerRepresentable>) -> GmapsController {
return GmapsController()
}
func updateUIViewController(_ uiViewController: GmapsController, context: UIViewControllerRepresentableContext<GmapsControllerRepresentable>) {}
}
The GmapsController itself:
class GmapsController: UIViewController, CLLocationManagerDelegate, GMSMapViewDelegate {
var locationManager = CLLocationManager()
var mapView: GMSMapView!
override func viewDidLoad() {
super.viewDidLoad()
locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
let camera = GMSCameraPosition.camera(
withLatitude: (locationManager.location?.coordinate.latitude)!,
longitude: (locationManager.location?.coordinate.longitude)!,
zoom: 15
)
mapView = GMSMapView.map(withFrame: view.bounds, camera: camera)
mapView.delegate = self
self.view = mapView
}
// HERE I WANT TO CHANGE SOME swiftUIStruct Text FIELDS
// calls after map move
func mapView(_ mapView: GMSMapView, idleAt сameraPosition: GMSCameraPosition) {
_swiftUIStruct.someTxt = "I hope this will work" // compilation pass, but value doesn't change
}
}
And the swiftUIStruct struct:
struct swiftUIStruct {
#State var someTxt = ""
var body: some View {
Text(someTxt) // THE TEXT I WISH I COULD CHANGE
}
}
Googling this a whole day just made me feel dumb, any help is appreciated.

I hope my example code helps. Basically, move the model data outside, and pass it along, and change it. If you run this code, you will see the text "I hope this will work", NOT "Initial Content".
import SwiftUI
class ViewModel: ObservableObject {
#Published var someTxt = "Initial Content"
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
ZStack {
GoogleMapsView(viewModel: viewModel)
Text(viewModel.someTxt)
}
}
}
struct GoogleMapsView: UIViewControllerRepresentable {
var viewModel: ViewModel
func makeUIViewController(context: Context) -> GmapsController {
let controller = GmapsController()
controller.viewModel = viewModel
return controller
}
func updateUIViewController(_ uiViewController: GmapsController, context: Context) {}
}
class GmapsController: UIViewController {
var viewModel: ViewModel?
override func viewDidLoad() {
viewModel?.someTxt = "I hope this will work"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return ContentView()
}
}

Related

How do I declare a view so it's initialization and itself are NOT in the representable view?

I have a UIKit representable view for using Mapbox in my SwiftUI app and control the mapView from inside the Coordinator by passing the MapboxViewRepresentable View itself when making the coordinator.
The problem I am having is that the mapView, its coordinator and subscriptions are all being created TWICE! While trying to figure out what the problem was someone commented that my problem was "holding on to the view in a variable in the representable. Then the initialization and the view are in the make. Don’t put it on a variable at struct level"
So I adjusted my code and passed the mapView inside makeUIView instead of declaring it in MapboxMapViewRepresentable and used didSet inside of the Coordinator to create/save the subscriptions once the view was passed.
Since I need to manipulate the environmentObjects via MapboxMapViewRepresentable.Coordinator methods, I have a control property (also REQUIRED by super.init() ) declared in the Coordinator.
I have no idea why everything is still being created twice, I don't think the fact I used extensions is doing it??
Anything else I could think of to check???
Thank you
PARENT STRUCT & REPRESENTABLE
struct MapboxMapView: View {
#ObservedObject var mapVM : MapViewModel
#ObservedObject var popupVM : PopupProfileViewModel
var body: some View {
ZStack{
MapboxMapViewRepresentable()
.environmentObject(self.mapVM)
.environmentObject(self.popupVM)
}
}
}
struct MapboxMapViewRepresentable: UIViewRepresentable {
#EnvironmentObject var session: SessionStore
#EnvironmentObject var mapVM : MapViewModel
#EnvironmentObject var popupVM: PopupProfileViewModel
func makeCoordinator() -> Coordinator {
let coordinator = Coordinator(control: self)
return coordinator
}
func makeUIView(context: Context) -> MGLMapView {
let view = MGLMapView(frame: .zero, styleURL: URL(string: MAPBOX_STYLE_URL))
context.coordinator.mapView = view
view.showsUserLocation = true
view.setCenter(CLLocationCoordinate2D(latitude: mapVM.usersCurrentLat, longitude: mapVM.usersCurrentLong), zoomLevel: MapViewModel.startAtZoomLevel, animated: false)
view.delegate = context.coordinator
view.maximumZoomLevel = MapViewModel.maxZoomLevel
view.allowsRotating = false
view.direction = 0.0
view.allowsTilting = false
mapVM.zoomLevel = view.zoomLevel
return view
}
func updateUIView(_ mapView: MGLMapView, context: Context) { }
}
Coordinator (extension of MapboxMapViewRepresentable)
extension MapboxMapViewRepresentable {
class Coordinator: NSObject {
var control: MapboxMapViewRepresentable
let locationManager = CLLocationManager()
var subscriptions = Set<AnyCancellable>()
// MARK: NEW
var mapView : MGLMapView? {
didSet {
guard let mapView = mapView else { return }
// all of these run twice since map and coordinator are created twice
control.mapVM.placePublisher.sink { bool in
guard let self = self else { return }
// control.mapVM.doSomething
print(bool)
}.store(in: &subscriptions)
Self.showRandomPlace.sink { location in
// mapView.setCenter....
// control.popupVM.doSomething()
print(location)
}.store(in: &subscriptions)
control.mapVM.newUserSelectedPublisher.sink { nearestPlaceCoords in
DispatchQueue.main.async {
mapView.zoomLevel = 15.9
mapView.setCenter(CLLocationCoordinate2D(latitude: nearestPlaceCoords.lat, longitude: nearestPlaceCoords.long), animated: false)
}
}.store(in: &subscriptions)
}
}
// publishes zoomLevel with coordiantor method regionIsChanging to update viewModel with current zoomLevel
let zoomLevelPublisher = PassthroughSubject<Double, Never>()
var previosZoomLevel = MapViewModel.maxZoomLevel
init(control: MapboxMapViewRepresentable) {
self.control = control
super.init()
setupLocationManager()
// set inital map view's center to user's/device's current location
if let coordinate = locationManager.location?.coordinate {
guard let mapView = mapView else { return }
self.setCenterAt(coordinate: coordinate)
control.mapVM.mapCenterCoordinate = mapView.centerCoordinate
}
} // END INIT
deinit {
subscriptions.forEach{$0.cancel()}
subscriptions.removeAll()
}
}
}
How delegate methods are declared
extension MapboxMapViewRepresentable.Coordinator {
func mapView(_: MGLMapView, didFinishLoading style: MGLStyle) {
print("DID FINISH LOADING STYLE")
self.control.mapVM.didFinishLoadingUrlStyle = true
self.control.mapVM.loadCurrentUsersPlaces()
self.control.mapVM.loadNearestPlaces()
}
}
You can pass a reference to the UIView to your Coordinator. That might look like this:
struct MapboxMapViewRepresentable: UIViewRepresentable {
#EnvironmentObject var session: SessionStore
#EnvironmentObject var mapVM : MapViewModel
#EnvironmentObject var popupVM: PopupProfileViewModel
func makeCoordinator() -> Coordinator {
let coordinator = Coordinator(control: self)
return coordinator
}
func makeUIView(context: Context) -> MGLMapView {
let view = MGLMapView(frame: .zero, styleURL: URL(string: MAPBOX_STYLE_URL))
context.coordinator.mapView = view
return view
}
func updateUIView(_ mapView: MGLMapView, context: Context) {
//code
}
class Coordinator: NSObject {
let locationManager = CLLocationManager()
var control: MapboxMapViewRepresentable
var subscriptions = Set<AnyCancellable>()
var mapView : MGLMapView? = nil {
didSet {
// subscriber receives data via publisher to manipulate map
mapView?.addAnnotations.sink { annotations in
//annotation sink
}.store(in: &subscriptions)
}
}
init(control: MapboxMapViewRepresentable) {
self.control = control
super.init()
setupLocationManager()
}
}
}

SwiftUI View init called multiple times

I am pretty new to SwiftUI. I have a very simple view. It's just a root view that contains a WKWebView wrapped in a UIViewRepresentable. My problem is, that the init method of the UIViewRepresentable is called 6 times when the view is opened. Which means the WKWebView is initialised 6 times and all my initialisation code (setting JS callbacks, ...) is called 6 times. I added print statements to the init functions of the root view MyWebView and the subview WebView (the UIViewRepresentable). The root view init is only called once, but the subview's init is called 6 times. Is this normal? Or am I doing something wrong?
struct MyWebView: View {
#ObservedObject private var viewModel = WebViewModel()
init() {
print("root init")
}
var body: some View {
VStack(alignment: .leading, spacing: 0, content: {
WebView(viewModel: viewModel)
})
.navigationBarTitle("\(viewModel.title)", displayMode: .inline)
.navigationBarBackButtonHidden(true)
} }
struct WebView: UIViewRepresentable {
var wkWebView: WKWebView!
init(viewModel: WebViewModel) {
print("webview init")
doWebViewInitialization(viewModel: viewModel)
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
let request = URLRequest(url: URL(string: "https://www.google.com")!, cachePolicy: .returnCacheDataElseLoad)
wkWebView.load(request)
return wkWebView
}
}
I'm not getting your issue of multiple calls to the init method of the UIViewRepresentable.
I modified slightly your WebView, and this is how I tested my answer:
import SwiftUI
import Foundation
import WebKit
#main
struct TestSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MyWebView()
}.navigationViewStyle(StackNavigationViewStyle())
}
}
// for testing
class WebViewModel: ObservableObject {
#Published var title = ""
}
struct WebView: UIViewRepresentable {
let wkWebView = WKWebView()
init(viewModel: WebViewModel) {
print("\n-----> webview init")
// doWebViewInitialization(viewModel: viewModel)
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
if let url = URL(string: "https://www.google.com") {
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
wkWebView.load(request)
}
return wkWebView
}
func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<WebView>) { }
}
struct MyWebView: View {
#ObservedObject private var viewModel = WebViewModel()
init() {
print("\n-----> root init")
}
var body: some View {
VStack(alignment: .leading, spacing: 0, content: {
WebView(viewModel: viewModel)
})
.navigationBarTitle("\(viewModel.title)", displayMode: .inline)
.navigationBarBackButtonHidden(true)
}
}
This leaves "doWebViewInitialization" with a possible problem spot.
You have to write your code assuming that the initializer of the View in SwiftUI will be called many times.
You write the initialization process in makeUIView(context:) in this case.
See:
https://developer.apple.com/documentation/swiftui/uiviewrepresentable/makeuiview(context:)
For example, I wrote the following code based on this answer. I added a toggle height button to this referenced code.
the -----> makeUIView log is only output once,
but the -----> webview init logs are output every time the toggle button is pressed.
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
MyWebView()
}
}
class WebViewModel: ObservableObject {
#Published var title = ""
}
struct WebView: UIViewRepresentable {
let wkWebView = WKWebView()
init(viewModel: WebViewModel) {
print("\n-----> webview init")
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
print("\n-----> makeUIView")
if let url = URL(string: "https://www.google.com") {
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
wkWebView.load(request)
}
return wkWebView
}
func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<WebView>) { }
}
struct MyWebView: View {
#State private var toggleHight = false
#ObservedObject private var viewModel = WebViewModel()
init() {
print("\n-----> root init")
}
var body: some View {
VStack {
WebView(
viewModel: viewModel
)
.frame(
height: { toggleHight ? 600 : 300 }()
)
Button(
"toggle",
action: {
toggleHight.toggle()
}
)
}
}
}
Furthermore, I realized after I wrote example code that WebView: UIViewRepresentable should not have an instance variable of wkWebView.
Please do it all(create instance and configuration) in makeUIView(context:), as shown below.
This is because instance variables are recreated every time the initializer is called.
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
MyWebView()
}
}
class WebViewModel: ObservableObject {
#Published var title = ""
}
struct WebView: UIViewRepresentable {
init(viewModel: WebViewModel) {
print("\n-----> webview init")
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
print("\n-----> makeUIView")
let wkWebView = WKWebView()
if let url = URL(string: "https://www.google.com") {
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
wkWebView.load(request)
}
return wkWebView
}
func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<WebView>) { }
}
struct MyWebView: View {
#State private var toggleHight = false
#ObservedObject private var viewModel = WebViewModel()
init() {
print("\n-----> root init")
}
var body: some View {
VStack {
WebView(
viewModel: viewModel
)
.frame(
height: { toggleHight ? 600 : 300 }()
)
Button(
"toggle",
action: {
toggleHight.toggle()
}
)
}
}
}
I struggled with this tight constraint when I was developing with UIViewControllerRepresentable. With the help of my colleagues, I managed to finish the code.
Your code has been called 6 times, so there may be some problem. but I cannot tell what the problem is from the code you provided.
It is common for init to be called multiple times in SwiftUI. We need to write code to deal with this. If your init is being called too often, you may want to look for the root cause. The code I referred to and the code I wrote are only once at startup.

Pass variable from UIViewController to SwiftUI View

I can't find a way or a good tutorial to explain how to pass the value of a variable (String or Int) that is owned by a UIViewController to a SwiftUI view that is calling the view.
For example:
class ViewController: UIViewController {
var myString : String = "" // variable of interest
....
func methodThatChangeValueOfString(){
myString = someValue
}
}
// to make the view callable on SwiftUI
extension ViewController: UIViewControllerRepresentable {
typealias UIViewControllerType = ViewController
public func makeUIViewController(context: UIViewControllerRepresentableContext<ViewController>) -> ViewController {
return ViewController()
}
func updateUIViewController(_ uiViewController: ViewController, context: UIViewControllerRepresentableContext<ViewController>) {
}
}
In SwiftUI I'll have
struct ContentView: View {
var body: some View {
ViewController()
}
}
How can I take myString of the ViewController and use it in ContentView?
Thanks in advance
Use MVVM pattern it is what is recommended with SwiftUI.
Share a ViewModel between your SwiftUI View and your UIKit ViewController.
I suggest you start with the basic Apple SwiftUI tutorials. Specifically how to "Interface with UIKit"
https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
import SwiftUI
struct SwiftUIView: View {
#StateObject var sharedVM: SharedViewModel = SharedViewModel()
var body: some View {
VStack{
UIKitViewController_UI(sharedVM: sharedVM)
Text(sharedVM.myString)
}
}
}
class SharedViewModel: ObservableObject{
#Published var myString = "init String"
}
//Not an extension
struct UIKitViewController_UI: UIViewControllerRepresentable {
typealias UIViewControllerType = UIKitViewController
var sharedVM: SharedViewModel
func makeUIViewController(context: Context) -> UIKitViewController {
return UIKitViewController(vm: sharedVM)
}
func updateUIViewController(_ uiViewController: UIKitViewController, context: Context) {
}
}
class UIKitViewController: UIViewController {
let sharedVM: SharedViewModel
var runCount = 0
init(vm: SharedViewModel) {
self.sharedVM = vm
super.init(nibName: nil, bundle: nil)
//Sample update mimics the work of a Delegate or IBAction, etc
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
self.runCount += 1
self.methodThatChangeValueOfString()
if self.runCount == 10 {
timer.invalidate()
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func methodThatChangeValueOfString(){
sharedVM.myString = "method change" + runCount.description
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}

Sheet not dismissing with presentation mode in SwiftUI/UIKit?

I have a SwiftUI view that displays a sheet using a #State variable:
import SwiftUI
struct AdRevenue: View {
#State var playAd = false
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(action: {
self.playAd = true
})
{
Text("Play Ad")
}.sheet(isPresented: $playAd) {
Ads()}
}
}
This is the UIViewRepresentable sheet:
struct Ads: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: Context) -> UIViewController {
return ViewController()
}
func updateUIViewController(_ uiView: UIViewController, context: Context) {
}
class ViewController: UIViewController, GADRewardedAdDelegate, AdManagerRewardDelegate {
var rewardedAd: GADRewardedAd?
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
override func viewDidLoad() {
super.viewDidLoad()
AdManager.shared.loadAndShowRewardAd(AdIds.rewarded.rawValue, viewController: self)
AdManager.shared.delegateReward = self
}
func rewardedAd(_ rewardedAd: GADRewardedAd, userDidEarn reward: GADAdReward) {
print("Reward received: \(reward.type), amount \(reward.amount).")
}
}
}
Within AdManager, a function is called as such:
func rewardAdDidClose() {
let mom = Ads()
mom.presentationMode.wrappedValue.dismiss()
print("mom.presentationMode.wrappedValue.dismiss()")
}
Yet although I see the presentationMode message when I run it, the sheet doesn't get dismissed. Is it possible to dismiss the sheet like this?
Is this because you have two Ads values?
The first is created by SwiftUI (inside the AdRevenue view), so its presentationMode property is likely to be correctly wired-up to the app's environment.
But later, within AdManager, you're creating another Ads value, and expecting it to have a usable presentationMode environment object. It's being created outside of SwiftUI, so cannot know about the environment of the rest of your app.
I'd try passing the Ads value into your AdManager, rather than having AdManager create a new one.

UIViewRepresentable.updateUIView(_:context:) is not called when #State, #Binding or #EnvironmentObject are changed

UIViewRepresentable.updateUIView is not called when an ObservableObject is changed that this view and its parent view depend on.
I have a GameView that has "#State var locationManager: LocationManager" which is passed to my MapView as a binding. The MapView conforms to UIViewRepresentable protocol. LocationManager conforms, among other things, to ObservableObject and has the following conformance-related code:
var didChange = PassthroughSubject<locationmanager, Never>()
var lastKnownLocation: CLLocation {
didSet {
// Propagate the update to the observers.
didChange.send(self)
print("Finished propagating lastKnownLocation to the observers.")
}
}
I suppose that GameView and consequently MapView must be updated every time LocationManager.lastKnownLocation changes. In practice I only see MapView.updateUIView() called when I exit the app per home button. At this point control gets in updateUIView() and when I open the app again (not compile-install-run), I get the update. This also happens once short after the GameView() was presented.
Is my understanding of how SwiftUI works wrong or is it some bug? How do I get it right?
struct GameView: View {
#State var locationManager: LocationManager
var body: some View {
MapView(locationManager: $locationManager)
}
}
struct MapView: UIViewRepresentable {
#Binding var locationManager: LocationManager
func makeUIView(context: Context) -> GMSMapView{ ... }
func updateUIView(_ mapView: GMSMapView, context: Context) { ... }
}
class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject {
var didChange = PassthroughSubject<LocationManager, Never>()
var lastKnownLocation: CLLocation {
didSet {
// Propagate the update to the observers.
didChange.send(self)
print("Finished propagating lastKnownLocation to the observers.")
}
}
...
}
I expect that every tim LocationManager.lastKnownLocation changes, MapView.updateUIView() is called. Practically the call only happens when I exit the app per home button (and enter again)
change MapView like this
struct MapView: UIViewRepresentable {
#State var locationManager = LocationManager()
func makeUIView(context: Context) -> GMSMapView{
let view = GMSMapView(frame: .zero)
...
willAppear(context)
return view
}
func updateUIView(_ mapView: GMSMapView, context: Context) { ... }
func makeCoordinator() -> MapView.Coordinator {
return Coordinator()
}
static func dismantleUIView(_ uiView: GMSMapView, coordinator: MapView.Coordinator) {
coordinator.cancellable.removeAll()
}
class Coordinator {
var cancellable = Set<AnyCancellable>()
}
}
extension MapView {
func willAppear(_ context: Context) {
locationManager.didChange.sink{
print("location: \($0)")
}.store(in: &context.coordinator.cancellable)
locationManager.startUpdating()
}
}
I think this method locationManager.startUpdating(), locationManager.stopUpdating() need call another class.

Resources