I am trying to get my own custom button floating over a GMSMapView. The button draws on the GMSMapView, but the button action is not triggered. The Floating button is written in SwiftUI and this is it:
struct MyLocationView: View {
#ObservedObject var viewController: ViewController
var body: some View {
ZStack {
Button(action: {
print("Hello")
self.viewController.myLocationButtonPressed = !self.viewController.myLocationButtonPressed
}) {
ZStack {
Circle()
.foregroundColor(.black)
.frame(width: 60, height: 60)
.shadow(radius: 10)
Image(systemName: viewController.myLocationButtonPressed ? "location.fill" : "location")
.foregroundColor(.blue)
}
}
}
}
}
This is my viewController
import UIKit
import SwiftUI
import GoogleMaps
class ViewController: UIViewController, ObservableObject {
#Published var myLocationButtonPressed: Bool = false
#IBOutlet var mapView: GMSMapView!
override func viewDidLoad() {
super.viewDidLoad()
// My Location Button
let myLocationView = MyLocationView(viewController: self)
let hostingController = UIHostingController(rootView: myLocationView)
hostingController.view.backgroundColor = .blue
addChild(hostingController)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
Any idea why the button action is not working here?
Related
I want to implement UIScrollView in UIKit using UIViewRepresentable in SwiftUI.
But I have a problem that UIScrollView implemented via UIViewRepresentable bridge can't perform animation with withAnimation .
To find the cause of the problem, I used a ScrollView in SwftUI as a comparison. By comparison, it is found that SwftUI ScrollView executes withAnimation normally; however, UIScrollView implemented through UIViewRepresentable bridge cannot specify animation.
May I ask if it is because of my incorrect use of UIViewRepresentable updateUIView?
I also found related articles, such as https://github.com/rcarver/swift-matched-animation It seems that the solution is mentioned, but it does not actually solve my problem.
English is not very good, I don't know if I can understand this description ðŸ˜
import SwiftUI
import UIKit
struct FriendView: View {
#State var display = false
var body: some View {
VStack {
// UIViewRepresentable + UIKit
UScrollView {
VStack {
RoundedRectangle(cornerRadius: 10)
.fill(self.display ? .red.opacity(0.3) : .blue.opacity(0.3))
.frame(width: 200, height: self.display ? 200 : 100)
.onTapGesture {
withAnimation {
self.display.toggle()
}
}
}
}
// SwiftUI
ScrollView {
VStack {
RoundedRectangle(cornerRadius: 10)
.fill(self.display ? .red.opacity(0.3) : .blue.opacity(0.3))
.frame(width: 200, height: self.display ? 200 : 100)
.onTapGesture {
withAnimation {
self.display.toggle()
}
}
}
}
}
}
}
The following is the specific implementation of UIScrollView in UIKit
struct UScrollView<Content: View>: UIViewRepresentable {
typealias UIViewType = UIScrollView
var content: () -> Content
let scrollView: UIScrollView = {
let view = UIScrollView()
return view
}()
var host: UIHostingController<AnyView> = UIHostingController(
rootView: AnyView(EmptyView())
)
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIView(context: Context) -> UIScrollView {
host.rootView = AnyView(self.content())
host.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(host.view)
scrollView.addConstraints([
host.view.topAnchor.constraint(equalTo: scrollView.topAnchor),
host.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
host.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
host.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
host.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
])
return scrollView
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
host.rootView = AnyView(self.content())
host.view.translatesAutoresizingMaskIntoConstraints = false
uiView.addSubview(host.view)
uiView.addConstraints([
host.view.topAnchor.constraint(equalTo: uiView.topAnchor),
host.view.leadingAnchor.constraint(equalTo: uiView.leadingAnchor),
host.view.trailingAnchor.constraint(equalTo: uiView.trailingAnchor),
host.view.bottomAnchor.constraint(equalTo: uiView.bottomAnchor),
host.view.widthAnchor.constraint(equalTo: uiView.widthAnchor)
])
}
}
Comparing the execution effect, 1 withAnimation is invalid, 2 withAnimation is valid
I'm reposting my question of yesterday and now adding a clean code example to demonstrate the problem
I have a MyCustomMapView, embedding a MKMApView and it starts at a fixed location. I have a function called gotoCoordinate, which accepts a coordinate and then navigates the mapview's center to that coordinate.
In the sample code that can be simulated by clicking on the red button labelleing "Click here to change map position".
This all works great. Until....
in the app I'm working on I also need to have a user location so I have a LocationViewModel handling the request. Once you have given request to access your location, click the button no longer moves the center of the map to that new coordinate.
Once you comment the #StateObject var locationViewModel = LocationViewModel() it is working again.
So it seems that once you are using a location manager with a delegate the map no longer moves when changing it's region
Is this a bug or am I doing something wrong?
import SwiftUI
struct ContentView: View {
#StateObject var locationViewModel = LocationViewModel()
var body: some View {
switch locationViewModel.authorizationStatus {
case .notDetermined:
AnyView(RequestLocationView())
.environmentObject(locationViewModel)
case .restricted:
ErrorView(errorText: "Location use is restricted.")
case .denied:
ErrorView(errorText: "The app does not have location permissions. Please enable them in settings.")
default:
EmptyView()
}
GeometryReader { geometry in
DisplayMapView(size:geometry.size)
}
}
}
import SwiftUI
import CoreLocation
import MapKit
struct MyCustomMapView: UIViewRepresentable {
var map = MKMapView() // << constructor contract !!
let coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:31,longitude: -86 )
func makeUIView(context: Context) -> MKMapView {
map.delegate = context.coordinator
map.showsUserLocation = true
map.showsCompass = true
let region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: coordinate.latitude,longitude: coordinate.longitude),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
map.setRegion(region, animated: true)
return map
}
func gotoCoordinate(_ newCoordinate: CLLocationCoordinate2D ){
let region = MKCoordinateRegion(center: newCoordinate, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
map.setRegion(region, animated: true)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
func makeCoordinator() -> MyCustomMapView.Coordinator {
return MyCustomMapView.Coordinator(parent1: self)
}
final class Coordinator: NSObject, MKMapViewDelegate {
var parent:MyCustomMapView
init(parent1:MyCustomMapView){
parent = parent1
}
}//class Coordinator
}
import SwiftUI
import CoreLocation
import MapKit
struct DisplayMapView: View {
#Environment(\.presentationMode) var presentationMode
var size: CGSize
var startCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
var map = MyCustomMapView()
var body: some View {
ZStack(alignment:.top){
map
VStack(alignment:.leading){
HStack {
HStack {
Text("Click here to change map position")
.onTapGesture(){
map.gotoCoordinate(startCoordinate)
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.black)
.background(Color(.red))
.cornerRadius(10.0)
}
}.padding(.top,50).padding(.leading,20).padding(.trailing,20)
}.ignoresSafeArea()
}
}
import Foundation
import SwiftUI
import CoreLocation
class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var authorizationStatus: CLAuthorizationStatus
#Published var lastSeenLocation: CLLocation?
#Published var currentPlacemark: CLPlacemark?
private let locationManager: CLLocationManager
static let shared = LocationViewModel()
override init() {
locationManager = CLLocationManager()
authorizationStatus = locationManager.authorizationStatus
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 0.4
locationManager.startUpdatingLocation()
}
func requestPermission() {
locationManager.requestWhenInUseAuthorization()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus = manager.authorizationStatus
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
lastSeenLocation = locations.first
}
}
struct RequestLocationView: View {
#EnvironmentObject var locationViewModel: LocationViewModel
var body: some View {
VStack(spacing:50) {
Image(systemName: "location.circle")
.resizable()
.frame(width: 100, height: 100, alignment: .center)
.foregroundColor(Color.init(red: 0.258, green: 0.442, blue: 0.254))
Button(action: {
locationViewModel.requestPermission()
}, label: {
Label(LocalizedStringKey("allowLocationAccess"), systemImage: "location")
})
.padding(10)
.foregroundColor(.white)
.background(.green)
.clipShape(RoundedRectangle(cornerRadius: 8))
Text("We need your permission to give you the best experience.")
.foregroundColor(.gray)
.font(.caption)
}
}
}
struct ErrorView: View {
var errorText: String
var body: some View {
VStack {
Image(systemName: "xmark.octagon")
.resizable()
.frame(width: 100, height: 100, alignment: .center)
Text(errorText)
}
.padding()
.foregroundColor(.white)
.background(Color.red)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Declare your coordinates as a stateful variable, either as #State or as #Published within an observable object:
struct DisplayMapView: View {
#State var coordinates = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
Then pass the coordinates in as an argument to your view - no need to store your view as a variable:
ZStack(alignment: .top) {
MyMapView(coordinates: coordinates)
VStack(alignment: .leading) {
// etc.
Then you’ll need to do some rejigging in your UIViewRepresentable. You mustn't retain map as a separate instance outside makeUIView and updateUIView - SwiftUI structs can be recreated at will, so that would release your MKMapView instance and create a new one. Instead, the object returned by makeUIView is retained for you by the system. You do need to declare a variable that will accept the coordinates argument above, and then respond to any changes in it in updateUIView.
struct MyMapView: UIViewRepresentable {
var coordinates: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
// etc.
return map
}
func updateUIView(_ uiView: MKMapView, context: Coordinator) {
let region = MKCoordinateRegion(center: coordinates, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
uiView.setRegion(region, animated: true)
}
}
Now, when the user taps, instead of calling a function inside your view, you update the DisplayMapView’s coordinates variable and the UIViewRepresentable’s update logic should redraw the map in the correct position.
I'm trying to show a polygon overlay on the map but I don't find what I'm doing wrong
my MapView file is:
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
#EnvironmentObject var vmHome: HomeViewModel
#State var restrictions: [MKOverlay] = []
func makeCoordinator() -> Coordinator {
return MapView.Coordinator()
}
func makeUIView(context: Context) -> MKMapView {
let view = vmHome.mapView
view.showsUserLocation = true
view.delegate = context.coordinator
vmHome.showRestrictedZones { (restrictions) in
self.restrictions = restrictions
print("dentro mapview \(restrictions)")
view.addOverlays(self.restrictions)
}
return view
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
class Coordinator: NSObject,MKMapViewDelegate{
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation.isKind(of: MKUserLocation.self){return nil}
else{
let pinAnnotation = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "PIN_VIEW")
pinAnnotation.tintColor = .red
pinAnnotation.animatesDrop = true
pinAnnotation.canShowCallout = true
return pinAnnotation
}
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let polygon = overlay as? MKPolygon {
let renderer = MKPolygonRenderer(polygon: polygon)
renderer.fillColor = UIColor.purple.withAlphaComponent(0.2)
renderer.strokeColor = .purple.withAlphaComponent(0.7)
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}
}
}
then the view model where I want to convert a fixed array of locations in polygon and add them to MKOverlay array (I cut out some come from the view model that is not related to overlay)
import Foundation
import MapKit
import CoreLocation
class HomeViewModel: NSObject, ObservableObject, CLLocationManagerDelegate{
#Published var mapView = MKMapView()
var overlays: [MKOverlay] = []
func showRestrictedZones(completion: #escaping ([MKOverlay]) -> ()) {
let locations = [CLLocation(latitude: 11.3844028, longitude: 45.6174815), CLLocation(latitude: 11.5608707,longitude: 45.3305094), CLLocation(latitude: 11.8533817, longitude: 45.4447992), CLLocation(latitude: 11.8382755, longitude: 45.6314077), CLLocation(latitude: 11.6624943, longitude: 45.6942722), CLLocation(latitude: 11.3844028, longitude: 45.6174815)]
var coordinates = locations.map({(location: CLLocation) -> CLLocationCoordinate2D in return location.coordinate})
let polygon = MKPolygon(coordinates: &coordinates, count: locations.count)
print(locations.count)
overlays.append(polygon)
print(overlays)
DispatchQueue.main.async {
completion(self.overlays)
}
}
}
ad finally the home view
import SwiftUI
import CoreLocation
struct Home: View {
#EnvironmentObject var vmHome: HomeViewModel
#State var locationManager = CLLocationManager()
var body: some View {
VStack{
HStack{
Text("Hi,")
.font(.title)
.foregroundColor(.theme.primary)
.padding(.horizontal)
Spacer()
VStack(alignment: .trailing) {
HStack {
Image(systemName: "mappin.and.ellipse")
.font(.largeTitle)
.foregroundColor(.blue)
Text("O1")
.font(.title)
.foregroundColor(.theme.primary)
}
Text(vmHome.currentAddress)
.font(.callout)
.foregroundColor(.theme.primary)
}
.padding(.horizontal)
}
ZStack(alignment: .bottom) {
MapView()
.environmentObject(vmHome)
.ignoresSafeArea(.all, edges: .bottom)
//VStack{
Button(action: vmHome.focusLocation, label: {
Image(systemName: "location.fill")
.font(.title2)
.padding(10)
.background(Color.primary)
.clipShape(Circle())
})
.frame(maxWidth: .infinity, alignment: .trailing)
.padding()
.padding(.bottom)
//}
}
}
.background(Color.theme.backgroud)
.onAppear {
locationManager.delegate = vmHome
locationManager.requestWhenInUseAuthorization()
}
.alert(isPresented: $vmHome.permissionDenied, content: {
Alert(title: Text("Permission Denied"), message: Text("Please Enable Permission In App Settings"), dismissButton: .default(Text("Goto Settings"), action: {
// Redireting User To Settings...
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
}))
})
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
.environmentObject(HomeViewModel())
}
}
when I debug the array of MKOverlay I have a value like this [<MKPolygon: 0x282200f30>]
so I suppose that inside there's something
thanks
I recommend watching WWDC 2019 Integrating SwiftUI to learn the correct design, from 12:41.
Notice their UIViewRepresentable struct has a #Binding var, which in your case should be the array of polygons or overlays. updateView is called when that value changes and that is where you need to update the MKMapView with the differences in the array from last time. Also you should create the MKMapView in makeUIView do not fetch one from somewhere else.
I would also suggest removing the view model objects and instead learning SwiftUI View structs and property wrappers (which make the efficient structs have view model object semantics). WWDC 2019 Data Flow Through SwiftUI is a great starting point. You'll notice they never use objects for view data.
I have a Storyboard UIKIT function getAcctBalance(). In my UIKIT file I display a UIHostingController to load a SWIFTUI view. In the SWIFTUI view I want to call my function getAcctBalance().
SWIFTUI:
import SwiftUI
import SWXMLHash
struct UserDashboard: View {
#ObservedObject var userModel: UserDashboardModel
Button(action: {
UserDashboardHostVC.getAcctBalance() //This function call is not working
}){
Text("Load Balance")
}
Text(userModel.totalBalance) //by default it is 0.0 and when button is pressed it should return 599
}
UIKIT:
import UIKit
class UserDashboardModel: ObservableObject {
#Published var totalBalance = 0.0
}
class UserDashboardHostVC: UIViewController {
var userModel = UserDashboardModel()
override func viewDidLoad() {
super.viewDidLoad()
let controller = UIHostingController(rootView: UserDashboard(userModel: self.userModel))
controller.view.translatesAutoresizingMaskIntoConstraints = false
self.addChild(controller)
self.view.addSubview(controller.view)
controller.didMove(toParent: self)
}
func getAcctBalance {
userModel.totalBalance = 599
}
Here is a sample
class UserDashboardModel: ObservableObject {
#Published var totalBalance = 0.0
internal var uIViewController: UserDashboardHostVC? = nil
func getAcctBalance(){
if uIViewController != nil{
uIViewController!.getAcctBalance()
}else{
print("view controller is not connected")
}
}
}
struct UserDashboard: View {
#ObservedObject var userModel: UserDashboardModel
var body: some View{
VStack{
Button(action: {
userModel.getAcctBalance() //Call shared model
}){
Text("Load Balance")
}
Text(userModel.totalBalance.description) //Now it will change
}.frame(width: 400, height: 400, alignment: .center)
}
}
class UserDashboardHostVC: UIViewController {
var userModel = UserDashboardModel()
override func viewDidLoad() {
super.viewDidLoad()
//Crucial connection
userModel.uIViewController = self
let controller = UIHostingController(rootView: UserDashboard(userModel: self.userModel))
self.addChild(controller)
self.view.addSubview(controller.view)
controller.view.translatesAutoresizingMaskIntoConstraints = false
//Update constraints per use case
controller.view.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
controller.view.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
controller.didMove(toParent: self)
}
func getAcctBalance() {
userModel.totalBalance = 599
}
}
I have new SwiftUI .keyboard toolbar added. And it works great with Swiftui TextFields. But I consider if it is possible and how can it be done to use this toolbar also with UITextFields wrapped in UIViewRepresentable. I don’t know if I am doing something wrong or this isn’t supported.
I had the same problem and couldn't find an answer, so I tried to recreate this keyboard toolbar in SwiftUI. Here is the code:
struct SomeView: View {
#State var text = ""
#State var focusedUITextField = false
var body: some View {
NavigationStack {
ZStack {
VStack {
Button("Remove UITextField focus") {
focusedUITextField = false
}
TextField("SwiftUI", text: $text)
CustomTextField(hint: "UIKit", text: $text, focused: $focusedUITextField)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal)
if focusedUITextField {
VStack(spacing: 0) {
Spacer()
Divider()
keyboardToolbarContent
.frame(maxWidth: .infinity, maxHeight: 44)
.background(Color(UIColor.secondarySystemBackground))
}
}
}
/*
.toolbar {
ToolbarItem(placement: .keyboard) {
keyboardToolbarContent
}
}
*/
}
}
var keyboardToolbarContent: some View {
HStack {
Rectangle()
.foregroundColor(.red)
.frame(width: 50, height: 40)
Text("SwiftUI stuff")
}
}
}
And for the custom UITextField:
struct CustomTextField: UIViewRepresentable {
let hint: String
#Binding var text: String
#Binding var focused: Bool
func makeUIView(context: Context) -> UITextField {
let uiTextField = UITextField()
uiTextField.delegate = context.coordinator
uiTextField.placeholder = hint
return uiTextField
}
func updateUIView(_ uiTextField: UITextField, context: Context) {
uiTextField.text = text
uiTextField.placeholder = hint
if focused {
uiTextField.becomeFirstResponder()
} else {
uiTextField.resignFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
let parent: CustomTextField
init(parent: CustomTextField) {
self.parent = parent
}
func textFieldDidBeginEditing(_ textField: UITextField) {
parent.focused = true
}
func textFieldDidEndEditing(_ textField: UITextField) {
parent.focused = false
}
}
}
Unfortunately the animation of the custom toolbar and the keyboard don't match perfectly. I would challenge the guy in the comments from this post How to add a keyboard toolbar in SwiftUI that remains even when keyboard not visible but sadly I can't comment.
Also the background color doesn't match the native toolbar exactly, I don't know which color is used there.