I am trying to make a SwiftUI View expand its available space. I have a MapView which uses the MapKit to display a map on the screen. I would like this map to expand the available space in the VStack. You can see it is above a view colored red, and below a search bar. If i make the red colored view have a height of 100, then the MapView shrinks down. If I do not set the height on the red colored view, then the MapView is bigger however the red view does not look as I want.
I want the red view to have a height of 100, and the MapView to fill all available height underneath the search bar, and above the red view.
ContentView:
struct ContentView: View {
#ObservedObject
var viewModel: HomeViewModel
var body: some View {
NavigationView {
ZStack {
ColorTheme.brandBlue.value.edgesIgnoringSafeArea(.all)
VStack {
CardView {
EditText(hint: "Search", text: self.$viewModel.searchText, textContentType: UITextContentType.organizationName)
}
MapView(annotations: self.$viewModel.restaurantAnnotations)
.cornerRadius(8)
CardView(height: 100) {
HStack {
Color.red
}
}
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
}
CardView
struct CardView<Content>: View where Content : View {
var height: CGFloat = .infinity
var content: () -> Content
var body: some View {
content()
.padding(EdgeInsets.init(top: 0, leading: 8, bottom: 8, trailing: 8))
.background(Color.white.cornerRadius(8))
.shadow(radius: 2, x: 0, y: 1)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: height)
}
}
EditText
struct EditText: View {
var hint: String
#Binding
var text: String
var label: String = ""
var textContentType: UITextContentType? = .none
var keyboardType: UIKeyboardType = .default
var textSize: CGFloat = 16
var body: some View {
return VStack(alignment: .leading) {
Text(label).font(.system(size: 12)).bold()
.foregroundColor(ColorTheme.text.value)
HStack {
TextField(hint, text: $text)
.lineLimit(1)
.font(.system(size: textSize))
.textContentType(textContentType)
.keyboardType(keyboardType)
.foregroundColor(ColorTheme.text.value)
}
Divider().background(ColorTheme.brandBlue.value)
}
}
}
MapView
struct MapView: UIViewRepresentable {
#Binding
var annotations: [MKAnnotation]
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
view.delegate = context.coordinator
view.addAnnotations(annotations)
if annotations.count == 1 {
let coords = annotations.first!.coordinate
let region = MKCoordinateRegion(center: coords, span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
view.setRegion(region, animated: true)
}
}
func makeCoordinator() -> MapViewCoordinator {
MapViewCoordinator(self)
}
}
MapViewCoordinator
class MapViewCoordinator: NSObject, MKMapViewDelegate {
var mapViewController: MapView
init(_ control: MapView) {
self.mapViewController = control
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?{
//Custom View for Annotation
let identifier = "Placemark"
if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) {
annotationView.annotation = annotation
return annotationView
} else {
let annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView.isEnabled = true
annotationView.canShowCallout = true
let button = UIButton(type: .infoDark)
annotationView.rightCalloutAccessoryView = button
return annotationView
}
return nil
}
}
As you can see the MapView does not fill the available space
Now all the available space is filled but the red view height is not 100
Here is a solution (tested with Xcode 11.4):
struct CardView<Content>: View where Content : View {
var height: CGFloat? = nil // << here !!
Note: defined height, even with .infinity, made your upper card equivalent by requesting height to map view, so they divided free space; when height is not specified, the component tights to content
Related
import SwiftUI
struct Level1: View {
#State var tapScore = 0
#State var showingMinedHammer = false
#State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(x:50,y:50)]
#State private var location = CGPoint.zero // < here !!
func mine() {
tapScore += 1
showMinedHammer()
}
func showMinedHammer() {
self.showingMinedHammer = true
DispatchQueue.main.asyncAfter(deadline: .now() + 99) {
self.showingMinedHammer = false
}
}
var body: some View {
GeometryReader { geometryProxy in
ZStack {
Image("hammer.fill").resizable().frame(width: UIScreen.main.bounds.height * 1.4, height: UIScreen.main.bounds.height)
.gesture(DragGesture(minimumDistance: 0).onEnded { value in
self.location = value.location // < here !!
self.mine()
})
if self.showingMinedHammer {
Image(systemName: "hammer.fill")
.resizable()
.frame(width: 30, height: 30)
.position(self.location) // < here !!
}
}
}.edgesIgnoringSafeArea(.all)
}
}
struct Level1_Previews: PreviewProvider {
static var previews: some View {
Level1()
}
}
struct GetTapLocation:UIViewRepresentable {
var tappedCallback: ((CGPoint) -> Void)
func makeUIView(context: UIViewRepresentableContext<GetTapLocation>) -> UIView {
let v = UIView(frame: .zero)
let gesture = UITapGestureRecognizer(target: context.coordinator,
action: #selector(Coordinator.tapped))
v.addGestureRecognizer(gesture)
return v
}
class Coordinator: NSObject {
var tappedCallback: ((CGPoint) -> Void)
init(tappedCallback: #escaping ((CGPoint) -> Void)) {
self.tappedCallback = tappedCallback
}
#objc func tapped(gesture:UITapGestureRecognizer) {
let point = gesture.location(in: gesture.view)
self.tappedCallback(point)
}
}
func makeCoordinator() -> GetTapLocation.Coordinator {
return Coordinator(tappedCallback:self.tappedCallback)
}
func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<GetTapLocation>) {
}
}
New to SwiftUI and I am trying to combine gestures that allow me to tap anywhere on the screen to add an infinite amount of "Images", but currently the image only stays on screen for a short while. Where am I going wrong? Am I supposed to combine another gesture to get the item to stay on screen whilst adding?
You only have a singular location, so only one item will ever appear in your example. To have multiple items, you'll need some sort of collection, like an Array.
The following is a pared-down example showing using an array:
struct Hammer: Identifiable {
var id = UUID()
var location: CGPoint
}
struct Level1: View {
#State var hammers: [Hammer] = [] //<-- Start with `none`
var body: some View {
ZStack {
ForEach(hammers) { hammer in // Display all of the hammers
Image(systemName: "hammer.fill")
.resizable()
.frame(width: 30, height: 30)
.position(hammer.location)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.gesture(DragGesture(minimumDistance: 0).onEnded { value in
self.hammers.append(Hammer(location: value.location)) // Add a Hammer
})
.edgesIgnoringSafeArea(.all)
}
}
Note: I'm unclear on what the GeometryReader is for in your code -- you declare it, then use UIScreen dimensions -- normally in SwiftUI we just use GeometryReader
I can’t figure out how to change the color of the image icon after clicking on it, for example, if the window is active, then the color of the icon is blue, if not, then it’s gray.
here is an example of what i am asking
If you know the solution please help me
Here is the full code
This code is fully working, you can check it out
import SwiftUI
struct AllLinesView: View {
#State var currentSelection: Int = 0
var body: some View {
PagerTabView(tint: .white, selection: $currentSelection) {
Image(systemName: "bolt.fill")
.pageLabel()
Image(systemName: "flame")
.pageLabel()
Image(systemName: "person.fill")
.pageLabel()
} content: {
Color.red
.pageView(ignoresSafeArea: true, edges: .bottom)
Color.green
.pageView(ignoresSafeArea: true, edges: .bottom)
Color.yellow
.pageView(ignoresSafeArea: true, edges: .bottom)
}
.ignoresSafeArea(.container, edges: .bottom)
}
}
TabView
struct PagerTabView<Content: View, Label: View>: View {
var content: Content
var label: Label
var tint: Color
#Binding var selection: Int
init(tint:Color,selection: Binding<Int>,#ViewBuilder labels: #escaping ()->Label,#ViewBuilder content: #escaping ()->Content) {
self.content = content()
self.label = labels()
self.tint = tint
self._selection = selection
}
#State var offset: CGFloat = 0
#State var maxTabs: CGFloat = 0
#State var tabOffset: CGFloat = 0
var body: some View {
VStack(alignment: .leading,spacing: 0) {
HStack(spacing: 0) {
label
}
.overlay(
HStack(spacing: 0) {
ForEach(0..<Int(maxTabs), id: \.self) { index in
Rectangle()
.fill(Color.black.opacity(0.01))
.onTapGesture {
let newOffset = CGFloat(index) * getScreenBounds().width
self.offset = newOffset
}
}
}
)
.foregroundColor(tint)
Capsule()
.fill(tint)
.frame(width: maxTabs == 0 ? 0 : (getScreenBounds().width / maxTabs), height: 2)
.padding(.top, 10)
.frame(maxWidth: .infinity, alignment: .leading)
.offset(x: tabOffset)
OffsetPageTabView(selection: $selection,offset: $offset) {
HStack(spacing: 0) {
content
}
.overlay(
GeometryReader { proxy in
Color.clear
.preference(key: TabPreferenceKey.self, value: proxy.frame(in: .global))
}
)
.onPreferenceChange(TabPreferenceKey.self) { proxy in
let minX = -proxy.minX
let maxWidth = proxy.width
let screenWidth = getScreenBounds().width
let maxTabs = (maxWidth / screenWidth).rounded()
let progress = minX / screenWidth
let tabOffset = progress * (screenWidth / maxTabs)
self.tabOffset = tabOffset
self.maxTabs = maxTabs
}
}
}
}
}
TabPreferenceKey
struct TabPreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .init()
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
pageLabel - pageView
extension View {
//IMAGE
func pageLabel()->some View {
self
.frame(maxWidth: .infinity, alignment: .center)
}
//PAGE
func pageView(ignoresSafeArea: Bool = false, edges: Edge.Set = [])->some View {
self
.frame(width: getScreenBounds().width, alignment: .center)
.ignoresSafeArea(ignoresSafeArea ? .container : .init(), edges: edges)
}
func getScreenBounds()->CGRect {
return UIScreen.main.bounds
}
}
OffsetPage
struct OffsetPageTabView<Content: View>: UIViewRepresentable {
var content: Content
#Binding var offset: CGFloat
#Binding var selection: Int
func makeCoordinator() -> Coordinator {
return OffsetPageTabView.Coordinator(parent: self)
}
init(selection: Binding<Int>,offset: Binding<CGFloat>, #ViewBuilder content: #escaping ()->Content) {
self.content = content()
self._offset = offset
self._selection = selection
}
func makeUIView(context: Context) -> UIScrollView {
let scrollview = UIScrollView()
let hostview = UIHostingController(rootView: content)
hostview.view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
hostview.view.topAnchor.constraint(equalTo: scrollview.topAnchor),
hostview.view.leadingAnchor.constraint(equalTo: scrollview.leadingAnchor),
hostview.view.trailingAnchor.constraint(equalTo: scrollview.trailingAnchor),
hostview.view.bottomAnchor.constraint(equalTo: scrollview.bottomAnchor),
hostview.view.heightAnchor.constraint(equalTo: scrollview.heightAnchor)
]
scrollview.addSubview(hostview.view)
scrollview.addConstraints(constraints)
scrollview.isPagingEnabled = true
scrollview.showsVerticalScrollIndicator = false
scrollview.showsHorizontalScrollIndicator = false
scrollview.delegate = context.coordinator
return scrollview
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
let currentOffset = uiView.contentOffset.x
if currentOffset != offset {
print("updating")
uiView.setContentOffset(CGPoint(x: offset, y: 0), animated: true)
}
}
class Coordinator: NSObject, UIScrollViewDelegate {
var parent: OffsetPageTabView
init(parent: OffsetPageTabView) {
self.parent = parent
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.x
let maxSize = scrollView.contentSize.width
let currentSelection = (offset / maxSize).rounded()
parent.selection = Int(currentSelection)
parent.offset = offset
}
}
}
This code is not built in a way that is easily changeable. The primary issue is that it uses ViewBuilders for the labels and pages, but our (not Apple's) SwiftUI code doesn't have insight into how many elements get passed into a ViewBuilder like this. So, I had to add a somewhat ugly hack of passing the number of child views by hand. I also had to add foregroundColor modifiers explicitly for each label, which is another result of shortcomings of the way the existing code works.
The original code's currentSelection logic was completely broken (ie didn't function at all), but was easily fixable once explicitly passing the number of child elements.
See updated code including inline comments of where changes were made.
struct AllLinesView: View {
#State var currentSelection: Int = 0
var body: some View {
PagerTabView(tint: .white, selection: $currentSelection, children: 3) { //<-- Here
Image(systemName: "bolt.fill")
.pageLabel()
.foregroundColor(currentSelection == 0 ? .blue : .white) //<-- Here
Image(systemName: "flame")
.pageLabel()
.foregroundColor(currentSelection == 1 ? .blue : .white) //<-- Here
Image(systemName: "person.fill")
.pageLabel()
.foregroundColor(currentSelection == 2 ? .blue : .white) //<-- Here
} content: {
Color.red
.pageView(ignoresSafeArea: true, edges: .bottom)
Color.green
.pageView(ignoresSafeArea: true, edges: .bottom)
Color.yellow
.pageView(ignoresSafeArea: true, edges: .bottom)
}
.ignoresSafeArea(.container, edges: .bottom)
.onChange(of: currentSelection) { newValue in
print(newValue)
}
}
}
struct PagerTabView<Content: View, Label: View>: View {
var content: Content
var label: Label
var tint: Color
var children: Int //<-- Here
#Binding var selection: Int
init(tint:Color,selection: Binding<Int>,children: Int, #ViewBuilder labels: #escaping ()->Label,#ViewBuilder content: #escaping ()->Content) {
self.children = children
self.content = content()
self.label = labels()
self.tint = tint
self._selection = selection
}
#State var offset: CGFloat = 0
#State var maxTabs: CGFloat = 0
#State var tabOffset: CGFloat = 0
var body: some View {
VStack(alignment: .leading,spacing: 0) {
HStack(spacing: 0) {
label
}
.overlay(
HStack(spacing: 0) {
ForEach(0..<Int(maxTabs), id: \.self) { index in
Rectangle()
.fill(Color.black.opacity(0.01))
.onTapGesture {
let newOffset = CGFloat(index) * getScreenBounds().width
self.offset = newOffset
}
}
}
)
.foregroundColor(tint)
Capsule()
.fill(tint)
.frame(width: maxTabs == 0 ? 0 : (getScreenBounds().width / maxTabs), height: 2)
.padding(.top, 10)
.frame(maxWidth: .infinity, alignment: .leading)
.offset(x: tabOffset)
OffsetPageTabView(selection: $selection,offset: $offset, children: children) { //<-- Here
HStack(spacing: 0) {
content
}
.overlay(
GeometryReader { proxy in
Color.clear
.preference(key: TabPreferenceKey.self, value: proxy.frame(in: .global))
}
)
.onPreferenceChange(TabPreferenceKey.self) { proxy in
let minX = -proxy.minX
let maxWidth = proxy.width
let screenWidth = getScreenBounds().width
let maxTabs = (maxWidth / screenWidth).rounded()
let progress = minX / screenWidth
let tabOffset = progress * (screenWidth / maxTabs)
self.tabOffset = tabOffset
self.maxTabs = maxTabs
}
}
}
}
}
struct TabPreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .init()
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
extension View {
//IMAGE
func pageLabel()->some View {
self
.frame(maxWidth: .infinity, alignment: .center)
}
//PAGE
func pageView(ignoresSafeArea: Bool = false, edges: Edge.Set = [])->some View {
self
.frame(width: getScreenBounds().width, alignment: .center)
.ignoresSafeArea(ignoresSafeArea ? .container : .init(), edges: edges)
}
func getScreenBounds()->CGRect {
return UIScreen.main.bounds
}
}
struct OffsetPageTabView<Content: View>: UIViewRepresentable {
var content: Content
#Binding var offset: CGFloat
#Binding var selection: Int
var children: Int //<-- Here
func makeCoordinator() -> Coordinator {
return OffsetPageTabView.Coordinator(parent: self)
}
init(selection: Binding<Int>,offset: Binding<CGFloat>, children: Int, #ViewBuilder content: #escaping ()->Content) {
self.content = content()
self._offset = offset
self._selection = selection
self.children = children
}
func makeUIView(context: Context) -> UIScrollView {
let scrollview = UIScrollView()
let hostview = UIHostingController(rootView: content)
hostview.view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
hostview.view.topAnchor.constraint(equalTo: scrollview.topAnchor),
hostview.view.leadingAnchor.constraint(equalTo: scrollview.leadingAnchor),
hostview.view.trailingAnchor.constraint(equalTo: scrollview.trailingAnchor),
hostview.view.bottomAnchor.constraint(equalTo: scrollview.bottomAnchor),
hostview.view.heightAnchor.constraint(equalTo: scrollview.heightAnchor)
]
scrollview.addSubview(hostview.view)
scrollview.addConstraints(constraints)
scrollview.isPagingEnabled = true
scrollview.showsVerticalScrollIndicator = false
scrollview.showsHorizontalScrollIndicator = false
scrollview.delegate = context.coordinator
return scrollview
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
let currentOffset = uiView.contentOffset.x
if currentOffset != offset {
//print("updating")
uiView.setContentOffset(CGPoint(x: offset, y: 0), animated: true)
}
}
class Coordinator: NSObject, UIScrollViewDelegate {
var parent: OffsetPageTabView
init(parent: OffsetPageTabView) {
self.parent = parent
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.x
let maxSize = scrollView.contentSize.width
let currentSelection = (offset / maxSize) * CGFloat(parent.children) //<-- Here
print("max: ", maxSize, offset)
print("Set selection to: ", Int(currentSelection), currentSelection)
parent.selection = Int(currentSelection)
parent.offset = offset
}
}
}
struct ContentView: View {
#State private var location: CGPoint = CGPoint(x: 150, y: 100)
#GestureState private var startLocation: CGPoint? = nil
#State var windowSize = CGSize(width: 300, height: 200)
var dragGesture: some Gesture {
DragGesture().onChanged { value in
location = (startLocation ?? location) + value.translation
}.updating($startLocation) {
(_, startLocation, _) in
startLocation = startLocation ?? location
}
}
var body: some View {
VStack {
//Title
ZStack {
Color.green
Text("Title bar")
}.frame(height: 30)
.gesture(dragGesture) // (1)
//Contents
Spacer()
Text("contents")
Spacer()
}.background(Color.gray)
.frame(width: windowSize.width, height: windowSize.height)
.position(location) // (3)
//.gesture(dragGesture) // (2)
}
}
func +(lhs: CGPoint, rhs: CGSize) -> CGPoint {
return CGPoint(
x: lhs.x + rhs.width,
y: lhs.y + rhs.height
)
}
The dragging effect works if (1) is commented out and (2) uncommented. But I want to recognize drag gestures only on the title bar to move the whole View. What I tried is to add a gesture to the ZStack (1) modifying the state of the VStack (3).
I got a solution later.
ZStack {
TitleBar() //some view
.position(/*position for title bar*/)
.gesture(dragGesture)
Contents() //some view
.position(/*position for contents below, which can be a computed variable from TitleBar's (state) position*/)
}
iOS 14.4 + Xcode 12.4
I want to make a simple checklist in SwiftUI on iOS where the text for each item is a TextEditor.
First, I create the basic app structure and populate it with some demo content:
import SwiftUI
#main
struct TestApp: App {
#State var alpha = "Alpha"
#State var bravo = "Bravo is a really long one that should wrap to multiple lines."
#State var charlie = "Charlie"
init(){
//Remove the default background of the TextEditor/UITextView
UITextView.appearance().backgroundColor = .clear
}
var body: some Scene {
WindowGroup {
ScrollView{
VStack(spacing: 7){
TaskView(text: $alpha)
TaskView(text: $bravo)
TaskView(text: $charlie)
}
.padding(20)
}
.background(Color.gray)
}
}
}
Then each TaskView represents a task (the white box) in the list:
struct TaskView:View{
#Binding var text:String
var body: some View{
HStack(alignment:.top, spacing:8){
Button(action: {
print("Test")
}){
Circle()
.strokeBorder(Color.gray,lineWidth: 1)
.background(Circle().foregroundColor(Color.white))
.frame(width:22, height: 22)
}
.buttonStyle(PlainButtonStyle())
FieldView(name: $text)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(EdgeInsets(top:10, leading:10, bottom: 10, trailing: 30))
.background(Color.white)
.cornerRadius(5)
}
}
Then finally, each of the TextEditors is in a FieldView like this:
struct FieldView: View{
#Binding var name: String
var body: some View{
ZStack{
Text(name)
.padding(EdgeInsets(top: -7, leading: -3, bottom: -5, trailing: -3))
.opacity(0)
TextEditor(text: $name)
.fixedSize(horizontal: false, vertical: true)
.padding(EdgeInsets(top: -7, leading: -3, bottom: -5, trailing: -3))
}
}
}
As you can see in the screenshot above, the initial height of the TextEditor doesn't automatically size to fit the text. But as soon as I type in it, it resizes appropriately. Here's a video that shows that:
How can I get the view to have the correct initial height? Before I type in it, the TextEditor scrolls vertically so it seems to have the wrong intrinsic content size.
Note: views are left semi-transparent with borders so you can see/debug what's going on.
struct FieldView: View{
#Binding var name: String
#State private var textEditorHeight : CGFloat = 100
var body: some View{
ZStack(alignment: .topLeading) {
Text(name)
.background(GeometryReader {
Color.clear
.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
//.opacity(0)
.border(Color.pink)
.foregroundColor(Color.red)
TextEditor(text: $name)
.padding(EdgeInsets(top: -7, leading: -3, bottom: -5, trailing: -7))
.frame(height: textEditorHeight + 12)
.border(Color.green)
.opacity(0.4)
}
.onPreferenceChange(ViewHeightKey.self) { textEditorHeight = $0 }
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
print("Reporting height: \(value)")
}
}
First, I used a PreferenceKey to pass the height from the "invisible" text view back up the view hierarchy. Then, I set the height of the TextEditor frame with that value.
Note that the view is now aligned to topLeading -- in your initial example, the invisible text was center aligned.
One thing I'm not crazy about is the use of the edge insets -- these feel like magic numbers (well, they are...) and I'd rather have a solution without them that still kept the Text and TextEditor completely aligned. But, this works for now.
Update, using UIViewRepresentable with UITextView
This seems to work and avoid the scrolling problems:
struct TaskView:View{
#Binding var text:String
#State private var textHeight : CGFloat = 40
var body: some View{
HStack(alignment:.top, spacing:8){
Button(action: {
print("Test")
}){
Circle()
.strokeBorder(Color.gray,lineWidth: 1)
.background(Circle().foregroundColor(Color.white))
.frame(width:22, height: 22)
}
.buttonStyle(PlainButtonStyle())
FieldView(text: $text, heightToTransmit: $textHeight)
.frame(height: textHeight)
.border(Color.red)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(EdgeInsets(top:10, leading:10, bottom: 10, trailing: 30))
.background(Color.white)
.cornerRadius(5)
}
}
struct FieldView : UIViewRepresentable {
#Binding var text : String
#Binding var heightToTransmit: CGFloat
func makeUIView(context: Context) -> UIView {
let view = UIView()
let textView = UITextView(frame: .zero, textContainer: nil)
textView.delegate = context.coordinator
textView.backgroundColor = .yellow // visual debugging
textView.isScrollEnabled = false // causes expanding height
context.coordinator.textView = textView
textView.text = text
view.addSubview(textView)
// Auto Layout
textView.translatesAutoresizingMaskIntoConstraints = false
let safeArea = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
textView.topAnchor.constraint(equalTo: safeArea.topAnchor),
textView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor)
])
return view
}
func updateUIView(_ view: UIView, context: Context) {
context.coordinator.heightBinding = $heightToTransmit
context.coordinator.textBinding = $text
DispatchQueue.main.async {
context.coordinator.runSizing()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
class Coordinator : NSObject, UITextViewDelegate {
var textBinding : Binding<String>?
var heightBinding : Binding<CGFloat>?
var textView : UITextView?
func runSizing() {
guard let textView = textView else { return }
textView.sizeToFit()
self.textBinding?.wrappedValue = textView.text
self.heightBinding?.wrappedValue = textView.frame.size.height
}
func textViewDidChange(_ textView: UITextView) {
runSizing()
}
}
}
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")}
)
}
}