Coding Workshop 9 - Location Finder App Part 2

homepage

 

The next step is to make the query to the API and decode the resulting JSON.

LocationFinderView

  1. We will need to enter a postal or zipCode in this view so this means a TextField that has to be bound to some @State property so create this inside of the LocationFinderView.
  1. We want this TextField only to be visible to our users if the selectedCountry is not our static Country.none so before the Spacer() in the VStack, present this if clause.

  2. Before we present an action to fetch the result, let's provide the user with the possible range that they might enter. Remember, this is the range property of the Country object.

    1. Create a Text view using this property as the String.

    2. Below that, because we are still in a VStack, create another Text view that will provide the users with the text for what to enter.

      1. Set the .font to a size of .caption.
      2. Set the .foregroundColor to .secondary.
    3. Now create the TextField, using "Code" for the title and bind the text to $code.

      1. Set the .textFieldStyle to .roundedBorder.

      2. Apply a .frame with a width of 100.

         

getLocation Button

Below this, we can create a Button that will make the API request.

  1. Create a Button with the label "Get Location".

  2. Leave the action empty for now. We will need to create a function in our LocationService class that can make that request on our behalf.

  3. Apply a .buttonStyle of .borderedProminent.

  4. Ensure that the button action is .disabled whenever code.isEmpty meaning we have yet to enter a code.

    2023-01-16_13-41-20

LocationService

When we receive a result back from the API, we want to decode it to create an instance of our Location type object.

The issue is that there may or may not be any places in the array that match the code that we entered.

Also, what if there is more than one? If that is the case we will pick the first one and extract from it the placeName, state and the latitude and longitude so that we can present the place and state name in a couple of Text views and use the coordinates to present a map.

We could, in LocationService create 4 different @Published properties for each, or, to make it easier to read and deal with, within the LocationService class we can create a Struct that contains those 4 properties

LocationInfo

  1. Inside theLocationService class, create a new struct called LocationInfo.

  2. Create 4 properties representing the values that we described above.

    Note: Though all 4 of the values coming back in our JSON are strings, we want to use Double values for our longitude and latitude, so we will be able to plot the location on a map later.

  1. Now create a single, @Publised property called locationInfo that is an optional instance of LocationInfo.

fetchLocation function

Now we need to create a function that will make the call to the API to fetch the location. In order to make that call, we will need to know the baseURL for our API site and that is https://api.zippopotam.us.

  1. Before we create the function, create a constant for the baseURL at the top of the LocationServices class.

  2. The other two pieces of information we will need will be the country and the entered postal code. Both of these pieces of information will come from the entry in our LocationFinderView.

    1. The country will be the selectedCountry's code from the Picker and the postal code will be the value entered in the TextField and bound to code.
    2. The function will be making a call to an external service that may take some time to respond with an answer so we need to wait for the answer to come back so we will make this an asynchronous function by specifying async.
    3. Create an asynchronous function with those two parameters called fetchLocation
    4. This function is also being executed on a different thread so when we update our locationInfo property and it in turn updates our UI, this needs to be done on the Main queue or thread so we mark the function as being a @MainActor.

    Dealing with possible errors

    At this point, your uses may enter characters that are not compatible with a properly formed URL, so rather than crashing the app, we should probably handle errors by presenting an error warning somewhere on the screen. We will do this shortly in our UI by adding a conditional Text field in red, only if there has been an error.

    To deal with this, we can, in our LocationServices class, create another optional @Published property that we can update whenever there is an error. This means, it will start out as nil, but when we get an error, we can set it to a String of our choosing.

    fetchLocation function continued
    1. Back to our fetchLocation function then, we can continue by first building our request URL from the base, country code and requested postal code.

      1. If our users add some extra spaces after the code, we will have a problem so we want to remove any of those spaces so we can do that by grouping that combined string in parenthesis and use a trimmingCharacters modifier.

      2. This will trim the white space at the end, but what if the postal code has a space in the middle as you will see for at least one of the country ranges? Spaces in a URL must be replaced with %20so we can add another modifier to convert this for us.

      3. As soon as we added that last modifier, our urlString is now an optional value and we can't have that so we will need to do a guard check to unwrap it using a guard let ..... else {} block and in the else statement we can assign an appropriate line of text for our errorString.

      4. Before we leave this guard check, we can do one more thing within the check and that is to see if that urlString can create a valid URL. If it can't we can use the same errorString, so right before the else statement, we can do another check for this by adding another condition to the guard check by using a , and forming the URL from the urlString which could be optional so this let URL inherits the guard from the first part so this will unwrap it if it is a good URL or set our errorString if if is not and then return from our function without proceeding any further.

      5. Now that we have a valid url, we can execute a shared URLSession method to fetch the data from that url. This is an asynchronous function that may fail so we need to enclose the request in a do ... catch block.

        1. When we retrieve the data back, it will return a tuple containing data and a response. If it fails, we can catch the error and assign an appropriate error message to our errorString property.
        2. I also do not care about the response, only the data, so when we assign the results to the tuple, I can use let (data, _)
        3. When we call the URLSession.shared.data method from that url, we must specify that we try and await the result before we can assign our tuple returned values,
        4. If it fails, we catch the error by setting the errorString to an appropriate line of text.
      6. Now that we have the data, we can try to decode it using a JSONDecoder() decode function to a Location.self object and assign it to a temporary location object. If this fails to decode, it will be caught and the error will be the same as we already have in our catch block.

      7. If we made it this far, we have a valid location and it has a places array that may or may not have any places in it. If it does, we only want the first one so we can use an if let to wrap it since the first method will always produce an optional value.

      8. This gives us the information we need now to assign an instance to our @Published optional locationInfo property.

      The completed function is as followed:

Reset function

There is one more small function we need to create.

Each time we choose another country, we want to reset our errorString and locationInfo back to nil.

Now that we have this LocationService class pretty much completed, we can return to the LocationFinderView and make the call to our new functions when necessary.

LocationFinderView

Actions

Whenever we change our selection of the country, we want to reset our errorString and locationInfo properties.

  1. At the end of our view struct, after the navigationTitle, we can add a method that will watch for changes of the selectedCountry, This .onChange method will provide us with a selectedCountry value that we "could" use in our code but we won't need it so we can simply use _ instead of assigning it to a variable.

  2. In the body of the trailing closure however, we can call the locationService.reset() function AND we can reset our TextField by setting code to an empty string.

  3. To make the call to the API, we need to update the action for the "Get Location" function.

    1. Since this is going to make a call to an asynchronous function in our LocationService class, we must embed it in an asynchronous unit of work called a Task
    2. Then with the task we can await the result of making the call to the locationService.fetchLocation function passing in the selectedCountry.code for the country code and code for the postalCode.

Updating the View

We can use this information then to update our view to present the place name and state.

Presenting Errors

But first, if there were an error, we can present it.

  1. After the disabled modifier, present a Text view if there is a non nil locationService.errorString and since it is optional, we can check for that and unwrap it using an if let and assign it to a local variable called errorString.

    1. after the .disabled modifier perform the check.

    2. Create a Text view using our now unwrapped errorString.

    3. Set the .foregroundColor to .red.

Presenting the result
  1. After this, we can see if we have a locationInfo object. Remember it is optional so we can preform another if let check to unwrap it if it does and assign it to a local variable that we can call locationInfo.
  2. If it does exist, we can then access the placeName and state and present two Text views in our VStack.
Filling the View with an Image

One last thing before we test and before we add a map. When the screen loads or if we fail to generate a locationInfo object, we can display an image on the screen.

  1. Just before the Spacer(), create a conditional check to see if the locationService.locationInfo is still nil.

  2. Display an Image view using the locationFinder asset.

This can be tested now in the preview or in the simulator.

Swift Concurrency and fetching data from an API is something that you will be doing all the time. I have created a 6 part series on that topic and it is available on the Code With Chris YouTube Channel. https://youtube.com/playlist?list=PLMRqhzcHGw1a4jFHEBitPwCtAgPxWldfy

I have also created an Xcode Playground that you can use to test out not only APIs, but also the decoding of data that is returned. A full video on this topic is available on my Channel at https://youtu.be/-UUyo3bOlys

 

Back to Project Index