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:
To start working with maps you need to do two things only:
MapKit
framework into your project.
MKMapView
to your screen.
You can always read more on
MKMapView
on
Apple Developer Website