How to change UI state when user touch GoogleMaps' Marker in SwiftUI? - delegates

This question is about SwiftUI.
I'm trying to show a map and allow the user to touch any marker available. When it happens, I wish to change a text on my view, reflecting that user's action.
After a lot of search, I think the solution can be somewhere near Observable protocol, but I just can't figure out the right way for doing that. Here's my code:
struct Home: View {
// Here's the attribute I want to be changed when user touches the marker
var selectedMarker: GMSMarker?
var body: some View {
VStack(spacing: 0) {
// Condition to be applied when user touches the marker
if (selectedMarker == nil){
Text("No marker selected").padding()
}else{
Text("Now, there's a marker selected").padding()
}
GoogleMapsHome()
}
.navigationBarBackButtonHidden(true)
.navigationBarTitle(Text("Marker question"), displayMode: .inline)
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
}
}
Here's the GoogleMaps definition:
struct GoogleMapsHome: UIViewRepresentable {
private let zoom: Float = 18
// Just for didactic purposes. Later, I'm going to use LocationManager
let lat: Double = -15.6692660716233
let lng: Double = -47.83980712156295
func makeUIView(context: Self.Context) -> GMSMapView {
let camera = GMSCameraPosition.camera(
withLatitude: lat,
longitude: lng,
zoom: zoom)
let mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
mapView.mapType = .hybrid
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
mapView.animate(toLocation: CLLocationCoordinate2D(latitude: lat, longitude: lng))
let position = CLLocationCoordinate2D(latitude: lat, longitude: lng)
let marker = GMSMarker(position: position)
marker.title = "You"
marker.map = mapView
}
func makeCoordinator() -> Coordinator {
Coordinator(owner: self)
}
class Coordinator: NSObject, GMSMapViewDelegate, ObservableObject {
let owner: GoogleMapsHome // access to owner view members,
init(owner: GoogleMapsHome) {
self.owner = owner
}
#Published var selectedMarker: GMSMarker? {
willSet { objectWillChange.send() }
}
func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
print("A marker has been touched by the user")
self.selectedMarker = marker
return true
}
}
}
I hope someone can help me and, later, this question become useful for anyone with the same need.
Best regards!

After a while, I found a way to solve it.
The keywords for that are "Coordinator" and "Binding".
Of course, I'm not sure if this is the right way, or the best, but it worked, at least.
import Foundation
import SwiftUI
import GoogleMaps
struct Home: View {
#State var selectedMarker: GMSMarker?
var body: some View {
VStack(spacing: 0) {
if (selectedMarker == nil){
Text("No marker selected").padding()
}else{
Text("There's a marker selected").padding()
}
GoogleMapsHome(selectedMarker: self.$selectedMarker)
}
.navigationBarBackButtonHidden(true)
.navigationBarTitle(Text("Map Test"), displayMode: .inline)
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
}
}
struct GoogleMapsHome: UIViewRepresentable {
private let zoom: Float = 18
let lat: Double = -15.6692660716233
let lng: Double = -47.83980712156295
#Binding var selectedMarker: GMSMarker?
func makeCoordinator() -> Coordinator {
return Coordinator(
owner: self,
selectedMarker: $selectedMarker)
}
func makeUIView(context: Self.Context) -> GMSMapView {
let camera = GMSCameraPosition.camera(
withLatitude: lat,
longitude: lng,
zoom: zoom)
let mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
mapView.mapType = .hybrid
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
mapView.animate(toLocation: CLLocationCoordinate2D(latitude: lat, longitude: lng))
let position = CLLocationCoordinate2D(latitude: lat, longitude: lng)
let marker = GMSMarker(position: position)
marker.title = "You"
marker.map = mapView
}
class Coordinator: NSObject, GMSMapViewDelegate, ObservableObject {
let owner: GoogleMapsHome // access to owner view members,
#Binding var selectedMarker: GMSMarker?
init(
owner: GoogleMapsHome,
selectedMarker: Binding<GMSMarker?>
) {
self.owner = owner
_selectedMarker = selectedMarker
}
func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
print("A marker has been touched")
self.selectedMarker = marker
return true
}
}
}

Related

How to optimize SwiftUI Map drag performance when many annotaitons showing under macOS?

This is really a curious problem for SwiftUI Map under macOS.
The code is definitely the same both on macOS & iOS/iPadOS, if we add tens of annotations on the map, then drag around, it's very smooth on the iOS/iPadOS, but feeling stuck, stuck and stuck on the macOS.
Is there any special coding thing we forget to setting on macOS for SwiftUI map? It's really confusion for the same code but different performance consequences...
Here is the full project files you may download from OneDrive:
https://1drv.ms/u/s!Ank9gPMc1GKYk_UlZ2V3BC_BArMofQ?e=Nsco9k
Below is the code you can check to:
import SwiftUI
import MapKit
struct ContentView: View {
#State var annotationTrackList = [TyphoonMKAnnotation]()
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 31.7, longitude: 118.4),
latitudinalMeters: 100_000,
longitudinalMeters: 100_000)
var body: some View {
ZStack {
Map(coordinateRegion: $region, annotationItems: annotationTrackList) { item in
MapAnnotation(coordinate: item.coordinate) {
ZStack(alignment: .center) {
//color
Image(systemName: "bubble.middle.bottom.fill")
.resizable()
.frame(width: 32, height: 32)
.foregroundColor(item.color)
.aspectRatio(contentMode: .fit)
//icon
Image(systemName: "tornado")
.resizable()
.frame(width: 16, height: 16)
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
}
//location title
Text(item.title ?? "")
.font(Font.system(size: 11, weight: .semibold))
.foregroundColor(.white)
}
}
}
.ignoresSafeArea()
.onAppear {
initialValues()
}
}
private func initialValues() {
var annotationList = [TyphoonMKAnnotation]()
let jsonData = TyphoonJsonData()
//forecast
let forecastJson = jsonData.forecast()
guard
let forecastList = forecastJson["forecast"].array
else { return }
for item in forecastList {
let type = item["type"].stringValue
let latitude = item["lat"].stringValue
let longitude = item["lon"].stringValue
let coordinate = CLLocationCoordinate2D(latitude: Double(latitude)!, longitude: Double(longitude)!)
let annotation = TyphoonMKAnnotation(coordinate: coordinate)
annotation.title = type
annotation.color = .pink
annotationList.append(annotation)
}
//history
let trackJson = jsonData.track()
guard
let trackList = trackJson["track"].array
else { return }
for item in trackList {
let type = item["type"].stringValue
let latitude = item["lat"].stringValue
let longitude = item["lon"].stringValue
let coordinate = CLLocationCoordinate2D(latitude: Double(latitude)!, longitude: Double(longitude)!)
let annotation = TyphoonMKAnnotation(coordinate: coordinate)
annotation.title = type
annotation.color = .green
annotationList.append(annotation)
}
self.annotationTrackList = annotationList
}
}
class TyphoonMKAnnotation: Identifiable {
let id = UUID()
var coordinate: CLLocationCoordinate2D
var title: String?
var subtitle: String?
var index: Int?
var color: Color?
init(coordinate: CLLocationCoordinate2D) {
self.coordinate = coordinate
}
}

SwiftUI / imagepicker / firebasefirestore - image uploads properly but does not refresh when leaving image picker view

I have this settingview where the user can update their profile picture, which gets uploaded in Firebase Storage, and gets loaded as WebImage(url:ImageURL). It works, however when the user changes the profile image, it does not get immediately refreshed. They have to leave the screen and come back, then the loadImageFromFirebase function gets called and the new image is displayed properly.
I would like the new image to show as soon as it is selected and uploaded to Firebase Storage.
I have tried SDImageCache.shared.clearMemory() but that didn't really help (perhaps I didn't do it in the right place?)
Here is my code:
import SwiftUI
import FirebaseFirestore
import Firebase
import SDWebImageSwiftUI
struct SettingsView: View {
#ObservedObject var settingsViewModel = SettingsViewModel()
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State private var presented = false
#State private var isShowPhotoLibrary = false
#State private var image = UIImage()
#State private var imageURL = URL(string: "")
var body: some View {
NavigationView {
VStack {
WebImage(url: imageURL)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 250.0, height: 250.0, alignment: .center)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
.scaledToFit()
Text("Project")
.font(.title)
.fontWeight(.semibold)
Form {
Section {
HStack {
Image(systemName: "checkmark.circle")
Text("Upload profile image")
Spacer()
Text("Plain")
.onTapGesture {
self.isShowPhotoLibrary = true
}
.sheet(isPresented: $isShowPhotoLibrary) {
ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
}
}
}
AccountSection(settingsViewModel: self.settingsViewModel)
}
.navigationBarTitle("Settings", displayMode: .inline)
.navigationBarItems(trailing:
Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
Text("Done")
})
}
.onAppear(perform: loadImageFromFirebase)
}
}
func loadImageFromFirebase() {
let storage = Storage.storage()
guard let userid = Auth.auth().currentUser?.uid else { return }
let storageRef = storage.reference().child(userid+"/profilephoto"+"/profile.jpeg")
storageRef.downloadURL {
(url, error) in
if error != nil {
print((error?.localizedDescription)!)
return
}
self.imageURL = url!
}
}
}
and the image picker:
import UIKit
import SwiftUI
import SDWebImageSwiftUI
import Firebase
struct ImagePicker: UIViewControllerRepresentable {
var sourceType: UIImagePickerController.SourceType = .photoLibrary
#Binding var selectedImage: UIImage
#Environment(\.presentationMode) private var presentationMode
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.allowsEditing = false
imagePicker.sourceType = sourceType
imagePicker.delegate = context.coordinator
return imagePicker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
parent.selectedImage = image
uploadprofileImage(image: image)
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}

How to auto-expand height of NSTextView in SwiftUI?

How do I properly implement NSView constraints on the NSTextView below so it interacts with SwiftUI .frame()?
Goal
An NSTextView that, upon new lines, expands its frame vertically to force a SwiftUI parent view to render again (i.e., expand a background panel that's under the text + push down other content in VStack). The parent view is already wrapped in a ScrollView. Since the SwiftUI TextEditor is ugly and under-featured, I'm guessing several others new to MacOS will wonder how to do the same.
Update
#Asperi pointed out a sample for UIKit buried in another thread. I tried adapting that for AppKit, but there's some loop in the async recalculateHeight function. I'll look more at it with coffee tomorrow. Thanks Asperi. (Whoever you are, you are the SwiftUI SO daddy.)
Problem
The NSTextView implementation below edits merrily, but disobeys SwiftUI's vertical frame. Horizontally all is obeyed, but texts just continues down past the vertical height limit. Except, when switching focus away, the editor crops that extra text... until editing begins again.
What I've Tried
Sooo many posts as models. Below are a few. My shortfall I think is misunderstanding how to set constraints, how to use NSTextView objects, and perhaps overthinking things.
I've tried implementing an NSTextContainer, NSLayoutManager, and NSTextStorage stack together in the code below, but no progress.
I've played with GeometryReader inputs, no dice.
I've printed LayoutManager and TextContainer variables on textdidChange(), but am not seeing dimensions change upon new lines. Also tried listening for .boundsDidChangeNotification / .frameDidChangeNotification.
GitHub: unnamedd MacEditorTextView.swift <- Removed its ScrollView, but couldn't get text constraints right after doing so
SO: Multiline editable text field in SwiftUI <- Helped me understand how to wrap, removed the ScrollView
SO: Using a calculation by layoutManager <- My implementation didn't work
Reddit: Wrap NSTextView in SwiftUI <- Tips seem spot on, but lack AppKit knowledge to follow
SO: Autogrow height with intrinsicContentSize <- My implementation didn't work
SO: Changing a ScrollView <- Couldn't figure out how to extrapolate
SO: Cocoa tutorial on setting up an NSTextView
Apple NSTextContainer Class
Apple Tracking the Size of a Text View
ContentView.swift
import SwiftUI
import Combine
struct ContentView: View {
#State var text = NSAttributedString(string: "Testing.... testing...")
let nsFont: NSFont = .systemFont(ofSize: 20)
var body: some View {
// ScrollView would go here
VStack(alignment: .center) {
GeometryReader { geometry in
NSTextEditor(text: $text.didSet { text in react(to: text) },
nsFont: nsFont,
geometry: geometry)
.frame(width: 500, // Wraps to width
height: 300) // Disregards this during editing
.background(background)
}
Text("Editing text above should push this down.")
}
}
var background: some View {
...
}
// Seeing how updates come back; I prefer setting them on textDidEndEditing to work with a database
func react(to text: NSAttributedString) {
print(#file, #line, #function, text)
}
}
// Listening device into #State
extension Binding {
func didSet(_ then: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
then($0)
self.wrappedValue = $0
}
)
}
}
NSTextEditor.swift
import SwiftUI
struct NSTextEditor: View, NSViewRepresentable {
typealias Coordinator = NSTextEditorCoordinator
typealias NSViewType = NSTextView
#Binding var text: NSAttributedString
let nsFont: NSFont
var geometry: GeometryProxy
func makeNSView(context: NSViewRepresentableContext<NSTextEditor>) -> NSTextEditor.NSViewType {
return context.coordinator.textView
}
func updateNSView(_ nsView: NSTextView, context: NSViewRepresentableContext<NSTextEditor>) { }
func makeCoordinator() -> NSTextEditorCoordinator {
let coordinator = NSTextEditorCoordinator(binding: $text,
nsFont: nsFont,
proxy: geometry)
return coordinator
}
}
class NSTextEditorCoordinator : NSObject, NSTextViewDelegate {
let textView: NSTextView
var font: NSFont
var geometry: GeometryProxy
#Binding var text: NSAttributedString
init(binding: Binding<NSAttributedString>,
nsFont: NSFont,
proxy: GeometryProxy) {
_text = binding
font = nsFont
geometry = proxy
textView = NSTextView(frame: .zero)
textView.autoresizingMask = [.height, .width]
textView.textColor = NSColor.textColor
textView.drawsBackground = false
textView.allowsUndo = true
textView.isAutomaticLinkDetectionEnabled = true
textView.displaysLinkToolTips = true
textView.isAutomaticDataDetectionEnabled = true
textView.isAutomaticTextReplacementEnabled = true
textView.isAutomaticDashSubstitutionEnabled = true
textView.isAutomaticSpellingCorrectionEnabled = true
textView.isAutomaticQuoteSubstitutionEnabled = true
textView.isAutomaticTextCompletionEnabled = true
textView.isContinuousSpellCheckingEnabled = true
textView.usesAdaptiveColorMappingForDarkAppearance = true
// textView.importsGraphics = true // 100% size, layoutManger scale didn't fix
// textView.allowsImageEditing = true // NSFileWrapper error
// textView.isIncrementalSearchingEnabled = true
// textView.usesFindBar = true
// textView.isSelectable = true
// textView.usesInspectorBar = true
// Context Menu show styles crashes
super.init()
textView.textStorage?.setAttributedString($text.wrappedValue)
textView.delegate = self
}
// Calls on every character stroke
func textDidChange(_ notification: Notification) {
switch notification.name {
case NSText.boundsDidChangeNotification:
print("bounds did change")
case NSText.frameDidChangeNotification:
print("frame did change")
case NSTextView.frameDidChangeNotification:
print("FRAME DID CHANGE")
case NSTextView.boundsDidChangeNotification:
print("BOUNDS DID CHANGE")
default:
return
}
// guard notification.name == NSText.didChangeNotification,
// let update = (notification.object as? NSTextView)?.textStorage else { return }
// text = update
}
// Calls only after focus change
func textDidEndEditing(_ notification: Notification) {
guard notification.name == NSText.didEndEditingNotification,
let update = (notification.object as? NSTextView)?.textStorage else { return }
text = update
}
}
Quick Asperi's answer from a UIKit thread
Crash
*** Assertion failure in -[NSCGSWindow setSize:], NSCGSWindow.m:1458
[General] Invalid parameter not satisfying:
size.width >= 0.0
&& size.width < (CGFloat)INT_MAX - (CGFloat)INT_MIN
&& size.height >= 0.0
&& size.height < (CGFloat)INT_MAX - (CGFloat)INT_MIN
import SwiftUI
struct AsperiMultiLineTextField: View {
private var placeholder: String
private var onCommit: (() -> Void)?
#Binding private var text: NSAttributedString
private var internalText: Binding<NSAttributedString> {
Binding<NSAttributedString>(get: { self.text } ) {
self.text = $0
self.showingPlaceholder = $0.string.isEmpty
}
}
#State private var dynamicHeight: CGFloat = 100
#State private var showingPlaceholder = false
init (_ placeholder: String = "", text: Binding<NSAttributedString>, onCommit: (() -> Void)? = nil) {
self.placeholder = placeholder
self.onCommit = onCommit
self._text = text
self._showingPlaceholder = State<Bool>(initialValue: self.text.string.isEmpty)
}
var body: some View {
NSTextViewWrapper(text: self.internalText, calculatedHeight: $dynamicHeight, onDone: onCommit)
.frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
.background(placeholderView, alignment: .topLeading)
}
#ViewBuilder
var placeholderView: some View {
if showingPlaceholder {
Text(placeholder).foregroundColor(.gray)
.padding(.leading, 4)
.padding(.top, 8)
}
}
}
fileprivate struct NSTextViewWrapper: NSViewRepresentable {
typealias NSViewType = NSTextView
#Binding var text: NSAttributedString
#Binding var calculatedHeight: CGFloat
var onDone: (() -> Void)?
func makeNSView(context: NSViewRepresentableContext<NSTextViewWrapper>) -> NSTextView {
let textField = NSTextView()
textField.delegate = context.coordinator
textField.isEditable = true
textField.font = NSFont.preferredFont(forTextStyle: .body)
textField.isSelectable = true
textField.drawsBackground = false
textField.allowsUndo = true
/// Disabled these lines as not available/neeed/appropriate for AppKit
// textField.isUserInteractionEnabled = true
// textField.isScrollEnabled = false
// if nil != onDone {
// textField.returnKeyType = .done
// }
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textField
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, height: $calculatedHeight, onDone: onDone)
}
func updateNSView(_ NSView: NSTextView, context: NSViewRepresentableContext<NSTextViewWrapper>) {
NSTextViewWrapper.recalculateHeight(view: NSView, result: $calculatedHeight)
}
fileprivate static func recalculateHeight(view: NSView, result: Binding<CGFloat>) {
/// UIView.sizeThatFits is not available in AppKit. Tried substituting below, but there's a loop that crashes.
// let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
// tried reportedSize = view.frame, view.intrinsicContentSize
let reportedSize = view.fittingSize
let newSize = CGSize(width: reportedSize.width, height: CGFloat.greatestFiniteMagnitude)
if result.wrappedValue != newSize.height {
DispatchQueue.main.async {
result.wrappedValue = newSize.height // !! must be called asynchronously
}
}
}
final class Coordinator: NSObject, NSTextViewDelegate {
var text: Binding<NSAttributedString>
var calculatedHeight: Binding<CGFloat>
var onDone: (() -> Void)?
init(text: Binding<NSAttributedString>, height: Binding<CGFloat>, onDone: (() -> Void)? = nil) {
self.text = text
self.calculatedHeight = height
self.onDone = onDone
}
func textDidChange(_ notification: Notification) {
guard notification.name == NSText.didChangeNotification,
let textView = (notification.object as? NSTextView),
let latestText = textView.textStorage else { return }
text.wrappedValue = latestText
NSTextViewWrapper.recalculateHeight(view: textView, result: calculatedHeight)
}
func textView(_ textView: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool {
if let onDone = self.onDone, replacementString == "\n" {
textView.resignFirstResponder()
onDone()
return false
}
return true
}
}
}
Solution thanks to #Asperi's tip to convert his UIKit code in this post. A few things had to change:
NSView also lacks the view.sizeThatFits() for a proposed bounds change, so I found that the view's .visibleRect would work instead.
Bugs:
There is a bobble on first render (from smaller vertically to the proper size). I thought it was caused by the recalculateHeight(), which would print out some smaller values initially. A gating statement there stopped those values, but the bobble is still there.
Currently I set the placeholder text's inset by a magic number, which should be done based on the NSTextView's attributes, but I didn't find anything usable yet. If it has the same font I guess I could just add a space or two in front of the placeholder text and be done with it.
Hope this saves some others making SwiftUI Mac apps some time.
import SwiftUI
// Wraps the NSTextView in a frame that can interact with SwiftUI
struct MultilineTextField: View {
private var placeholder: NSAttributedString
#Binding private var text: NSAttributedString
#State private var dynamicHeight: CGFloat // MARK TODO: - Find better way to stop initial view bobble (gets bigger)
#State private var textIsEmpty: Bool
#State private var textViewInset: CGFloat = 9 // MARK TODO: - Calculate insetad of magic number
var nsFont: NSFont
init (_ placeholder: NSAttributedString = NSAttributedString(string: ""),
text: Binding<NSAttributedString>,
nsFont: NSFont) {
self.placeholder = placeholder
self._text = text
_textIsEmpty = State(wrappedValue: text.wrappedValue.string.isEmpty)
self.nsFont = nsFont
_dynamicHeight = State(initialValue: nsFont.pointSize)
}
var body: some View {
ZStack {
NSTextViewWrapper(text: $text,
dynamicHeight: $dynamicHeight,
textIsEmpty: $textIsEmpty,
textViewInset: $textViewInset,
nsFont: nsFont)
.background(placeholderView, alignment: .topLeading)
// Adaptive frame applied to this NSViewRepresentable
.frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
}
}
// Background placeholder text matched to default font provided to the NSViewRepresentable
var placeholderView: some View {
Text(placeholder.string)
// Convert NSFont
.font(.system(size: nsFont.pointSize))
.opacity(textIsEmpty ? 0.3 : 0)
.padding(.leading, textViewInset)
.animation(.easeInOut(duration: 0.15))
}
}
// Creates the NSTextView
fileprivate struct NSTextViewWrapper: NSViewRepresentable {
#Binding var text: NSAttributedString
#Binding var dynamicHeight: CGFloat
#Binding var textIsEmpty: Bool
// Hoping to get this from NSTextView,
// but haven't found the right parameter yet
#Binding var textViewInset: CGFloat
var nsFont: NSFont
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text,
height: $dynamicHeight,
textIsEmpty: $textIsEmpty,
nsFont: nsFont)
}
func makeNSView(context: NSViewRepresentableContext<NSTextViewWrapper>) -> NSTextView {
return context.coordinator.textView
}
func updateNSView(_ textView: NSTextView, context: NSViewRepresentableContext<NSTextViewWrapper>) {
NSTextViewWrapper.recalculateHeight(view: textView, result: $dynamicHeight, nsFont: nsFont)
}
fileprivate static func recalculateHeight(view: NSView, result: Binding<CGFloat>, nsFont: NSFont) {
// Uses visibleRect as view.sizeThatFits(CGSize())
// is not exposed in AppKit, except on NSControls.
let latestSize = view.visibleRect
if result.wrappedValue != latestSize.height &&
// MARK TODO: - The view initially renders slightly smaller than needed, then resizes.
// I thought the statement below would prevent the #State dynamicHeight, which
// sets itself AFTER this view renders, from causing it. Unfortunately that's not
// the right cause of that redawing bug.
latestSize.height > (nsFont.pointSize + 1) {
DispatchQueue.main.async {
result.wrappedValue = latestSize.height
print(#function, latestSize.height)
}
}
}
}
// Maintains the NSTextView's persistence despite redraws
fileprivate final class Coordinator: NSObject, NSTextViewDelegate, NSControlTextEditingDelegate {
var textView: NSTextView
#Binding var text: NSAttributedString
#Binding var dynamicHeight: CGFloat
#Binding var textIsEmpty: Bool
var nsFont: NSFont
init(text: Binding<NSAttributedString>,
height: Binding<CGFloat>,
textIsEmpty: Binding<Bool>,
nsFont: NSFont) {
_text = text
_dynamicHeight = height
_textIsEmpty = textIsEmpty
self.nsFont = nsFont
textView = NSTextView(frame: .zero)
textView.isEditable = true
textView.isSelectable = true
// Appearance
textView.usesAdaptiveColorMappingForDarkAppearance = true
textView.font = nsFont
textView.textColor = NSColor.textColor
textView.drawsBackground = false
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
// Functionality (more available)
textView.allowsUndo = true
textView.isAutomaticLinkDetectionEnabled = true
textView.displaysLinkToolTips = true
textView.isAutomaticDataDetectionEnabled = true
textView.isAutomaticTextReplacementEnabled = true
textView.isAutomaticDashSubstitutionEnabled = true
textView.isAutomaticSpellingCorrectionEnabled = true
textView.isAutomaticQuoteSubstitutionEnabled = true
textView.isAutomaticTextCompletionEnabled = true
textView.isContinuousSpellCheckingEnabled = true
super.init()
// Load data from binding and set font
textView.textStorage?.setAttributedString(text.wrappedValue)
textView.textStorage?.font = nsFont
textView.delegate = self
}
func textDidChange(_ notification: Notification) {
// Recalculate height after every input event
NSTextViewWrapper.recalculateHeight(view: textView, result: $dynamicHeight, nsFont: nsFont)
// If ever empty, trigger placeholder text visibility
if let update = (notification.object as? NSTextView)?.string {
textIsEmpty = update.isEmpty
}
}
func textDidEndEditing(_ notification: Notification) {
// Update binding only after editing ends; useful to gate NSManagedObjects
$text.wrappedValue = textView.attributedString()
}
}
I found nice gist code created by unnamedd.
https://gist.github.com/unnamedd/6e8c3fbc806b8deb60fa65d6b9affab0
Sample Usage:
MacEditorTextView(
text: $text,
isEditable: true,
font: .monospacedSystemFont(ofSize: 12, weight: .regular)
)
.frame(minWidth: 300,
maxWidth: .infinity,
minHeight: 100,
maxHeight: .infinity)
.padding(12)
.cornerRadius(8)

Customise Annotation in MapKit in swiftUI

Im try to customise my annotation in MapKit and SwiftUI
From the code below, I search in the map the specific coordinate (coord) and I display with my custom annotation.
1) I'm try to increase the size of the UIimage because to small (see the picture attached)and change the color, any idea how?
2)in the map after the app start it display only the icon, after I tap on the icon the annotation appear, any idea how to display immediately my annotation without tapping?
3)now in the annotation I manage to display title and subtitle, how to display also the coordinate
struct MapView: UIViewRepresentable {
let Mm : MapManager
let coord = CLLocationCoordinate2D(latitude: 52.28792, longitude: 4.73415327)
class Coordinator: NSObject, MKMapViewDelegate {
var parent : MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "TESTING NOTE")
annotationView.canShowCallout = true
annotationView.image = UIImage(systemName: "location.circle")
return annotationView
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
let view = MKMapView(frame: .zero)
Mm.georeverseCoordinate(coord) { (pin) in
if let pinOK = pin {
view.removeAnnotation(pinOK)
view.mapType = MKMapType.satellite
let span = MKCoordinateSpan(latitudeDelta: 0.04, longitudeDelta: 0.04)
let region = MKCoordinateRegion(center: self.coord, span: span)
view.setRegion(region, animated: true)
view.delegate = context.coordinator
view.addAnnotation(pinOK)
}
}
return view
}
func updateUIView(_ view: MKMapView, context: Context) {
}
}
Map manager
class MapManager: NSObject, CLLocationManagerDelegate {
static let shared = MapManager()
func georeverseCoordinate(_ coord: CLLocationCoordinate2D , closure: #escaping (Pin?) -> Void) {
let location = CLLocation(latitude: coord.latitude, longitude: coord.longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (arrayResponse, error) in
if let errorTest = error {
debugPrint(errorTest.localizedDescription)
closure(nil)
return
}
if let arrayPins = arrayResponse {
if let valorePinArray = arrayPins.first {
debugPrint(valorePinArray.locality!)
debugPrint(valorePinArray.isoCountryCode!)
let pin = Pin(title: valorePinArray.locality!, subtitle: valorePinArray.isoCountryCode!, coordinate: valorePinArray.location!.coordinate)
closure(pin)
}
else { closure(nil) }
}
else { closure(nil) }
}
}
}
Pin Model
class Pin:NSObject, MKAnnotation {
var title : String?
var subtitle : String?
var coordinate : CLLocationCoordinate2D
var color: UIColor?
init(title: String?, subtitle: String?, coordinate: CLLocationCoordinate2D) {
self.title = title
self.subtitle = subtitle
self.coordinate = coordinate
}
}
1) - There seems to be a bug that makes it difficult to change the colour of SF Symbols with tintColor and a specific rendering mode but there is a workaround that also allows an easy way to change the size of the symbol.
In mapView:viewFor annotation add the following:
annotationView.canShowCallout = true
annotationView.image = UIImage(systemName: "location.circle")?.withTintColor(.systemGreen, renderingMode: .alwaysOriginal)
let size = CGSize(width: 40, height: 40)
annotationView.image = UIGraphicsImageRenderer(size:size).image {
_ in annotationView.image!.draw(in:CGRect(origin:.zero, size:size))
}
2) - To show the callout as soon as the annotation is added to the map, use selectAnnotation.
view.addAnnotation(pinOK)
view.selectAnnotation(pinOK, animated: true)
3) - The coordinate is available when your annotation is being constructed so you can simply change the init in the Pin class:
init(title: String?, subtitle: String?, coordinate: CLLocationCoordinate2D) {
self.coordinate = coordinate
self.title = "\(coordinate.latitude) : \(coordinate.longitude)"
self.subtitle = subtitle
}

How do I clear pins on a mapview in swift?

Part of my app contains a mapview which searches automatically based on a keyword. I added a textbox and search button to my mapview, but I noticed the pins do not clear from the previous search. How do I clear the map of all pins before the new search?
import UIKit
import MapKit
import CoreLocation
import iAd
class MapClass: UIViewController, CLLocationManagerDelegate, UISearchBarDelegate, ADBannerViewDelegate {
var searchController:UISearchController!
var annotation:MKAnnotation!
var localSearchRequest:MKLocalSearchRequest!
var localSearch:MKLocalSearch!
var localSearchResponse:MKLocalSearchResponse!
var error:NSError!
var pointAnnotation:MKPointAnnotation!
var pinAnnotationView:MKPinAnnotationView!
var holidayKeyWord = NSString()
#IBOutlet var mapSearchTextbox: UITextField!
#IBOutlet weak var mapView: MKMapView!
#IBOutlet var adBannerView: ADBannerView!
var locationManager: CLLocationManager!
let searchRadius: CLLocationDistance = 2000
override func viewDidLoad() {
super.viewDidLoad()
holidayKeyWord = "Restaurant"
println(holidayKeyWord)
mapSearchTextbox.text = holidayKeyWord as String
self.canDisplayBannerAds = true
self.adBannerView?.delegate = self
self.adBannerView?.hidden = false
if (CLLocationManager.locationServicesEnabled())
{
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
}
#IBAction func mapSearchButton(sender: UIButton) {
//clear pins
var holidayKeyWord = mapSearchTextbox.text
if (CLLocationManager.locationServicesEnabled())
{
let request = MKLocalSearchRequest()
request.naturalLanguageQuery = holidayKeyWord as String
let search = MKLocalSearch(request: request)
search.startWithCompletionHandler {
(response: MKLocalSearchResponse!, error: NSError!) in
for item in response.mapItems as! [MKMapItem] {
println(item.name)
self.addPinToMapView(item.name, latitude: item.placemark.location.coordinate.latitude, longitude: item.placemark.location.coordinate.longitude)
}
}
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
//replace spaces with dashes in wikiDate string
if (segue.identifier == "amazonToWeb") {
var wikiDate = "http://www.amazon.com/s/ref=nb_sb_noss?url=search-alias%3Ddigital-text&field-keywords=\(holidayKeyWord)"
var DestViewController : WebBrowser = segue.destinationViewController as! WebBrowser
DestViewController.wikiDate = wikiDate
}
if (segue.identifier == "searchToWeb") {
var wikiDate = "http://www.bing.com"
var DestViewController : WebBrowser = segue.destinationViewController as! WebBrowser
DestViewController.wikiDate = wikiDate
}
if (segue.identifier == "ebayToWeb") {
var wikiDate = "http://search.ebay.com/\(holidayKeyWord)"
var DestViewController : WebBrowser = segue.destinationViewController as! WebBrowser
DestViewController.wikiDate = wikiDate
}
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
let location = locations.last as! CLLocation
let center = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
self.mapView.setRegion(region, animated: true)
var latitude: Double = location.coordinate.latitude
var longitude: Double = location.coordinate.longitude
let initialLocation = CLLocation(latitude: latitude, longitude: longitude)
// 1
let request = MKLocalSearchRequest()
request.naturalLanguageQuery = holidayKeyWord as String
// 2
let span = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
request.region = MKCoordinateRegion(center: initialLocation.coordinate, span: span)
// 3
let search = MKLocalSearch(request: request)
search.startWithCompletionHandler {
(response: MKLocalSearchResponse!, error: NSError!) in
for item in response.mapItems as! [MKMapItem] {
println(item.name)
//println("Latitude = \(item.placemark.location.coordinate.latitude)")
//println("Longitude = \(item.placemark.location.coordinate.longitude)")
self.addPinToMapView(item.name, latitude: item.placemark.location.coordinate.latitude, longitude: item.placemark.location.coordinate.longitude)
}
}
locationManager.stopUpdatingLocation()
let coordinateRegion = MKCoordinateRegionMakeWithDistance(initialLocation.coordinate, searchRadius * 2.0, searchRadius * 2.0)
mapView.setRegion(coordinateRegion, animated: true)
}
func addPinToMapView(title: String, latitude: CLLocationDegrees, longitude: CLLocationDegrees) {
let location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
let annotation = MyAnnotation(coordinate: location, title: title)
mapView.addAnnotation(annotation)
}
func removePinFromMapView(title: String, latitude: CLLocationDegrees, longitude: CLLocationDegrees) {
let location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
let annotation = MyAnnotation(coordinate: location, title: title)
mapView.removeAnnotation(annotation)
}
func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!)
{
println("Error: " + error.localizedDescription)
}
Iterate through your annotations and delete them one by one like this:
if let annotations = self.mapView.annotations {
for _annotation in annotations {
if let annotation = _annotation as? MKAnnotation
{
self.mapView.removeAnnotation(annotation)
}
}
}
There is a short way of doing #ezcoding's answer.
mapView.removeAnnotations(mapView.annotations)
You are adding the pins by saying addAnnotation. To remove them, just reverse that: say removeAnnotation.
[self.mapView removeAnnotations:mapView.annotations];
For Swift 3.0
func removeAllAnnotations() {
for annotation in self.mapView.annotations {
self.mapView.removeAnnotation(annotation)
}
}

Resources