How to make SwiftUI MapPin clickable [duplicate] - ios

I'm using SwiftUI new Map view to display pins for annotations, and would like the pins, when clicked, to display a view for editing the annotation's name and description.
I've tried to use MapAnnotation with a view that contains a button with a pin image. The image displays correctly, but the button doesn't work.
Is it possible to do this without falling back to a UIViewRepresentable of MKMapView?
import SwiftUI
import MapKit
struct ContentView: View {
#State private var showingEditScreen = false
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.334722,
longitude: -122.008889),
span: MKCoordinateSpan(latitudeDelta: 1,
longitudeDelta: 1)
)
#State private var pins: [Pin] = [
Pin(name: "Apple Park",
description: "Apple Inc. headquarters",
coordinate: CLLocationCoordinate2D(latitude: 37.334722,
longitude:-122.008889))
]
var body: some View {
Map(coordinateRegion: $region,
interactionModes: .all,
annotationItems: pins,
annotationContent: { pin in
MapAnnotation(coordinate: pin.coordinate,
content: {
PinButtonView(pin: pin)
})
})
}
}
My Pin definition:
struct Pin: Identifiable {
let id = UUID()
var name: String
var description: String
var coordinate: CLLocationCoordinate2D
}
The view with the button and pin image:
struct PinButtonView: View {
#State private var showingEditScreen = false
#State var pin: Pin
var body: some View {
Button(action: {
showingEditScreen.toggle()
}) {
Image(systemName: "mappin")
.padding()
.foregroundColor(.red)
.font(.title)
}
.sheet(isPresented: $showingEditScreen,
content: {
EditView(pin: self.$pin)
})
}
}
Editing view:
struct EditView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var pin: Pin
var body: some View {
NavigationView {
Form {
TextField("Place name", text: $pin.name)
TextField("Description", text: $pin.description)
}
.navigationTitle("Edit place")
.navigationBarItems(trailing: Button("Done") {
self.presentationMode.wrappedValue.dismiss()
})
}
}
}

As of XCode 12.3 this appears to be functioning with buttons. One key gotcha that I noticed though was that if you use offsets then it's possible your buttons will be placed out of the tappable area of the annotation.
In order to counter that you can add additional padding to account for the offset and keep it within tappable bounds:
MapAnnotation(coordinate: item.placemark.coordinate) {
ZStack {
MapPinView() // My view for showing a precise pin
VStack { // A prompt with interactive option, offset by 60pt
Text("Use this location?")
HStack {
Button("Yes", action: {
print("yes")
})
Button("No", action: {
print("no")
})
}
}
.offset(x: 0, y: 60) // offset prompt so it's below the marker pin
}
.padding(.vertical, 60) // compensate for offset in tappable area of annotation. pad both the top AND the bottom to keep contents centered
}

I have a SwiftUI MapKit view that uses MapAnnotations with a pin that is tappable and it displays a View with information in.
It is not pretty but it works for now iOS 14 only.
https://github.com/PhilStollery/BAB-Club-Search/blob/main/Shared/views/AnnotatedMapView.swift
import MapKit
import SwiftUI
struct AnnotatedMapView: View {
#ObservedObject
private var locationManager = LocationManager()
// Default to center on the UK, zoom to show the whole island
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 54.4609,
longitude: -3.0886),
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))
#ObservedObject var store: ClubStore
var body: some View {
ZStack {
Map(coordinateRegion: $region,
showsUserLocation: true,
annotationItems: store.clubs!) {
club in MapAnnotation(coordinate: club.coordinate) {
VStack{
if club.show {
ZStack{
RoundedRectangle(cornerRadius: 10)
.fill(Color(UIColor.systemBackground))
.shadow(radius: 2, x: 2, y: 2)
VStack{
NavigationLink(destination: ClubLocationView(club: club)) {
Text(club.clubname)
.fontWeight(.bold)
}
.padding()
Text(club.association)
.padding(.leading)
.padding(.trailing)
Text(club.town)
.padding(.bottom)
}
}
.onTapGesture {
let index: Int = store.clubs!.firstIndex(where: {$0.id == club.id})!
store.clubs![index].show = false
}
} else {
Image(systemName: "house.circle")
.font(.title)
.foregroundColor(.accentColor)
.onTapGesture {
let index: Int = store.clubs!.firstIndex(where: {$0.id == club.id})!
store.clubs![index].show = true
}
}
}
}
}
.edgesIgnoringSafeArea(.bottom)
}
.navigationBarTitle(Text("Map View"), displayMode: .inline)
.navigationBarItems(trailing:
Button(action: {
withAnimation {
self.region.center = locationManager.current!.coordinate
self.region.span = MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
}
})
{Image(systemName: "location")}
)
}
}

Related

SwiftUI sheet reappears after dismissed on Apple Watch

I'm developing a fitness app for the Apple Watch that lets you choose an intensity before starting a workout. I want to present the intensity picker before the workout starts, so I tried presenting it as a sheet before navigating to the actual workout view. The problem is that when I try to dismiss the sheet, it is dismissed but it comes right back. I'm using Xcode 14.2 and watchOS 9.1.
This is the main view and also the view that presents the said sheet (the first one, controlled by showingZonePickerView):
import SwiftUI
#main
struct Wise_Watch_AppApp: App {
#StateObject private var workoutManager = WorkoutManager()
#SceneBuilder var body: some Scene {
WindowGroup {
NavigationView {
WorkoutView()
}
.sheet(isPresented: $workoutManager.showingZonePickerView, onDismiss: {
workoutManager.showingZonePickerView = false
}) {
WorkoutLevelPickerView(total: 5, completed: 1)
.toolbar(.hidden)
}
.sheet(isPresented: $workoutManager.showingSummaryView) {
SummaryView()
}
.environmentObject(workoutManager)
}
}
}
This is the view where the user can pick the preferred workout:
import SwiftUI
import HealthKit
struct WorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#State var linkActive: Bool = false
var workoutTypes: [HKWorkoutActivityType] = [.cycling, .running, .walking]
var workoutDictionary: Dictionary<String, HKWorkoutActivityType> = [
"figure.outdoor.cycle" : .cycling,
"figure.run" : .running,
"figure.walk" : .walking
]
var body: some View {
List(Array(workoutDictionary.keys), id: \.self) { workoutType in
NavigationLink(
destination: SessionPagingView(),
tag: workoutDictionary[workoutType]!,
selection: $workoutManager.selectedWorkout
) {
Label("\(workoutDictionary[workoutType]!.name)", systemImage: workoutType)
.font(.subheadline)
.fontWeight(.semibold)
.padding()
}
.padding(EdgeInsets(top: 15, leading: 5, bottom: 15, trailing: 5))
}
.listStyle(.carousel)
.navigationBarTitle("Workouts")
.onAppear {
workoutManager.requestAuthorization()
}
}
}
This is the view to which the app navigates when a NavigationLink is pressed:
import SwiftUI
import WatchKit
import ConfettiSwiftUI
struct SessionPagingView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#State private var selection: Tab = .metrics
#State private var counter: Int = 0
#State private var isViewHidden: Bool = true
enum Tab {
case metrics, nowPlaying, milestone
}
var body: some View {
if(isViewHidden) {
sessionView.hidden()
} else {
sessionView
}
}
var sessionView: some View {
TabView(selection: $selection) {
ForEach(workoutManager.tabItems) { item in
VStack {
Text("Congrats!")
.font(.title2)
.fontWeight(.bold)
.confettiCannon(counter: $counter, num: 40, radius: 200)
Text("You just reached")
.font(.subheadline)
.foregroundColor(.cyan)
.tag(Tab.milestone)
Text("\(item.value) km")
.font(.subheadline)
.foregroundColor(.cyan)
.tag(Tab.milestone)
}
}
MetricsView().tag(Tab.metrics)
NowPlayingView().tag(Tab.nowPlaying)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.onAppear {
workoutManager.showingZonePickerView = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isViewHidden = false
}
}
.onChange(of: workoutManager.running) { _ in
displayMetricsView()
}
.onChange(of: workoutManager.tabItems) { _ in
if(workoutManager.tabItems.count > 0) {
displayMilestoneView()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: {
displayMetricsView()
})
}
}
.ignoresSafeArea()
.animation(.easeInOut, value: self.selection)
}
private func displayMetricsView() {
withAnimation {
selection = .metrics
}
}
private func displayMilestoneView() {
withAnimation {
selection = .milestone
WKInterfaceDevice.current().play(.notification)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
counter += 1
})
}
}
}
This is the actual view that I'm presenting inside the sheet:
import SwiftUI
struct WorkoutLevelPickerView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#Environment(\.dismiss) private var dismiss
let total: Int
#State var lineWidth: CGFloat = 16
#State var color: Color = .green
#State var completed: Double = 1.0
#State var currentZone: HeartRateZone = zones[0]
#State var isScrolling: Bool = false
var body: some View {
VStack {
ZStack {
CircleLabelView(
radius: 30,
tracking: 0,
size: .init(width: 120, height: 120),
text: currentZone.intensity.uppercased()
)
.font(.headline)
.frame(width: 50, height: 50)
.rotationEffect(Angle(degrees: Double(currentZone.labelRotationAngleModifier * currentZone.intensity.count / 2)))
.opacity(isScrolling ? 0 : 1)
.animation(.easeInOut, value: isScrolling)
WorkoutLevelPickerBackgroundView(total: total, lineWidth: lineWidth)
withAnimation(.spring()) {
WorkoutLevelPickerProgressView(total: total, completed: Int(completed), lineWidth: lineWidth, zone: currentZone)
}
VStack {
Button {
dismiss()
} label: {
Image(systemName: currentZone.iconName)
.padding()
.font(.title2)
}
.frame(width: 70, height: 70)
}
}
.frame(width: 150, height: 150)
.focusable()
.digitalCrownRotation(
detent: $completed.animation(.spring()),
from: 1.0,
through: 5.0,
by: 1.0,
sensitivity: .low,
isContinuous: false,
isHapticFeedbackEnabled: true,
onChange: { _ in
isScrolling = true
},
onIdle: {
isScrolling = false
}
)
.digitalCrownAccessory(.hidden)
.onChange(of: completed) {_ in
if(Int(completed) != currentZone.id) {
currentZone = zones[Int(completed) - 1]
print(currentZone.tint.description)
}
}
}
}
}
This is a video of the flow that generates my problem:
I tried presenting the sheet from other views. I also tried dismissing the sheet through a binding, not through the dismiss action. On all these changes, the outcome was the same as before.
Edit:
Forgot to mention that, when the sheet is presented, pressing the side button will dismiss it and the app will work as intended until the user tries to start a new workout.
As Paulw11 mentioned in his comment, the problem was that I was constantly setting showingZonePickerView on true when the sheet was dismissed and the view reappeared.

SwiftUI Rotation Animation off center

I am trying to make a simple dropdown list item in SwiftUI. This is what the code looks like:
struct SomeObject: Hashable {
var title: String = "title"
var entries: [String] = ["details", "details2", "details3"]
}
struct ContentView: View {
var data: [SomeObject] = [SomeObject()]
var body: some View {
List(data, id: \.self) { item in
HStack {
Text(item.title)
Spacer()
}
ForEach(item.entries, id: \.self) { entry in
ListItemView(entry)
}
}.listStyle(.plain)
}
}
struct ListItemView: View {
#State var expanded: Bool = false
#State var rotation: Double = 0
private let entry: String
init(_ entry: String) {
self.entry = entry
}
var body: some View {
VStack {
Divider().frame(maxWidth: .infinity)
.overlay(.black)
HStack {
Text(entry)
.fixedSize(horizontal: false, vertical: true)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(.black)
.padding()
.rotationEffect(.degrees(expanded ? 180 : 360))
.animation(.linear(duration: 0.3), value: expanded)
}.padding(.horizontal)
.padding(.vertical, 6)
if expanded {
Text("Details")
}
Divider().frame(maxWidth: .infinity)
.overlay(.black)
}
.listRowSeparator(.hidden)
.listRowInsets(.init())
.onTapGesture {
expanded.toggle()
}
}
}
For some reason when clicking on the list item the animation looks like this:
How can I make the arrow rotate on its center point without moving up or down at all?
The problem you have there is that the arrow is animated but when the hidden text appears, that vertical expansion is not animated. That contrast between an element animated and another that is not makes the chevron looks like it is not doing it properly. So, try to animate the VStack like this:
struct CombineView: View {
#State var expanded: Bool = false
#State var rotation: Double = 0
let entry: String = "Detalle"
var body: some View {
VStack {
Divider().frame(maxWidth: .infinity)
.overlay(.black)
HStack(alignment: .center) {
Text(entry)
.fixedSize(horizontal: false, vertical: true)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(.black)
.padding()
.rotationEffect(.degrees(expanded ? 180 : 360))
.animation(.linear(duration: 0.3), value: expanded)
}.padding(.horizontal)
.padding(.vertical, 6)
.background(.green)
if expanded {
Text("Details")
}
Divider().frame(maxWidth: .infinity)
.overlay(.black)
}.animation(.linear(duration: 0.3), value: expanded)//Animation added
.listRowSeparator(.hidden)
.listRowInsets(.init())
.onTapGesture {
expanded.toggle()
}
}
}
I hope this works for you ;)

SwiftUI Get Coordinates of TextField or any View

I'm struggling to find a way in SwiftUI to get the x,y coordinates of the origin of a
TextField (or any view). I can certainly provide a position or offset to move the view
but I can't seem to find a way to get a reference to the TextField to get its
coordinates.
In UIKit, I believe I would use myTextField.frame.origin.x
This is a very simple example:
struct ContentView: View {
#State private var someNumber1 = "1000"
#State private var someNumber2 = "2000"
//bunch more
#State private var enteredNumber = "Some Text at the Top"
#State var value: CGFloat = 0
var body: some View {
ScrollView {
VStack {
Spacer()
Text("\(enteredNumber)")
Spacer()
Group { //1
TextField("Placeholder", text: $someNumber1)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
//this does not work
.onTapGesture {
let aY = (textfieldreferencehere).frame.origin.y
}
TextField("Placeholder", text: $someNumber2)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
}//group 1
//bunch more
Button(action: {
self.enteredNumber = self.someNumber1
self.someNumber1 = ""
UIApplication.shared.endEditing()
}) {
Text("Submit")
}
.padding(.bottom, 50)
}//outer v
.padding(.horizontal, 16)
.padding(.top, 44)
}//Scrollview or Form
.modifier(AdaptsToSoftwareKeyboard())
}
}
Any Guidance would be appreciated. Xcode 11.4.1
Here is a demo of how specific view coordinates can be read (see helpful comments inline)
struct DemoReadViewOrigin: View {
#State private var someNumber1 = "1000"
#State private var someNumber2 = "2000"
//bunch more
#State private var enteredNumber = "Some Text at the Top"
#State var value: CGFloat = 0
var body: some View {
ScrollView {
VStack {
Spacer()
Text("\(enteredNumber)")
Spacer()
Group { //1
TextField("Placeholder", text: $someNumber1)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
.background(GeometryReader { gp -> Color in
let rect = gp.frame(in: .named("OuterV")) // < in specific container
// let rect = gp.frame(in: .global) // < in window
// let rect = gp.frame(in: .local) // < own bounds
print("Origin: \(rect.origin)")
return Color.clear
})
//this does not work
.onTapGesture {
}
TextField("Placeholder", text: $someNumber2)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
}//group 1
//bunch more
Button(action: {
self.enteredNumber = self.someNumber1
self.someNumber1 = ""
// UIApplication.shared.endEditing()
}) {
Text("Submit")
}
.padding(.bottom, 50)
}//outer v
.coordinateSpace(name: "OuterV") // << declare coord space
.padding(.horizontal, 16)
.padding(.top, 44)
}//Scrollview or Form
// .modifier(AdaptsToSoftwareKeyboard())
}
}

SwiftUI: Transition won't slide

I have control with an edit and a list in a Stack. I want the list to drop down from top to bottom to animate like a fancy menu. However I am having a few issues. If I just try to use the move transition nothing happens. If I use the scale transition it always scales form center never from top down. This was just trying to get the transition overridden to slide. Anything but fade.
My control looks like so
struct Search: Identifiable {
let id: UUID
let text: String
}
struct SearchBox: View {
#State var searchParam: String = ""
#State var stuff = [Search]()
init() {
// To remove only extra separators below the list:
UITableView.appearance().tableFooterView = UIView()
// To remove all separators including the actual ones:
UITableView.appearance().separatorStyle = .none
}
var body: some View {
var binding = Binding<String>(
get: {
self.searchParam
},
set: {
self.stuff.append(
Search(id: UUID(), text: $0))
self.searchParam = $0
})
return VStack(spacing: 0.0) {
TextField("Search", text: binding )
.font(.title)
.padding()
.background(Color.white)
Color(.darkGray)
.frame(height: 1.0)
if stuff.count > 0 {
List(stuff, id: \.id) {
Text($0.text)
}
.transition(.slide)
}
}
}
struct SearchBox_Preview: PreviewProvider {
static var previews: some View{
SearchBox()
}
}
}
The content view is simple..
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .topLeading) {
Color.blue
SearchBox()
.frame(width: geometry.size.width * 0.40, alignment: .topLeading)
.frame(minHeight: 0, maxHeight: geometry.size.height * 0.40,
alignment: .topLeading)
.padding()
.clipped()
.shadow(radius: 5.0)
}
.background(Color.clear)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I have to be missing, or not getting, something simple
Answered my own question. We need to force an animation by using withAnimation on a state. So I have changed my biding to be like so:
struct Search: Identifiable {
let id: UUID
let text: String
}
struct SearchBox: View {
#State var searchParam: String = ""
#State var stuff = [Search]()
#State var showList = false
init() {
// To remove only extra separators below the list:
UITableView.appearance().tableFooterView = UIView()
// To remove all separators including the actual ones:
UITableView.appearance().separatorStyle = .none
}
var body: some View {
var binding = Binding<String>(
get: {
self.searchParam
},
set: {
self.stuff.append(
Search(id: UUID(), text: $0)
)
self.searchParam = $0
// change visibility state with in animation block.
withAnimation { self.showList = stuff.count > 0 }
})
return VStack(spacing: 0.0) {
TextField("Search", text: binding )
.font(.title)
.padding()
.background(Color.white)
Color(.darkGray)
.frame(height: 1.0)
if showList {
List(stuff, id: \.id) {
Text($0.text)
}
.transition(.slide)
}
}
}
struct SearchBox_Preview: PreviewProvider {
static var previews: some View{
SearchBox()
}
}
}

Custom modal transitions in SwiftUI

I'm trying to recreate the iOS 11/12 App Store with SwiftUI.
Let's imagine the "story" is the view displayed when tapping on the card.
I've done the cards, but the problem I'm having now is how to do the animation done to display the "story".
As I'm not good at explaining, here you have a gif:
Gif 1
Gif 2
I've thought of making the whole card a PresentationLink, but the "story" is displayed as a modal, so it doesn't cover the whole screen and doesn't do the animation I want.
The most similar thing would be NavigationLink, but that then obliges me to add a NavigationView, and the card is displayed like another page.
I actually do not care whether its a PresentationLink or NavigationLink or whatever as long as it does the animation and displays the "story".
Thanks in advance.
My code:
Card.swift
struct Card: View {
var icon: UIImage = UIImage(named: "flappy")!
var cardTitle: String = "Welcome to \nCards!"
var cardSubtitle: String = ""
var itemTitle: String = "Flappy Bird"
var itemSubtitle: String = "Flap That!"
var cardCategory: String = ""
var textColor: UIColor = UIColor.white
var background: String = ""
var titleColor: Color = .black
var backgroundColor: Color = .white
var body: some View {
VStack {
if background != "" {
Image(background)
.resizable()
.frame(width: 380, height: 400)
.cornerRadius(20)
} else {
RoundedRectangle(cornerRadius: 20)
.frame(width: 400, height: 400)
.foregroundColor(backgroundColor)
}
VStack {
HStack {
VStack(alignment: .leading) {
if cardCategory != "" {
Text(verbatim: cardCategory.uppercased())
.font(.headline)
.fontWeight(.heavy)
.opacity(0.3)
.foregroundColor(titleColor)
//.opacity(1)
}
HStack {
Text(verbatim: cardTitle)
.font(.largeTitle)
.fontWeight(.heavy)
.lineLimit(3)
.foregroundColor(titleColor)
}
}
Spacer()
}.offset(y: -390)
.padding(.bottom, -390)
HStack {
if cardSubtitle != "" {
Text(verbatim: cardSubtitle)
.font(.system(size: 17))
.foregroundColor(titleColor)
}
Spacer()
}
.offset(y: -50)
.padding(.bottom, -50)
}
.padding(.leading)
}.padding(.leading).padding(.trailing)
}
}
So
Card(cardSubtitle: "Welcome to this library I made :p", cardCategory: "CONNECT", background: "flBackground", titleColor: .white)
displays:
SwiftUI doesn't do custom modal transitions right now, so we have to use a workaround.
One method that I could think of is to do the presentation yourself using a ZStack. The source frame could be obtained using a GeometryReader. Then, the destination shape could be controlled using frame and position modifiers.
In the beginning, the destination will be set to exactly match position and size of the source. Then immediately afterwards, the destination will be set to fullscreen size in an animation block.
struct ContentView: View {
#State var isPresenting = false
#State var isFullscreen = false
#State var sourceRect: CGRect? = nil
var body: some View {
ZStack {
GeometryReader { proxy in
Button(action: {
self.isFullscreen = false
self.isPresenting = true
self.sourceRect = proxy.frame(in: .global)
}) { ... }
}
if isPresenting {
GeometryReader { proxy in
ModalView()
.frame(
width: self.isFullscreen ? nil : self.sourceRect?.width ?? nil,
height: self.isFullscreen ? nil : self.sourceRect?.height ?? nil)
.position(
self.isFullscreen ? proxy.frame(in: .global).center :
self.sourceRect?.center ?? proxy.frame(in: .global).center)
.onAppear {
withAnimation {
self.isFullscreen = true
}
}
}
}
}
.edgesIgnoringSafeArea(.all)
}
}
extension CGRect {
var center : CGPoint {
return CGPoint(x:self.midX, y:self.midY)
}
}
SwiftUI in iOS/tvOS 14 and macOS 11 has matchedGeometryEffect(id:in:properties:anchor:isSource:) to animate view transitions between different hierarchies.
Link to Official Documentation
Here's a minimal example:
struct SomeView: View {
#State var isPresented = false
#Namespace var namespace
var body: some View {
VStack {
Button(action: {
withAnimation {
self.isPresented.toggle()
}
}) {
Text("Toggle")
}
SomeSourceContainer {
MatchedView()
.matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: !isPresented)
}
if isPresented {
SomeTargetContainer {
MatchedTargetView()
.matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: isPresented)
}
}
}
}
}

Resources