Unexpected visual behavior of SwiftUI Context Menu with UIViewRepresentable - swiftui

UIViewRepresentable:
import SwiftUI
import MapKit
import CoreData
struct CustomView: UIViewRepresentable {
var coordinate: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
let region = MKCoordinateRegion(center: coordinate, span: span)
uiView.setRegion(region, animated: true)
}
}
ContentView:
import SwiftUI
import CoreLocation
struct ContentView: View {
var body: some View {
GeometryReader { geo in
CustomView(coordinate: CLLocationCoordinate2D(latitude: 37.33182, longitude: -122.03118))
.frame(width: geo.size.width, height: 300)
.cornerRadius(25)
.contextMenu(/*#START_MENU_TOKEN#*/ContextMenu(menuItems: {
Text("Menu Item 1")
Text("Menu Item 2")
Text("Menu Item 3")
})/*#END_MENU_TOKEN#*/)
}
.padding(.horizontal)
.frame(height: 300)
}
}
The above code works fine in the SwiftUI Canvas and the Simulator, however on my physical testing device (an iPhone 7 - iOS 14 Beta 5), when I long press the CustomView, it becomes black. The app also sometimes crashes with the following error which may be related:
CGImageCreate: invalid image alphaInfo: kCGImageAlphaNone. It should be kCGImageAlphaNoneSkipLast
If I replace the CustomView with an Image like below, everything works as expected:
import SwiftUI
struct ContentView: View {
var body: some View {
Image("imageName")
.resizable()
.frame(width: 350, height: 300)
.cornerRadius(25)
.contextMenu(/*#START_MENU_TOKEN#*/ContextMenu(menuItems: {
Text("Menu Item 1")
Text("Menu Item 2")
Text("Menu Item 3")
})/*#END_MENU_TOKEN#*/)
}
}
How can I fix it? Thanks!

Related

SwiftUI UIViewRepresentable withAnimation invalid

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

swiftUI Mapkit and Corelocation causing problems

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.

swiftui mapkit polygon overlay

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.

SwiftUI NavigationView and NavigationLink changes layout of custom view

I have a DetailView() that displays an image, text, and then a map of the location of the displayed image. In my ContentView() I have a NavigationView and NavigationLink to go from the main view to my custom view. Everything works fine, except that the alignment of my DetailView() is not aligned properly as when I view the preview for DetailView(). The text description is showing well below the picture. I have been pulling my hair out for 2 days trying to figure this out, but haven't so far.
Picture of ContentView()
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailView(picture: "dunnottar-castle")) {
Text("Hello, World!")
Image(systemName: "sun.min.fill")
} .buttonStyle(PlainButtonStyle())
.navigationBarHidden(true)
}
}
}
=================== My DetailView()
struct MapView: UIViewRepresentable {
// 1.
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MKMapView {
MKMapView(frame: .zero)
}
// 2.
func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MapView>) {
// 3.
let location = CLLocationCoordinate2D(latitude: 30.478340,
longitude: -90.037687)
// 4.
let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
let region = MKCoordinateRegion(center: location, span: span)
uiView.setRegion(region, animated: true)
// 5.
let annotation = MKPointAnnotation()
annotation.coordinate = location
annotation.title = "Abita Springs"
annotation.subtitle = "Louisiana"
uiView.addAnnotation(annotation)
}
}
struct DetailView: View {
let picture: String
var body: some View {
VStack(spacing: -50.0){
// Picture and Title
ZStack (alignment: .bottom) {
//Image
Image(picture)
.resizable()
.aspectRatio(contentMode: .fit)
Rectangle()
.frame(height: 80)
.opacity(0.25)
.blur(radius: 10)
HStack {
VStack(alignment: .leading, spacing: 8.0) {
Text("EDINBURGH")
.foregroundColor(.yellow)
.font(.largeTitle)
}
.padding(.leading)
.padding(.bottom)
Spacer()
}
}.edgesIgnoringSafeArea(.top)
VStack{
// Description
Text("Edinburgh is Scotland's compact, hilly capital. It has a medieval Old Town and elegant Georgian New Town with gardens and neoclassical buildings. Looming over the city is Edinburgh Castle, home to Scotland’s crown jewels and the Stone of Destiny, used in the coronation of Scottish rulers. Arthur’s Seat is an imposing peak in Holyrood Park with sweeping views, and Calton Hill is topped with monuments and memorials.")
.font(.body)
.lineLimit(9)
.lineSpacing(5.0)
.padding()
// .frame(maxHeight: 310)
}
Spacer()
// Map of location
VStack {
MapView()
.edgesIgnoringSafeArea(.all)
.padding(.top)
.frame(maxHeight: 310)
// Image(systemName: "person")
.padding(.top)
}
}
}
}
I changed
NavigationLink(destination: DetailView(picture: "dunnottar-castle")) {
to
NavigationLink(destination: DetailView(picture: "dunnottar castle").edgesIgnoringSafeArea(.all)) {
and it works like I want it to now.
If you just want to have the normal layout, you can also use:
NavigationLink(destination: DetailView(picture: "dunnottar castle").navigationBarHidden(true))

SwiftUI Button action not working over GMSMapView

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?