Im new to SwiftUI and Im using google maps within my app, I need to track 2 Map events as shown in below code,
Main View :
struct HomeView : View {
#State var mapView = GMSMapView()
#State var locationManager = CLLocationManager()
#State var alert = false
#State var currentLocation = CLLocationCoordinate2D()
#State var isLocationChanged = false
var body: some View{
ZStack{
MapView(mapView: self.$mapView, locationManager: self.$locationManager, alert: self.$alert, currentLocation: self.$currentLocation, isLocationChanged: self.$isLocationChanged)
.equatable()
.ignoresSafeArea(.all)
.onAppear{
self.locationManager.requestWhenInUseAuthorization()
}
if isLocationChanged {
Text("Show Search Progress")
.foregroundColor(.black)
.padding(.vertical,10)
.frame(width: UIScreen.main.bounds.width / 2)
}
}// Show Alert
}
}
MapView :
struct MapView : UIViewRepresentable, Equatable {
#Binding var mapView : GMSMapView
#Binding var locationManager : CLLocationManager
#Binding var alert : Bool
#Binding var currentLocation : CLLocationCoordinate2D
#Binding var isLocationChanged : Bool
static func == (lhs: MapView, rhs: MapView) -> Bool {
return lhs.isLocationChanged == rhs.isLocationChanged && lhs.isLocationChanged != rhs.isLocationChanged
}
func makeUIView(context: Context) -> GMSMapView {
mapView.delegate = context.coordinator
locationManager.delegate = context.coordinator
mapView.isMyLocationEnabled = true
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
locationManager.startUpdatingLocation()
mapView.animate(toLocation: self.currentLocation)
mapView.animate(toZoom: 15)
}
func makeCoordinator() -> Coordinator {
return Coordinator(googlemapview: self)
}
class Coordinator : NSObject, GMSMapViewDelegate , CLLocationManagerDelegate {
var parent : MapView
init( googlemapview : MapView) {
self.parent = googlemapview
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
self.parent.currentLocation = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
self.parent.locationManager.stopUpdatingLocation()
}
func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition) {
self.parent.isLocationChanged = false
print("========= Idle")
}
func mapView(_ mapView: GMSMapView, willMove gesture: Bool) {
self.parent.isLocationChanged = true
print("================ Changed")
}
}
}
As shown every time the map view rendered it will fire the didChange events which will make the whole view body to reload even the map its self and this will cause an infinite main view reloading, how can i fix this ?
Update
I tried to use EquatableView to ignore the changes from the Map View but Im still getting the same results and the mapView will be redrawn evert time i scroll it ??!! I need to track when user scroll the map to new position
It might be due to location manager recreated, try with StateObject
struct GoogleMapsView: UIViewRepresentable {
#StateObject private var locationManager = LoccationManager() // << here !!
//...
}
Related
Overview: I'm using SwiftUI, but wanted to use UIKit-MapKit. I used UIViewRepresentable to be able to wrap the UIKit feature.
Problem: I'm learning about swiftui-uikit-interoperability and I'm getting stuck on being able to display multiple SwiftUI views.
Code Snippet:
ContentView
struct ContentView: View {
#ObservedObject var viewModel: MapView.PinViewModel
init() {
self.viewModel = MapView.PinViewModel()
}
var body: some View {
NavigationView {
MapView()
.sheet(isPresented: $viewModel.showPinForm) {
PinForm()
}
.navigationTitle("SwiftUI UIKit Interop").scaledToFill()
}
}
}
MapView
struct MapView: UIViewRepresentable {
class PinViewModel: ObservableObject {
#Published var showPinForm: Bool
init() {
self.showPinForm = false
}
func updateShowPinVar() {
self.showPinForm = true
}
}
func showPinForm() {
pinViewModel.updateShowPinVar()
}
func makeCoordinator() -> MapViewCoordinator {
let coordinator = MapViewCoordinator()
coordinator.delegate = self
return coordinator
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
let coordinate = CLLocationCoordinate2D(latitude: 40.7209, longitude: -74.0007)
let span = MKCoordinateSpan(latitudeDelta: 0.03, longitudeDelta: 0.03)
let mapRegion = MKCoordinateRegion(center: coordinate, span: span)
mapView.setRegion(mapRegion, animated: true)
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
}
In this I have a #Published var showPinForm that gets toggled in MapView. ContentView is supposed to watch this variable and when it is true it will cause the sheet to pull up. However, I believe when I enter MapView() from ContentView() then I no longer recognize ContentView.
Using the UIViewRepresentable, what is the best way to display another swiftui view? Does not have to use .sheet (Although, it would be nice)
I have tried to simplify the code to show the main problem, so I left out a lot of additional info and took out basic patterns that I used (MVVM)
Please let me know if you need any clarifications
try to follow this pattern, you can toggle the flag both inside and outside your MapView
struct MapView: UIViewRepresentable {
#Binding var switcher: Bool // -> use binding
func makeUIView(context: Context) -> MKMapView { MKMapView() }
func updateUIView(_ uiView: MKMapView, context: Context) { }
}
struct MainView: View {
#ObservedObject var viewModel = MainViewModel()
var body: some View {
MapView(switcher: $viewModel.flag)
.sheet(isPresented: $viewModel.flag) {
Text("Pin pin")
}
}
}
class MainViewModel: ObservableObject {
#Published var flag: Bool = false
}
So I am using a WKWebView within UIViewRepresentable so I can show a web view in my SwiftUI view.
For a while I could not figure out why my SwiftUI view would not update when the Coordinator would set #Publsihed properties that affect the SwiftUI view.
In the process I finally understood better how UIViewRepresentable works and realized what the problem was.
This is the UIViewRepresentable:
struct SwiftUIWebView : UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
func makeCoordinator() -> Coordinator {
Coordinator(self, viewModel: viewModel)
}
let webView = WKWebView()
func makeUIView(context: Context) -> WKWebView {
....
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
// This made my SwiftUI view update properly when the web view would report loading progress etc..
context.coordinator.viewModel = viewModel
}
}
The SwiftUI view would pass in the viewModel, then makeCoordinator would be called (only the first time at init...), then the Coordinator would be returned with that viewModel.
However, subsequently when a new viewModel was passed in on updates and not on coordinator init, the coordinator would just keep the old viewModel and things would stop working.
So I added this in the updateUIView... call, which did fix the problem:
context.coordinator.viewModel = viewModel
Question:
Is there a way to pass in the viewModel to the Coordinator during the func makeUIView(context: Context) -> WKWebView { ... } so that if a new viewModel is passed in to SwiftUIWebView the coordinator would also get the change automatically instead of me having to add:
context.coordinator.viewModel = viewModel
in updateUIView...?
EDIT:
Here is the entire code. The root content view:
struct ContentView: View {
#State var showTestModal = false
#State var redrawTest = false
var body: some View {
NavigationView {
VStack {
Button(action: {
showTestModal.toggle()
}) {
Text("Show modal")
}
if redrawTest {
Text("REDRAW")
}
}
}
.fullScreenCover(isPresented: $showTestModal) {
WebContentViewTest(redraw: $redrawTest)
}
}
}
And what the Content view presents:
struct SwiftUIProgressBar: View {
#Binding var progress: Double
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(Color.gray)
.opacity(0.3)
.frame(width: geometry.size.width, height: geometry.size.height)
Rectangle()
.foregroundColor(Color.blue)
.frame(width: geometry.size.width * CGFloat((self.progress)),
height: geometry.size.height)
.animation(.linear(duration: 0.5))
}
}
}
}
struct SwiftUIWebView : UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
func makeCoordinator() -> Coordinator {
Coordinator(self, viewModel: viewModel)
}
let webView = WKWebView()
func makeUIView(context: Context) -> WKWebView {
print("SwiftUIWebView MAKE")
if let url = URL(string: viewModel.link) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
//add your code here...
}
}
class Coordinator: NSObject {
private var viewModel: WebViewModel
var parent: SwiftUIWebView
private var estimatedProgressObserver: NSKeyValueObservation?
init(_ parent: SwiftUIWebView, viewModel: WebViewModel) {
print("Coordinator init")
self.parent = parent
self.viewModel = viewModel
super.init()
estimatedProgressObserver = self.parent.webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in
print(Float(webView.estimatedProgress))
guard let weakSelf = self else{return}
print("in progress observer: model is: \(Unmanaged.passUnretained(weakSelf.parent.viewModel).toOpaque())")
weakSelf.parent.viewModel.progress = webView.estimatedProgress
}
}
deinit {
estimatedProgressObserver = nil
}
}
class WebViewModel: ObservableObject {
#Published var progress: Double = 0.0
#Published var link : String
init (progress: Double, link : String) {
self.progress = progress
self.link = link
print("model init: \(Unmanaged.passUnretained(self).toOpaque())")
}
}
struct WebViewContainer: View {
#ObservedObject var model: WebViewModel
var body: some View {
ZStack {
SwiftUIWebView(viewModel: model)
VStack {
if model.progress >= 0.0 && model.progress < 1.0 {
SwiftUIProgressBar(progress: .constant(model.progress))
.frame(height: 15.0)
.foregroundColor(.accentColor)
}
Spacer()
}
}
}
}
struct WebContentViewTest : View {
#Binding var redraw:Bool
var body: some View {
let _ = print("WebContentViewTest body")
NavigationView {
ZStack(alignment: .topLeading) {
if redraw {
WebViewContainer(model: WebViewModel(progress: 0.0, link: "https://www.google.com"))
}
VStack {
Button(action: {
redraw.toggle()
}) {
Text("redraw")
}
Spacer()
}
}
.navigationBarTitle("Test Modal", displayMode: .inline)
}
}
}
If you run this you will see that while WebViewModel can get initialized multiple times, the coordinator will only get initialized once and the viewModel in it does not get updated. Because of that, things break after the first redraw.
I'm trying to wrap PKCanvasView as a SwiftUI view called CanvasView. I'd like to be able to toggle the whole canvas on top of another view. When the CanvasView appears, I'd like the PKToolPicker to appear. When it disappears, I'd like the PKToolPicker to disappear.
I've found a few similar approaches on here but they only involve showing the picker or toggling the picker with a button; I'd like the picker visibility to be tied to the view visibility.
In the below example you can see that you can toggle the canvas, but once the tool picker is visible, it stays visible.
Here's my CanvasView:
import SwiftUI
import PencilKit
struct CanvasView: UIViewRepresentable {
class Coordinator: NSObject, PKCanvasViewDelegate {
var canvasView: Binding<PKCanvasView>
let onChange: () -> Void
init(canvasView: Binding<PKCanvasView>, onChange: #escaping () -> Void) {
self.canvasView = canvasView
self.onChange = onChange
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
if canvasView.drawing.bounds.isEmpty == false {
onChange()
}
}
}
#Binding var canvasView: PKCanvasView
#Binding var toolPickerIsActive: Bool
private let toolPicker = PKToolPicker()
let onChange: () -> Void
func makeUIView(context: Context) -> PKCanvasView {
canvasView.backgroundColor = .clear
canvasView.isOpaque = true
canvasView.delegate = context.coordinator
showToolPicker()
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
toolPicker.setVisible(toolPickerIsActive, forFirstResponder: uiView)
}
func showToolPicker() {
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
}
func makeCoordinator() -> Coordinator {
Coordinator(canvasView: $canvasView, onChange: onChange)
}
}
And an example ContentView:
struct ContentView: View {
#State private var canvasView = PKCanvasView()
#State private var toolPickerIsActive = false
#State private var canvasIsVisible = false
var body: some View {
ZStack {
if canvasIsVisible {
CanvasView(canvasView: $canvasView,
toolPickerIsActive: $toolPickerIsActive,
onChange: canvasDidChange)
.onAppear { toolPickerIsActive = true }
.onDisappear { toolPickerIsActive = false }
}
Button(action: {
canvasIsVisible.toggle()
}, label: {
Text("Toggle canvas view")
})
}
}
private func canvasDidChange() {
// Do something with updated canvas.
}
}
Any guidance would be much appreciated!
In your scenario the CanvasView is destroyed on disappear, so SwiftUI rendering engine just not update it on any state change (as it see that no needs for that).
The possible solution for this use-case is to hide picker on coordinator deinit (because it is destroyed with owner view).
Here is a demo. Tested with Xcode 12.4 / iOS 14.4
struct CanvasView: UIViewRepresentable {
class Coordinator: NSObject, PKCanvasViewDelegate {
var canvasView: Binding<PKCanvasView>
let onChange: () -> Void
private let toolPicker: PKToolPicker
deinit { // << here !!
toolPicker.setVisible(false, forFirstResponder: canvasView.wrappedValue)
toolPicker.removeObserver(canvasView.wrappedValue)
}
init(canvasView: Binding<PKCanvasView>, toolPicker: PKToolPicker, onChange: #escaping () -> Void) {
self.canvasView = canvasView
self.onChange = onChange
self.toolPicker = toolPicker
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
if canvasView.drawing.bounds.isEmpty == false {
onChange()
}
}
}
#Binding var canvasView: PKCanvasView
#Binding var toolPickerIsActive: Bool
private let toolPicker = PKToolPicker()
let onChange: () -> Void
func makeUIView(context: Context) -> PKCanvasView {
canvasView.backgroundColor = .clear
canvasView.isOpaque = true
canvasView.delegate = context.coordinator
showToolPicker()
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
toolPicker.setVisible(toolPickerIsActive, forFirstResponder: uiView)
}
func showToolPicker() {
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
}
func makeCoordinator() -> Coordinator {
Coordinator(canvasView: $canvasView, toolPicker: toolPicker, onChange: onChange)
}
}
struct ContentView: View {
#State private var canvasView = PKCanvasView()
#State private var toolPickerIsActive = false
#State private var canvasIsVisible = false
var body: some View {
ZStack {
if canvasIsVisible {
CanvasView(canvasView: $canvasView,
toolPickerIsActive: $toolPickerIsActive,
onChange: canvasDidChange)
.onAppear { toolPickerIsActive = true }
// .onDisappear { toolPickerIsActive = false }
}
Button(action: {
canvasIsVisible.toggle()
}, label: {
Text("Toggle canvas view")
})
}
}
private func canvasDidChange() {
// Do something with updated canvas.
}
}
Note: there might be redesign of ownership, so toolPicker will live only within coordinator, but it does not change idea, and is up to you.
Rather than selecting from a list, I'm trying to navigate between drawings like a book by using buttons to cycle through, but the canvas doesn't update.
I'm following the great tutorial by DevTechie at https://www.youtube.com/watch?v=amZH2i6l004&list=PLbrKvTeCrFAfoACvHOPWFmDIaKUqBZgEr&index=5
The github repo is at https://github.com/devtechie/DrawingDocuments
Here's my ContentView and my version of the DrawingWrapper. The DrawingWrapper uses a DrawingManager (SwiftUI) to pull from CoreData and the DrawingViewController to define a PKCanvas. I wasn't sure which delegate to use and really struggling understanding how to refresh the canvas.
ContentView
struct ContentView: View {
#StateObject var manager = DrawingManager()
#State var addNewShown = false
#State var pageNumber: Int = 0
#State var newVar = UUID()
var body: some View {
VStack{
Text(manager.docs[pageNumber].name!)
HStack{
Button(action:{
pageNumber -= 1
newVar = manager.docs[pageNumber].id!
//desiredDoc = manager.docs[pageNumber]
}){
Image(systemName: "chevron.left")
}
Spacer()
Button(action:{
pageNumber += 1
newVar = manager.docs[pageNumber].id!
//desiredDoc = manager.docs[pageNumber]
}){
Image(systemName: "chevron.right")
}
}
}
}
DrawingWrapper
struct DrawingWrapper: UIViewControllerRepresentable {
var manager: DrawingManager
#Binding var doc: DrawingDoc
typealias UIViewControllerType = DrawingViewController
class Coordinator: NSObject, PKCanvasViewDelegate {
var parent: DrawingWrapper
init(_ parent: DrawingWrapper){
self.parent = parent
}
func canvasViewDidFinishRendering(_ canvasView: PKCanvasView) {
if let uiDrawing = canvasView.drawing as? PKDrawing {
parent.doc.data = uiDrawing.dataRepresentation()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<DrawingWrapper>) -> DrawingWrapper.UIViewControllerType {
let viewController = DrawingViewController()
viewController.drawingData = doc.data!
viewController.drawingChanged = {data in
manager.update(data: data, for: doc.id!)
}
viewController.delegate = context.coordinator
return viewController
}
func updateUIViewController(_ uiViewController: DrawingViewController, context: UIViewControllerRepresentableContext<DrawingWrapper>) {
uiViewController.drawingData = doc.data!
}
}
Context
I have created a UIViewRepresentable to wrap a UITextField so that:
it can be set it to become the first responder when the view loads.
the next textfield can be set to become the first responder when enter is pressed
Problem
When used inside a NavigationView, unless the keyboard is dismissed from previous views, the view doesn't observe the value in their ObservedObject.
Question
Why is this happening? What can I do to fix this behaviour?
Screenshots
Keyboard from root view not dismissed:
Keyboard from root view dismissed:
Code
Here is the said UIViewRepresentable
struct SimplifiedFocusableTextField: UIViewRepresentable {
#Binding var text: String
private var isResponder: Binding<Bool>?
private var placeholder: String
private var tag: Int
public init(
_ placeholder: String = "",
text: Binding<String>,
isResponder: Binding<Bool>? = nil,
tag: Int = 0
) {
self._text = text
self.placeholder = placeholder
self.isResponder = isResponder
self.tag = tag
}
func makeUIView(context: UIViewRepresentableContext<SimplifiedFocusableTextField>) -> UITextField {
// create textfield
let textField = UITextField()
// set delegate
textField.delegate = context.coordinator
// configure textfield
textField.placeholder = placeholder
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textField.tag = self.tag
// return
return textField
}
func makeCoordinator() -> SimplifiedFocusableTextField.Coordinator {
return Coordinator(text: $text, isResponder: self.isResponder)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<SimplifiedFocusableTextField>) {
// update text
uiView.text = text
// set first responder ONCE
if self.isResponder?.wrappedValue == true && !uiView.isFirstResponder && !context.coordinator.didBecomeFirstResponder{
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
private var isResponder: Binding<Bool>?
var didBecomeFirstResponder = false
init(text: Binding<String>, isResponder: Binding<Bool>?) {
_text = text
self.isResponder = isResponder
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
func textFieldDidBeginEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder?.wrappedValue = true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder?.wrappedValue = false
}
}
}
}
And to reproduce, here is the contentView:
struct ContentView: View {
var body: some View {
return NavigationView { FieldView(tag: 0) }
}
}
and here's the view with the field and its view model
struct FieldView: View {
#ObservedObject private var viewModel = FieldViewModel()
#State private var focus = false
var tag: Int
var body: some View {
return VStack {
// listen to viewModel's value
Text(viewModel.value)
// text field
SimplifiedFocusableTextField("placeholder", text: self.$viewModel.value, isResponder: $focus, tag: self.tag)
// push to stack
NavigationLink(destination: FieldView(tag: self.tag + 1)) {
Text("Continue")
}
// dummy for tapping to dismiss keyboard
Color.green
}
.onAppear {
self.focus = true
}.dismissKeyboardOnTap()
}
}
public extension View {
func dismissKeyboardOnTap() -> some View {
modifier(DismissKeyboardOnTap())
}
}
public struct DismissKeyboardOnTap: ViewModifier {
public func body(content: Content) -> some View {
return content.gesture(tapGesture)
}
private var tapGesture: some Gesture {
TapGesture().onEnded(endEditing)
}
private func endEditing() {
UIApplication.shared.connectedScenes
.filter {$0.activationState == .foregroundActive}
.map {$0 as? UIWindowScene}
.compactMap({$0})
.first?.windows
.filter {$0.isKeyWindow}
.first?.endEditing(true)
}
}
class FieldViewModel: ObservableObject {
var subscriptions = Set<AnyCancellable>()
// diplays
#Published var value = ""
}
It looks like SwiftUI rendering engine again over-optimized...
Here is fixed part - just make destination unique forcefully using .id. Tested with Xcode 11.4 / iOS 13.4
NavigationLink(destination: FieldView(tag: self.tag + 1).id(UUID())) {
Text("Continue")
}