Map Views

In this chapter, we are going to add a Map View to the Tutorial App.

First thing lets update table data in our primary Table View - we need to add Map View cell that will open MapScreen:

        def set_table_data
          @data = [
            {
              cells: [
                {
                  title: "Collection Views",
                }, {
                  title: "Map Views",
                }
              ]
            }
          ]
        end

        # ... other HomeScreen code ...

        def tableView(table, didSelectRowAtIndexPath: index)
          if index.row == 0
            open CollectionScreen.new
          elsif index.row == 1
            open MapScreen.new
          end
        end
      
iOS has some frameworks that included by default like UIKit or Foundation. MapKit is not one of them, so we have to add it explicitly to the project. If we don't do it, the app won't know how to work with maps. We can include them in the Rakefile:
        app.frameworks += [ 'MapKit' ]
      

Now we can start working on a Map Screen and its Layout. Let's start with a screen. For now it will just render our layout:

        class MapScreen < PM::Screen
          title "Map"

          def load_view
            @layout = MapScreenLayout.new
            self.view = @layout.view
          end

          def on_load
            self.view.backgroundColor = 0xffffff.uicolor
          end
        end
      
The layout should have a full-screen map view, which is MKMapView, and a text field, which is UITextField. We will use a text field for a search queries - user writes some address, and we will look for it using MapKit. So here is the MapScreenLayout:
        class MapScreenLayout < MotionKit::Layout
          def layout
            add MKMapView, :map
            add UITextField, :search
          end

          def map_style
            constraints do
              x 0
              y 0
              right.equals(:superview, :right)
              bottom.equals(:superview, :bottom)
            end
          end

          def search_style
            background_color 0xffffff.uicolor
            placeholder "Search..."

            # add some space left:
            left_view UIView.alloc.initWithFrame [[0, 0], [10, 10]]
            left_view_mode UITextFieldViewModeAlways

            layer do
              corner_radius 5
              border_color 0xc4c4c4.cgcolor
              border_width 1
            end

            constraints do
              top.equals(:superview, :top).plus(70)
              left.equals(:superview, :left).plus(20)
              right.equals(:superview, :right).minus(20)
              height(38)
            end
          end
        end
      
You can find new layer block in search's styles. The layer is a UIView's property that is responsible for the visual content of the view. You can specify things like border color or width, corner radius, shadow and other stuff right inside layer block. If you are outside of Layout, you can use it like this as well:
        some_view.layer.borderWidth = 5
      
We also had to add some more space on the left side of the view with the left_view method. Usually, developers use it to add some picture in the view, but in this example, we needed it to add some padding to the text field.

At this point Tutorial App should show something like this:

Looks great! Let's now make our search bar work: each time user writes something in the text field and presses the Search button, we should start looking for a provided address. This is very easy to do. First we need to add UITextfield delegate method that will be called each time user presses Search/Enter/Return button on his keyboard: textFieldShouldReturn(text_field). Then we will get a text in the text field, and we will provide it to Geocoder. When Geocoder returns us some results, we will add pins for each result location, and will move the map there. But first, let's change text field's Enter button to a Search button:

        class MapScreenLayout < MotionKit::Layout

          # other MapScreenLayout code ...

          def search_style
            background_color 0xffffff.uicolor
            placeholder "Search..."
            return_key_type UIReturnKeySearch

            # other search_style code ...
          end

          # other MapScreenLayout code ...

        end
      
Now we can configure the screen:
        class MapScreen < PM::Screen
          # other MapScreen code ...

          def on_load
            self.view.backgroundColor = 0xffffff.uicolor

            @map = @layout.get(:map)

            # tell textfield where delegate methods can be found:
            @layout.get(:search).delegate = self

            # we need to store placemarks somewhere. We will use
            # this array to remove old placemarks each time we show
            # new ones:
            @placemarks = []
          end

          def textFieldShouldReturn(text_field)
            search_query = text_field.text

            # start looking for adress if it is not empty:
            if !search_query.empty?
              search_address search_query
            end

            # make text_field not-active (hiding the keyboard)
            text_field.resignFirstResponder

            true
          end

          def search_address(address)
            # create geocoder instance:
            geocoder = CLGeocoder.new
            geocoder.geocodeAddressString(address, completionHandler: lambda do |placemarks, error|
              # print in console that geocoder has finished his job:
              NSLog "Found #{placemarks.count}"
              NSLog "Has error? #{error}"

              if error
                # show an error, and stop this method:
                UIAlertController.alert(self, "Error", message: "#{error.localizedDescription}")
                return
              end

              # remove old placemarks from the map
              @placemarks.each do |old_placemark|
                @map.removeAnnotation old_placemark
              end

              # don't continue if there were not addresses found:
              return if placemarks.empty?

              # remove any previous annotations on the map:
              @placemarks = []
              placemarks.each do |placemark|
                address_placemark = MKPlacemark.alloc.initWithPlacemark(placemark)
                @map.addAnnotation address_placemark
                @placemarks << address_placemark
              end

              # move map to the first address
              region = @map.region
              region.center = @placemarks.first.region.center
              @map.setRegion region, animated: true
            end)
          end
        end
      
First we saved the map object into the @map variable to access it in the future. Then we told text field where to look for delegate methods. If we would not do @layout.get(:search).delegate = self, our searching for a location would never start - the app will not know that something should happen each time user presses "Search" button on a keyboard.

@placemarks array is used to store placemarks (annotations) added to the map. In the beginning, it is empty since the map has no annotations added. Later we will add placemarks there when we search for something. With every new search, we first remove previous placemarks from the map, and only then we create new ones.

textFieldShouldReturn(text_field) is used to get a search query, and start the search with our search_address method. According to Apple's docs, we should always return `true` in textFieldShouldReturn(text_field) method, so the app will know whether it can process the pressing of the button.

In search_address we create new Geocoder object that will help us to look for a provided address. In our example we use Apple's geocoder, but you can always change it to whatever you want, for example Google Maps' Geocoder. You can see that we pass it an address and completion block. Completion block will be fired when geocoder will finish its job. It will pass us an array of found addresses and an error. If the error exists, we should show alert view, and stop method execution. Otherwise we remove previous placemarks and create new ones. Later we also move the center of the map to the first found address. When you start the app you should see something like this:

Summary

To start working with maps you need to do two things only:

These two basic actions will show the map to users, and then you can start customizing it to your needs.

You can always read more on MKMapView on Apple Developer Website

Book Index | Next