Map in SwiftUI (episode II°)

In this post, we’ll learn how to:

  • Add Markers
  • Customize Annotations
  • Add Map Buttons

Add Markers

Let’s see how to add a marker to both Berkeley and Stanford Universities.

First, we’ll create a structure for our locations:

struct Place: Identifiable {
    let id = UUID()
    var name: String
    var coordinates: CLLocationCoordinate2D = .init(latitude: 0.0, longitude: 0.0)
}

The struct implements the Identifiable protocol because we need to define an ID for each element. This is especially crucial as we want to use an array of places, and it’s necessary to uniquely identify an element when iterating over the array. The other fields of the struct should be self-explanatory.

Now, let’s see how to display these two places on the map. We’ll set the center of the map over San Mateo, CA, as it’s equidistant from both locations. Then, for each place, we’ll add a marker on the map:

struct ContentView: View {
    let locationManager = CLLocationManager()
    
    @State var position: MapCameraPosition = .region(MKCoordinateRegion(
        center: .init(latitude: 37.554169, longitude: -122.313057),
        span: .init(latitudeDelta: 0.7, longitudeDelta: 0.7)
    ))
    
    var places = [Place(name: "Berkeley", coordinates: CLLocationCoordinate2D(latitude: 37.8715, longitude: -122.2730)), Place(name: "Stanford", coordinates: CLLocationCoordinate2D(latitude: 37.4275, longitude: -122.1697))]

    var body: some View {
        ZStack{
            Map(position: $position) {
                ForEach(places) { place in
                    Marker(place.name, coordinate: place.coordinates)
                    .tint(.orange)
                }
            }
        }
    }
}

The marker requires two parameters: a string for identification and the coordinates. It’s also possible to change some properties, such as the color. In this case, we’ve set it to orange.

Customize Annotations

If we need a different marker or don’t like the default one, we can customize our own using annotations. Suppose we want to use an orange brain icon for the universities, we could create it as follows:

Annotation(place.name, coordinate: place.coordinates) {
                        Image(systemName: "brain.fill")
                            .resizable()
                            .frame(width: 50, height: 50)
                            .foregroundStyle(.orange)
                    }

Instead of an image, we can add whatever we want, such as a button, additional text, or a shape, and so on.

If we want to hide the default title, it’s possible by modifying the titles property:

Annotation(place.name, coordinate: place.coordinates) {
                        Image(systemName: "brain.fill")
                            .resizable()
                            .frame(width: 50, height: 50)
                            .foregroundStyle(.orange)
                    }.annotationTitles(.hidden)

Add Map Buttons

MapKit provides us the opportunity to use various map buttons, which we will explore in the next section:

  • MapUserLocationButton: Centers the map on the user’s current position.
  • MapPitchToggle: Switches between 2D and 3D views (note that this is not possible for all cities).
var body: some View {
        ZStack{
            Map(position: $position) {
                ForEach(places) { place in
                    Annotation(place.name, coordinate: place.coordinates) {
                        Image(systemName: "brain.fill")
                            .resizable()
                            .frame(width: 50, height: 50)
                            .foregroundStyle(.blue)
                    }.annotationTitles(.hidden)
                }
            }.mapControls {
                MapUserLocationButton()
                MapPitchToggle()
            }.mapStyle(.hybrid(elevation:.realistic))
        }
    }

I have also changed the map style to a hybrid (satellite) view and added elevation to the camera.

For 2D:

For 3D:

Map in SwiftUI (Episode I°)

This post is the first in a series about using Maps in SwiftUI. In this post, we’ll learn how to:

  • Create a Map
  • Show the user’s position
  • Add a circle centered on the user’s position

Map Creation

First, we need to add values in the info.plist for:

  • Privacy – Location Always Usage Description
  • Privacy – Location When In Use Usage Description

Now, let’s take a look at the code:

import SwiftUI
import MapKit

struct ContentView: View {
    @State var position: MapCameraPosition = .region(MKCoordinateRegion(
        center: .init(latitude: 47.499167,longitude: 8.726667),
        span: .init(latitudeDelta: 0.2, longitudeDelta: 0.2)
    ))
    
    var body: some View {
        ZStack{
            Map(position: $position) 
            .ignoresSafeArea(.all)
        }
    }
}

To use the map, we need to import MapKit, then define a position such as MapCameraPosition (with iOS 17, the direct use of the region is deprecated). The span represents the amount of map to display (think of it as zoom). (I used the coordinates of Winterthur in memory of Niklaus Wirth, who passed away a few days ago). The map is displayed in fullscreen mode because it ignores the safe area.

Show User Position

Now, take a look at how to create a map centered on the user’s position:

struct ContentView: View {
    let locationManager = CLLocationManager()

    @State var position: MapCameraPosition = .userLocation(fallback: .automatic)
    
    var body: some View {
        ZStack{
            Map(position: $position) {
                UserAnnotation()
            }
            .ignoresSafeArea(.all)           .onAppear{self.locationManager.requestWhenInUseAuthorization()
            }
        }
    }
}

First, we define a locationManager; it’s necessary to access the user’s position. When the map appears, it will request the user’s permission.

In this case, ‘position’ contains the user’s location (the fallback is a position to use if the map hasn’t resolved the person’s location).

Add a circle around the user position

struct ContentView: View {
    let locationManager = CLLocationManager()
    @State var distance: CLLocationDistance = 500.0
    @State var defaultLocation: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0)

    @State var position: MapCameraPosition = .userLocation(fallback: .automatic)
    
    var body: some View {
        ZStack{
            Map(position: $position) {
                MapCircle(center:  locationManager.location?.coordinate ?? defaultLocation, radius: distance)
                    .foregroundStyle(Color(white: 0.4, opacity: 0.25))
                    .stroke(.orange, lineWidth: 2)
                UserAnnotation()
            }
            .ignoresSafeArea(.all)
            .onAppear{self.locationManager.requestWhenInUseAuthorization()
            }
        }
    }
}

Here we’ve added a distance (in meters) that we use as the radius for the MapCircle. The MapCircle is centered using the coordinates of the user, obtained through the locationManager. In the image, you’ll also see a black circle; that’s Apple Park.

That’s all for this episode.

Horizontal Menu in SwiftUI

In this post, we’ll learn how to build a horizontal menu similar to the one used in the Medium mobile application, like this:

The code is:

struct MenuSwiftUIView: View {
    var days = ["Monday", "Tuesday", "Wednesday", "Thursday",
    "Friday", "Saturday", "Sunday"]
    @State var selected = 0
    var body: some View {
        VStack {
            ScrollView(.horizontal) {
                HStack(spacing: 0) {
                    ForEach(Array(days.enumerated()), id: \.offset) {  index, day in
                        VStack {
                            Text(day).padding(5)
                                .foregroundStyle(.white)
                            if selected == index {
                                Color(.white)
                                    .frame(height: 2)
                            } else {
                                Color(.gray)
                                    .frame(height: 1)
                            }
                                
                        }.onTapGesture {
                            selected = index
                        }
                    }
                }
            }.scrollIndicators(ScrollIndicatorVisibility.hidden)
            Spacer() // Here what you want show considering the selected value
        }.padding()
        .background(.black)
    }
}

We define an array of strings that contains the menu options and a variable selected to store the value of the selected index in the menu.

The menu is built using a ScrollView that scrolls horizontally. In this, we add the Text that shows the label and the bottom line.

The HStack has spacing set to zero to avoid space between the bottom lines.

To have the index of the elements, we create a collection of indices and elements with Array(days.enumerated())

[(offset: 0, element: "Monday"), (offset: 1, element: "Tuesday"), (offset: 2, element: "Wednesday"), (offset: 3, element: "Thursday"), (offset: 4, element: "Friday"), (offset: 5, element: "Saturday"), (offset: 6, element: "Sunday")]

We hide the scrollbar in this way:

.scrollIndicators(ScrollIndicatorVisibility.hidden)

The line (indicator) changes color depending on the selected item.

For the last step, we can extract the ScrollView to create a reusable component:

struct HorizontalMenu: View {
    var labels: Array<String> = []
    var defaultColor = Color.gray
    var selectedColor = Color.white
    var textColor = Color.white
    
    @Binding var selected: Int
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack(spacing: 0) {
                ForEach(Array(labels.enumerated()), id: \.offset) {  index, label in
                    VStack {
                        Text(label).padding(5)
                            .foregroundStyle(textColor)
                        if selected == index {
                            selectedColor
                                .frame(height: 2)
                        } else {
                            defaultColor
                                .frame(height: 1)
                        }
                        
                    }.onTapGesture {
                        selected = index
                    }
                }
            }
        }.scrollIndicators(ScrollIndicatorVisibility.hidden)
    }
}

Note that we bind the selected variable because we need to use its value in the main view.

Thus, the code becomes:

struct MenuSwiftUIView: View {
    var days = ["Monday", "Tuesday", "Wednesday", "Thursday",
    "Friday", "Saturday", "Sunday"]
    @State var selected = 0
    var body: some View {
        VStack {
            HorizontalMenu(labels: days, selected: $selected)
            Spacer() // Here what you want show considering the selected value
        }.padding()
        .background(.black)
    }
}

(Blog image from unsplash)

Segmented Control in SwiftUI

In this post we’ll learn:

  • How to create a segment control.
  • How to customize the look and feel of a segment control.

Create the segment control

Let’s start by creating a segment control that allows choosing from the days of the week:

struct SegUIView: View {
    @State private var daySelected = 0
    var body: some View {
        VStack {
            Picker("Choose a day", selection: $daySelected) {
                Text("Mo").tag(0)
                Text("Tu").tag(1)
                Text("We").tag(2)
                Text("Th").tag(3)
                Text("Fr").tag(4)
                Text("Sa").tag(5)
                Text("Su").tag(6)
            }
            .pickerStyle(.segmented)
            Text("Selected the day \(daySelected)")
            Spacer()
        }.padding()
    }
}

So, the Segment Control is created by using a Picker and setting its style to segmented. If you remove this style, the default Picker appears as a contextual menu.

The state variable daySelected stores the tag value of the selected item. We then simply display this value in a Text.

Thus we have:

Thus, upon selecting a day, we see the number corresponding to daySelected (the tag number).

To create something more realistic, we can create a function that returns a view for the different daySelected:

@ViewBuilder
func DayView(day: Int) -> some View {
    switch (day) {
    case 0:
        Text("Monday")
    case 1:
        Text("Tuesday")
    case 2:
        Text("Wednesday")
    case 3:
        Text("Thursday")
    case 4:
        Text("Friday")
    case 5:
        Text("Saturday")
    case 6:
        Text("Sunday")
    default:
        Text("")
    }
    
}

By replacing the code in the body, we can update the view dynamically based on the daySelected. Here’s how it’s done:

var body: some View {
        VStack {
            Picker("Choose a day", selection: $daySelected) {
                Text("Mo").tag(0)
                Text("Tu").tag(1)
                Text("We").tag(2)
                Text("Th").tag(3)
                Text("Fr").tag(4)
                Text("Sa").tag(5)
                Text("Su").tag(6)
            }
            .pickerStyle(.segmented)
            DayView(day: daySelected)
            Spacer()
        }.padding()
    }

Customize the Segment Control

The background of the Picker can be changed by using its background property:

var body: some View {
        VStack {
            Picker("Choose a day", selection: $daySelected) {
                Text("Mo").tag(0)
                Text("Tu").tag(1)
                Text("We").tag(2)
                Text("Th").tag(3)
                Text("Fr").tag(4)
                Text("Sa").tag(5)
                Text("Su").tag(6)
            }
            .pickerStyle(.segmented)
            .background(.yellow)
            DayView(day: daySelected)
            Spacer()
        }.padding()
    }

Instead, to change the properties of the text and the background color of the selected element, we need to use code from UIKit. Therefore, we define an init function in this way:

init() {
    // Color for the selected item
    UISegmentedControl.appearance().selectedSegmentTintColor = UIColor.orange
    
    // The text color for the selected item
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
}

So we have:

If we want to remove the separator between the days, we can add this code to the init function:

UISegmentedControl.appearance().setDividerImage(UIImage(), forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default)

To change the text color of the unselected items, add the following code:

UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.magenta], for: .normal)

The code https://github.com/niqt/swift/tree/master/SegmentController

Playing music in SwiftUI application

In this short post, we’ll learn how to add the play function to our SwiftUI application. To test this functionality, we will add a Christmas song to our previous project (timer-and-shape-in-swiftui).

First, we need to create a player. We’ll do this by using the new Observable class annotation, which also works in the latest iOS 17. Some might wonder why I’m focusing on the latest features. The goal of my post is to update current iOS developers on the latest advancements and to help new developers, who usually start by using the most recent tools, create code effectively.

So, let’s take a look at the player:

import AVFoundation

@Observable
class AudioPlayerViewModel {
  var audioPlayer: AVAudioPlayer?

  var isPlaying = false

  init() {
    if let sound = Bundle.main.path(forResource: "silent", ofType: "mp3") {
      do {
        self.audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: sound))
      } catch {
        print("AVAudioPlayer can't be instantiated.")
      }
    } else {
      print("Audio file not found.")
    }
  }

  func playOrPause() {
    guard let player = audioPlayer else { return }

    if player.isPlaying {
      player.pause()
      isPlaying = false
    } else {
      player.play()
      isPlaying = true
    }
  }
}

First, we import the AVFoundation framework, as described in the official documentation at https://developer.apple.com/av-foundation/. As we can read there: “AVFoundation is the full featured framework for working with time-based audiovisual media on iOS, macOS, watchOS and tvOS. Using AVFoundation, you can easily play, create, and edit QuickTime movies and MPEG-4 files, play HLS streams, and build powerful media functionality into your apps”.

In the class, the player is defined as an optional, because it could be null for some reason. After that, we retrieve the path to the file we want to play and pass it to the player’s constructor. If something goes wrong, we’ll see an error message.

The method playOrPause, plays or stops the music depending the current playing state.

Regarding the audio file, it has not been added to the assets, but rather to the project structure.

Now add the player to the code:

.
.
@State var audioPlayerViewModel = AudioPlayerViewModel()
    
    var body: some View {
        ZStack {
            Color.black.frame(maxWidth: .infinity, maxHeight: .infinity).opacity(0.9).edgesIgnoringSafeArea(.all)
            VStack(spacing: 0) {
                Image(systemName: stars[indexStar])
                    .resizable()
                    .frame(width: 100, height: 100)
                    .foregroundStyle(.yellow)
                    .offset(y: 20)
                TriangleWidthDecoration(triangleWidth: 260, leftColorDecoration: currentColors[3], rightColorDecoration: currentColors[7])
                
            }.offset(y: -50)
            ForEach(Array(widths.enumerated()), id: \.offset) { index, leaf in
                TriangleWidthDecoration(triangleWidth: Double(leaf), leftColorDecoration: currentColors[index], rightColorDecoration: currentColors[index * 2])
                    .offset(y: 60 * Double( index + 1))
            }
            
            Rectangle()
                .frame(width: 20, height: 60)
                .foregroundStyle(.brown)
                .offset(y: 360)
        }.onReceive(timer) { _ in
            shuffleColor()
            self.indexStar = (self.indexStar + 1) % 2
        }.onAppear {
            audioPlayerViewModel.playOrPause()
        }.onTapGesture {
            audioPlayerViewModel.playOrPause()
        }
    }
    
    func shuffleColor() {
        for i in 0..<currentColors.count {
            currentColors[i] = colors[Int.random(in: 0..<9)]
        }
    }
}

The music starts when the tree appears. You can play or stop it by tapping on the tree.

You can obtain a free MP3 file from this source: https://www.freemusicpublicdomain.com/royalty-free-christmas-music/

The code is here

https://github.com/niqt/swift/tree/master/ChristmasTree

That’s all.

Note: English is not my native language, so I’m sorry for some errors. I appreciate if your correct me.