The first video in our iOS 101 series. Learn the basics of using Xcode to create a simple iPhone app.
Video Tutorial: Hello, iPhone! is a post from: Ray Wenderlich
The post Video Tutorial: Hello, iPhone! appeared first on Ray Wenderlich.
The first video in our iOS 101 series. Learn the basics of using Xcode to create a simple iPhone app.
Video Tutorial: Hello, iPhone! is a post from: Ray Wenderlich
The post Video Tutorial: Hello, iPhone! appeared first on Ray Wenderlich.
Learn about the most common UIKit controls in iOS development, like text fields, sliders, segmented controls, and more.
Video Tutorial: Common UIKit Controls is a post from: Ray Wenderlich
The post Video Tutorial: Common UIKit Controls appeared first on Ray Wenderlich.
Learn the basics of using Auto Layout to make your user interfaces adapt to different screen sizes and orientations.
Video Tutorial: Beginning Auto Layout is a post from: Ray Wenderlich
The post Video Tutorial: Beginning Auto Layout appeared first on Ray Wenderlich.
Learn how to transition between different "screens" of your app with Storyboards and segues.
Video Tutorial: Storyboards and Segues is a post from: Ray Wenderlich
The post Video Tutorial: Storyboards and Segues appeared first on Ray Wenderlich.
We're excited to introduce a brand new feature of raywenderlich.com - high quality video tutorials!
Introducing raywenderlich.com Video Tutorials (Beta)! is a post from: Ray Wenderlich
The post Introducing raywenderlich.com Video Tutorials (Beta)! appeared first on Ray Wenderlich.
Learn the basics of how to make your first Android app and learn the ins and outs of working with Android Studio.
Make Your First Android App: Part 1/3 is a post from: Ray Wenderlich
The post Make Your First Android App: Part 1/3 appeared first on Ray Wenderlich.
Learn how to easily manage a stack of view controllers in your app using navigation controllers.
Video Tutorial: Navigation Controllers is a post from: Ray Wenderlich
The post Video Tutorial: Navigation Controllers appeared first on Ray Wenderlich.
This tutorial is the second of three parts. If you’re looking to start from scratch, Part One is the tutorial for you!
The first part of this series covered a lot of zoomed-out Android concepts, as well as the general structure of Android projects. In this part of the tutorial, you’ll learn more “on the ground” Android: how a layout file works and the specifics of Android layouts and views.
By the time you’re done with this section, you’ll have an app with:
You should already be at the point where you have a “Hello World” app running on your emulator or device. At the end of the last section, you changed the text so that it greets you personally, like mine:
It’s great that you’ve come this far — but now it’s time to take it to the next level! There’s a lot to do, so let’s get to it!
Looking ahead, the first thing you should do is check that you’re making your app as simple as possible. You don’t want to introduce additional complexity unless it’s needed since extra complexity in how something is implemented means that it takes more time and requires more work if you were to later modify the bits with the extra complexity.
The project that Android Studio creates by default gives you a structure that works well for a lot of apps. It uses a Fragment
. Fragments are like sub-activities. They contain UI elements that could either take up the whole screen or just a portion of it. They’re controlled by their parent Activity
and can be swapped out for another Fragment
quite easily.
But for your first app, the additional complexity of a Fragment
isn’t really needed. So, you’ll remove this default Fragment
setup.
First, open MainActivity.java and find the following section inside onCreate
:
if (savedInstanceState == null) { getFragmentManager().beginTransaction() .add(R.id.container, new PlaceholderFragment()) .commit(); } |
Select and delete that bit of code.
Second, scroll to the bottom of the file and find the section defining the PlaceholderFragment
class:
/** * A placeholder fragment containing a simple view. */ public static class PlaceholderFragment extends Fragment { public PlaceholderFragment() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main, container, false); return rootView; } } |
Carefully select and delete the whole class definition, making sure that the final closing bracket for MainActivity
is not removed.
Next, open res/layout/fragment_main.xml. After opening the file, you may have to switch the editor to Text mode if you can’t see the raw XML. Click the appropriate tab at the bottom of the editor pane as shown below.
Select all the text inside fragment_main.xml and copy it. Then, open res/layout/activity_main.xml and replace its contents with the former contents of fragment_main.xml.
There’s Just a bit of clean up left to do. Remove this line:
xmlns:tools=http://schemas.android.com/tools |
And this one:
tools:context=".MainActivity$PlaceholderFragment" |
Note: The above line might not look exactly like that. It might have the .MainActivity$PlaceholderFragment
bit prefixed by the package name you set for your project. So don’t be confused if you the line doesn’t look exactly the same as the one above :] Also, do not remove the closing angle bracket if the above line is on the last line of that particular section.
And these four lines which set the padding:
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" |
After all of these steps, your activity_main.xml should look something like this:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:text="@string/hello_world" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </RelativeLayout> |
Now, right-click on fragment_main.xml on the left pane of Android Studio and select the Delete… option from the context menu.
Now that you’ve finished that bit of cleaning up, you’re all set to work with a simple Activity
. And it’s time to learn how to make a layout for one!
Android layouts are in XML format, in the form of a tree with a single root and a hierarchy of views. The hierarchy is very strict and straightforward: each view in the tree is termed the parent of the views it contains and the child of the view that contains it.
Open res/layout/activity_main.xml. You can see the XML for your activity here. There is a parent RelativeLayout
and a child TextView
.
Look at the TextView
specifically. You can see that it contains three attributes. Of those, two are present in every view you’ll ever put into your Android layouts: layout_width
and layout_height
. The values for these attributes can take several forms:
wrap_content
: This constant value specifies that the view will be just large enough to fit whatever is inside it, whether that’s an image, text or child view.match_parent
: This constant sets the view to be as big as its parent.5px
), but it is usually wiser to use density independent pixels (Ex: 5dp
). A dp
is a pixel on a “medium-density” (mdpi
) device, and the number of actual pixels automatically scales for devices designated as low-density (ldpi
), high-density (hdpi
), extra-high-density (xhdpi
), etc.In other words, using straight-up pixels would result in your views being all sorts of crazy sizes, depending on whether a device has 160 pixels per inch or 300 pixels per inch, or what have you. Who knows! Let the Android system take care of the scaling and just use dp
.
Note: Designations like mdpi
and hdpi
are only general categories. Actual pixel densities are even more variable, but they are all given the same scaling factor regardless. So dp
scaling, while convenient, is not an exact science.
iOS Developers should be familiar with a similar practice of density independence, using “points” instead of pixels in their layouts to account for early iPhone screens not having Retina displays.
The final attribute of the TextView
is simply text
, in which you specify the text to be displayed. This attribute is a good example of how different views respond to different attributes. Adding a text
attribute to a RelativeLayout
or a Space
wouldn’t accomplish anything because, unlike the TextView
, they wouldn’t know what to do with it.
But the value of the attribute, @string/hello_world
, isn’t what’s displaying, is it? What you specify in your layout file is not the actual string to be displayed but rather a string resource ID identifying the actual text. That way, all your app’s copy can be in one place – res/values/strings.xml.
Now let’s look at the parent node in the XML: RelativeLayout
. What’s going on there?
The layouts in iOS apps used to be in purely absolute terms, like: “Place View X at pixels (x,y)”, but now iOS developers have AutoLayout. Android developers have always needed to keep device screen sizes in mind. Layout files are very well-suited for this consideration.
The default project Studio created for you sets you up with a useful layout: a RelativeLayout
. It is currently the parent layout and the TextView
element is its child.
A RelativeLayout is an intuitive and powerful thing. It holds a bunch of child views and positions them in relation to each other. Here are three examples of what you can easily do with a RelativeLayout
:
Example 1: Use layout_alignParentBottom
and the similar attributes for top, left and right to line up a view’s edge with the corresponding edge of the RelativeLayout
, which may or may not also be the edge of the screen.
Example 2: You can use layout_toRightOf
and the analogous attributes for left, above and below to position one View
relative to another.
Example 3: You can use layout_alignRight
and the analogous attributes to align a side of one View
with another.
You can see how that could be useful! For now, though, let’s move on to the layout type you’ll be using for this tutorial.
A LinearLayout needs to have an orientation specified, either horizontal
or vertical
. Then it lines up its children in that orientation, in the order in which they are specified in your XML.
The children of LinearLayouts
don’t respond to attributes like layout_toRightOf
, but they do respond to two other attributes: layout_weight
and layout_gravity
.
Specifying a layout_weight
expands the view to a proportion of its parent so that the parent weight is the sum of all child view weights.
The layout_weight
of View
X
———————————————————————-
The sum of all weights of View
X and its siblings
Confused? Perhaps the following image might help explain it better.
Notice how the full height of the parent view is split up between the child views based on the layout weight assigned to each child view.
Assigning a layout_gravity
to a view sets its horizontal and vertical positions within its parent LinearLayout
. For example, a view might have a layout_gravity
attribute with a value like left
, right
, and center_vertical
. The previous values can also be combined, like this: top|center_horizontal
.
Then, there’s gravity
, not to be confused with layout_gravity
. layout_gravity
is about where you place the view itself. The gravity
attribute defines how you place the content of a view within itself. If you want your text to be left or center justified, use gravity
.
The following example shows how layout_gravity
and gravity
work in a vertical LinearLayout
. Note that the top three have a layout_width
of wrap_content
while for the bottom three it’s set to match_parent
:
One handy trick, which you’ll see in just a bit, is that you can nest layouts inside each other. Cue the theme music from Inception!
You don’t want your layouts to be as multi-layered as a Christopher Nolan movie, though. So, if you start to see your nested LinearLayout
s scheme getting out of hand, consider switching to a RelativeLayout
.
Views
— to do a tiny calculation to put the view together.
Now, modern devices won’t have any trouble rendering your basic layouts. But if you were to make an indefinitely long ListView
— for instance, one full of cells making copious use of nested LinearLayout
s and layout_weight
s — all those extra tiny calculations could add up.
Before you move on to the next section, open res/layout/activity_main.xml and change the root node from a RelativeLayout
— what Android Studio gave you as a default — to a LinearLayout
.
To do that, you should replace these lines:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> |
And this one at the very end of the file:
</RelativeLayout> |
With This:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> |
And this:
</LinearLayout> |
Layouts are primarily the domain of your XML. But there are plenty of visual elements you will want to create, destroy, change, and trigger from within your Java code!
So first, edit the TextView
in activity_main.xml to match the following:
<TextView android:id="@+id/main_textview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginTop="20dp" android:text="@string/hello_world"/> |
Notice the addition of the id
attribute. Using this tag (or attribute, if you prefer) allows you to access that specific View
from within your code, so you can thereafter manipulate the View
via code.
There's also a change to make in the text
tag. The name of the string resource hello_world
is a bit outdated now, don't you think? Right-click on the @string/hello_world part of the line and then choose Refactor > Rename.
Then, type in textview
and click Refactor.
This not only changes the name of the resource ID in your layout file, it also changes the original resource ID in your strings.xml
file. It also renames the resource ID wherever else it might be used in the project. This is a useful trick to remember when renaming something that appears all over your project!
Now open MainActivity.java and add the following line above onCreate
:
TextView mainTextView; |
Android Studio might throw an error at you. The TextView
class hasn't been imported into MainActivity.java yet. Android Studio can quickly auto-import files for you. Just tap Option-Enter on your keyboard to automatically import TextView
.
Next, add the following code to onCreate
after the two existing lines of code:
// 1. Access the TextView defined in layout XML // and then set its text mainTextView = (TextView) findViewById(R.id.main_textview); mainTextView.setText("Set in Java!"); |
Finally, run your app and look at the results!
The text set via Java code now appears on screen. What are the steps you just took to make that happen?
id
attribute to the View
in XML.id
to access the View
via your code.View
to change its text value.Note: You added the code to access and set the text of your TextView
in the onCreate
method of your Activity
, meaning that the app runs all the code in that block right away when it first creates the Activity
.
It’s time to build on your TextView
and get more interactive! Next up is a Button
.
Add a Button
to activity_main.xml, directly after your TextView
:
<!-- Set OnClickListener to trigger results when pressed --> <Button android:id="@+id/main_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_marginLeft="20dp" android:text="@string/button" /> |
Notice there's an XML comment above the Button
, a reminder of how to trigger results.
The layout_margin
attributes simply add 20 density-independent pixels of space above and to the left of the Button
to keep your layout from looking cramped. Remember that the value of 20 will be scaled by the screen density of the device to get an actual pixel value.
Then there's text
, for which you need to add a line in strings.xml such as the following:
<string name="button">Update The TextView</string> |
Note: Till you add the matching resource ID to strings.xml
, Android Studio will keep warning you that the resource ID in activity_main.xml
is invalid by displaying it in red.
Next, open MainActivity.java
and add the following right below the previous line you added for a TextView
variable:
Button mainButton; |
Make sure you Option-Enter to import any missing classes.
Now add the following code to the end of onCreate
, after the code you added earlier:
// 2. Access the Button defined in layout XML // and listen for it here mainButton = (Button) findViewById(R.id.main_button); mainButton.setOnClickListener(this); |
Again, you see the same three steps as when you added code to access the TextView
:
id
to the View
in XML. Or, in this case, you add a view with an id
attribute.View
in code by using the id
.View
.This time, the method you called on the Button
is setOnClickListener
. What you put in the parentheses of that method becomes the answer to this question: Which Object
is going to respond when this Button
gets pressed?
To answer that question with simply the word this
seems a little curt and unspecific, but Java knows that it means MainActivity
itself is your intended listener.
This means that MainActivity
has to implement the View.OnClickListener
interface. If that sentence doesn't make much sense to you, I suggest finding an intro on what an interface is and how to create one, like this one.
If have an iOS/Objective-C background, an interface is comparable to a protocol. In fact, in object oriented programming the words protocol and interface are used interchangeably.
Android Studio is smart and can help you do the implementation. Simply single-click on this
, which is underlined in red, indicating an issue (in this case the fact that MainActivity
currently does not support the necessary interface). Then, when a red light bulb appears at the beginning of the line, click on it and select Make 'MainActivity' implement 'android.view.View.OnClickListener'.
Simply click OK on the next dialog, which lets you know which method(s) Studio will automatically create for you.
Studio then generates the code necessary to make your MainActivity
qualify as a union-certified OnClickListener
.
First, it added a bit to the class declaration indicating that the Activity
implements a specific interface:
public class MainActivity extends Activity implements View.OnClickListener |
Second, Studio added a stub for a method you need to implement in order to get your OnClickListener
license (other interfaces may require more than one method to be implemented): onClick
. This method fires when your Button
gets pressed.
The method currently does nothing. So add the following code to onClick
to make it do something:
// Test the Button mainTextView.setText("Button pressed!"); |
Can you tell from the code what should happen? Run your app and see if you're right...
The app now changes the text in the TextView
when you press the Button
. Cool! You'll be putting this Button
to even better use later — to submit input.
It's always fun to include images in your UI. So how about adding an ImageView
to show a little icon? Along the way, you'll also get to see how a nested LinearLayout
works.
First off, what image will you show? Well, it's easiest to start with the image you're given by default. It's already in your project and here's where to find it.
Use the Project Navigator to expand the src/main/res
directory:
You can see several directories within res
, including values
, which you dealt with when you edited strings.xml. Notice that several are named drawable
, plus some letters that look like screen density abbreviations.
Indeed, those abbreviations correspond to the pixel density buckets used to classify Android devices in dots per inch (dpi):
mdpi:
mediumhdpi:
highxhdpi:
extra highThere are now xxhdpi
and xxxhdpi
buckets with even higher densities. Personally, I think they should use Roman numerals for the next level up and call it xlhdpi
, but on second thought that would probably be a terribly confusing way to go...
Look inside the drawable
directories. You'll see a file named ic_launcher.png. This is simply the default launch image you're given, at several different sizes for different screens. The Android system will pick the right one for the device.
Now head back to activity_main.xml and replace the following section:
<!-- Set OnClickListener to trigger results when pressed --> <Button android:id="@+id/main_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_marginLeft="20dp" android:text="@string/button" /> |
With this:
<!-- This nested layout contains views of its own --> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal"> <!-- Set OnClickListener to trigger results when pressed --> <Button android:id="@+id/main_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_marginLeft="20dp" android:text="@string/button" /> <!-- Shows an image from your drawable resources --> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_marginLeft="20dp" android:src="@drawable/ic_launcher" /> <!-- Closing tag for the horizontal nested layout --> </LinearLayout> |
You added a new LinearLayout
inside the existing root layout, directly underneath the TextView
as its new sibling. You also moved the existing Button
into the nested layout and added a new ImageView
, as well.
By wrapping your Button
in a second, horizontal LinearLayout
, you are able to place a Button
and an ImageView
side-by-side horizontally, even as the root layout has a vertical orientation.
As for the ImageView
itself, the important attribute is src
, to which you give your drawable image resource. Note the format you use to reference the drawable image.
Run the app, and you'll see the new image right beside the button!
Now it's time to get some user input... by introducing an EditText
. This is a special subclass of TextView
that opens the keyboard and displays what the user types as its content.
Add the EditText
XML to activity_main.xml as a sibling to the TextView
and the horizontal LinearLayout
. Be careful not to get it caught inside the nested layout! Instead, add it right after the closing for the embedded linear layout.
<!-- Displays keyboard when touched --> <EditText android:id="@+id/main_edittext" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_marginLeft="20dp" android:hint="@string/hint" /> |
Notice the special attribute, hint
. You’re using this text as a placeholder in the input field. The app will overwrite it once the user starts typing.
As usual, you need to define the string resource for your hint in res/values/strings.xml:
<string name="hint">A Name</string> |
Now open MainActivity.java and add a new variable for the EditText
(below the other two existing variables):
EditText mainEditText; |
As before, remember to use Option-Enter to automatically add the missing imports.
Next, add the following code to the end of onCreate
:
// 3. Access the EditText defined in layout XML mainEditText = (EditText) findViewById(R.id.main_edittext); |
The above code, similar to the previous code, simply gets a reference to the EditText
control and saves it in the assigned variable.
Now that you have a reference to the EditText
control, you need to do something with user input. Replace the current contents of onClick
with the following:
// Take what was typed into the EditText // and use in TextView mainTextView.setText(mainEditText.getText().toString() + " is learning Android development!"); |
Run your app, and try typing a few things!
Now you receive user input with an EditText
, submit it with a Button
, and display it in a TextView
. Very nice! But how about visualizing more than one piece of data at a time?
The ListView
is a useful control that visualizes a list of items. It's analogous to a UITableView
in iOS.
You define a ListView
just as you would any other view in your XML. Add one to activity_main.xml as a sibling to the TextView
, the horizontal LinearLayout
, and the EditText
by adding the following lines after the lines for the EditText
control:
<!-- List whose dataset is defined in code with an adapter --> <ListView android:id="@+id/main_listview" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:layout_marginTop="20dp" /> |
Wait... what? How in the world could setting layout_height
to 0dp
be a good idea? After all, no matter what screen you're on, 0
is always going to scale to 0
.
Well, take a look at what directly follows: a layout_weight
. Since you haven’t given anything else in your layout a weight yet, the ListView
is going to expand to fill as much space as possible, no matter what value you give the layout_height
.
The general practice, then, is to use a value of 0
so the layout inflater has one fewer dimension to think about and can get the job done a bit quicker.
Now open MainActivity.java and, as before, add a new variable to hold a reference to the ListView
:
You'll note that instead of a single variable, the above code adds three new variables. The one for the ListView
makes sense. But what about the others? The others are for supplying the ListView
with data to display. All will be explained in a bit :]
But first, add the following code to the end of onCreate
:
// 4. Access the ListView mainListView = (ListView) findViewById(R.id.main_listview); // Create an ArrayAdapter for the ListView mArrayAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, mNameList); // Set the ListView to use the ArrayAdapter mainListView.setAdapter(mArrayAdapter); |
Some of that looks familiar by now: finding the ListView
using its id
. But what else is going on?
mArrayAdapter
is an example of an adapter, which is basically a go-between so your ListView
can get the data it needs.
ListView
as being a picky sort, as far as Objects
go. It's great at what it does but doesn't want to get its hands dirty with any real data. It's all got to be prepared for it or else it'll throw a fit.
The Adapter
, then, is the enterprising Object
that is able to code-switch between the rough language of the datasource and the refined dialect of the ListView
.
When you create mArrayAdapter
, you have to specify the datasource (mNameList
) and the target XML view for the data (simple_list_item_1
).
But hang on, you didn't write anything with an id
of simple_list_item_1
! So where is that coming from?
Notice the android.R.layout
part before simple_list_item_1
. There are several important concepts here, but let's look at the R
bit first. R
(or, R.java
, if you prefer) is a dynamically created class which gives you access to the resources in your project. If interested, you can read more about accessing resources via the R
class, here.
As the linked article above explains, you can use the R
class to get a resource ID by specifying a resource type and a resource name. The resource type would be something like string
, drawable
, or layout
- matching the various resource types you see in your project. And thus, the layout
part in android.R.layout.simple_list_item_1
simply specifies that you are referring to a layout resource.
But what about the android
prefix? Why is it there? It is an indicator that you didn't create the view; it's already part of the Android platform. It represents a simple TextView
that a default list cell can use.
The datasource in this case is mNameList
, which is simply a list of String
s. It's initialized, but empty. So the next step is to add some data that the ListView
can display.
Add the following code to the end of onClick
:
// Also add that value to the list shown in the ListView mNameList.add(mainEditText.getText().toString()); mArrayAdapter.notifyDataSetChanged(); |
You simply add whatever the user typed into the EditText
to the list of names and then shoot a signal to the adapter to update what's shown in the ListView
.
Now run your app.
You should be able to type a name into the EditText
, then see the name used in the TextView
and added to a new row in the ListView
when you press the Button
. Cool!
Looking at the items in a list is cool, but this is an app, and interactivity is even better! So next you’re going to set up a way to detect user selections from the list.
First, modify the class definition in MainActivity.java to add support for another interface. To do this, modify this line:
public class MainActivity extends Activity implements View.OnClickListener { |
To look like this:
public class MainActivity extends Activity implements View.OnClickListener, AdapterView.OnItemClickListener { |
All you've really done is add support for a new interface - AdapterView.OnItemClickListener
, which, as the name suggests, listens for item selections from a ListView
. MainActivity
is really stacking up the credentials!
Next, add the following code to the end of onCreate
:
// 5. Set this activity to react to list items being pressed mainListView.setOnItemClickListener(this); |
The above code sets MainActivity
as the listener for any item clicks on mainListView
.
Finally, add the following method to the end of the MainActivity
class (but before the final closing curly brace):
@Override public void onItemClick(AdapterView parent, View view, int position, long id) { // Log the item's position and contents // to the console in Debug Log.d("omg android", position + ": " + mNameList.get(position)); } |
This implements onItemClick
, thus setting up MainActivity
to live up to the title of being a card-carrying OnItemClickListener
.
But what's happening inside onItemClick
? There's a weird Log.d
in there, and then something with a get(position)
...
Take a look at what you’re passing along to onItemClick
. In particular, look at int position
, which is an integer equal to the index of the item the user pressed on the list (counting up from 0).
You take that position, as well as the item at that index in your list of names, and log them. Logging is a very basic, but very useful debugging technique.
Run your app, enter a few values and add them to the list, just as before. Then select an item. There's no visible effect for the moment.
With the app still running, click the 6: Android button in Studio, as shown below:
When you click this button, a console called logcat
pops up. It will read off tons of stuff from your emulator or device, the majority of which is not of much interest to you at this point. The log statements you generated with your selections are here, but there's too much noise to see them.
Here are some useful ways to filter out the noise and see only what you want:
Notice the option for Log level. When you put your Log
command into code, it specifically was the Log.d
command. The d
is for "debug" level. The levels are:
v
: Verbosed
: Debugi
: Infow
: Warninge
: ErrorWhen you select a log level for logcat
, it will only show messages at that level or higher. And the levels start at verbose and go up to error, in the same order as listed above. So, if you select the log level as Warning, then you'll see all warnings and errors but nothing else.
Meanwhile, you can use the text box to the right of the log level drop-down to apply a filter and show only those messages that contain the text you typed in.
Now that you know this, set the log level to Debug and type omg android into the filter text box.
Great! You now have a clean feed of log statements and you can detect when a certain item gets selected in your list. The ability to log will come in handy as you create more complicated apps and want to stay informed of the inner workings of your code.
Your app has several different views now, and it's time to think about other ways to add functionality. Older Android devices used to have a Menu device button that would display a bunch of options depending on the situation, but since Honeycomb in early 2011, Android has used the Action Bar to display any options for the current view.
The Action Bar provides a familiar base for your users. Since it's present across apps, making good use of the Action Bar means a significant part of your app's functionality will be immediately intuitive to an Android user. Conversely, neglecting the Action Bar would confuse all your users who expect it to work — and that would be weird!
The Action Bar is already in your app — it just has no options attached to it yet. That will be your first order of business next!
Soon you'll have a chance to show off the fact that you're learning Android, from within your own app! You'll do this using an intersection of the Android concept of the Intent
and the Action Bar, known as a ShareActionProvider
.
One of the advantages of an Intent
is that you can construct it in either a specific (explicit) or generic (implicit) manner. You used an example of the explicit type when you specifically defined the Intent
that launches your app, which the manifest then identifies as MainActivity
. Now you'll see an example of the implicit type.
A generic Intention
really helps in this case. After all, some of us prefer to share things with the entire world, and others with just a few friends. Rather than wondering what a potential user's favorite social network might be and integrating them one by one, you can politely tell the Android device that you'd very much like to share a bit of content (thus expressing an Intent
), and Android will graciously take it from there!
Navigate to src/main/res/menu/main.xml and open it.
You'll note that there's some auto-generated XML in there, but you don't need it. Replace the whole thing with this:
<!-- Defines the menu item that will appear on the Action Bar in MainActivity --> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Share item --> <item android:id="@+id/menu_item_share" android:showAsAction="ifRoom" android:title="Share" android:actionProviderClass= "android.widget.ShareActionProvider" /> </menu> |
The ShareActionProvider
is built-in (hence the android.widget
prefix) and ready-to-use. So, when a user selects your menu item, the necessary functionality is already in place for you to make use of.
Now head over to MainActivity.java and add the following variable at the top of the file (where the previous variables are):
ShareActionProvider mShareActionProvider; |
ShareActionProvider
you mean, so if it asks, use android.widget.ShareActionProvider
.Next, you need to add the following two methods to the class - the first one, onCreateOptionsMenu
, might already be implemented in the class. If it is, simply replace the existing implementation with the new one.
@Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu. // Adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); // Access the Share Item defined in menu XML MenuItem shareItem = menu.findItem(R.id.menu_item_share); // Access the object responsible for // putting together the sharing submenu if (shareItem != null) { mShareActionProvider = (ShareActionProvider)shareItem.getActionProvider(); } // Create an Intent to share your content setShareIntent(); return true; } private void setShareIntent() { if (mShareActionProvider != null) { // create an Intent with the contents of the TextView Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Android Development"); shareIntent.putExtra(Intent.EXTRA_TEXT, mainTextView.getText()); // Make sure the provider knows // it should work with that Intent mShareActionProvider.setShareIntent(shareIntent); } } |
onOptionsItemSelected
in the class, remove it.onCreateOptionsMenu
gets called once, when the activity first starts. Similar to how you specified which layout XML file you wanted to use for the activity in onCreate
, you now direct the menu inflater to look at main.xml for the menu items that go on the Action Bar.
From there, you can access the menu item you defined in XML by its id
, menu_item_share
, and then you can access its action provider. Previously, you specified that this item's action provider was a ShareActionProvider
. So, you can safely cast to that type in your code and hang onto a reference to it via the mShareActionProvider
variable.
Then, you call setShareIntent
. This method creates an Intent
, but not just any Intent
. It creates an Intent
whose action you've set to ACTION_SEND
. It's truly as generic as it looks: you're going to tell Android you want to take the action of sending something.
From there, you set the Intent
's content type, subject — used by email programs and the like, as the subject header of the message —, and text. The text matches whatever is currently in your TextView
. After you’ve packed up everything the Intent
needs to know, you pair it with mShareActionProvider
.
This code will work, but only "kind of." As-is, you only call setShareIntent
once, at the creation of the menu. It would be much better to update the Intent
whenever the TextView
changes — otherwise you're stuck with the initial message forever!
Add the following code to the end of onClick
:
// 6. The text you'd like to share has changed, // and you need to update setShareIntent(); |
Here, you simply make sure that the share intent is always up-to-date.
Run the app, and try out the new sharing feature - tapping the share icon on the Action Bar should reveal a lot of choices!
The ShareActionProvider
automatically puts together an array of possible avenues for sharing content based on the apps you have installed on a given device. This array of options will differ from device to device. The emulator will most likely have far fewer options for sharing, whereas you may have apps like Twitter and Facebook on an actual device and could share through those networks, too.
Everything you've done so far with regard to user input only persists while the app is running. But what about between sessions? Let's see some data persistence in action, with a new feature that will record and remember your name each time you open the app.
There are a few good options on Android to persist data, and the simplest one is SharedPreferences
.
SharedPreferences
stores data in key-value pairs, meaning that you specify a name (the key) for a piece of data (the value) when you save it, and you can retrieve it later by using the original key.
Let's see how it works in action, shall we?
First, add the following constants and variable to MainActivity.java (to the same place as the previous variables):
private static final String PREFS = "prefs"; private static final String PREF_NAME = "name"; SharedPreferences mSharedPreferences; |
The above sets PREF
and PREF_NAME
at the top of the class. You’ll use PREF
as a filename to keep your SharedPreferences
in a single location. You’ll use PREF_NAME
as the key for storing your name in shared preferences.
Strings
that you'll need/refer to multiple times. That way, if you need to change the string value later, you can do it in one place. You’ll also never have to worry about some weird spelling error creating bugs in your code.The final line adds a variable named mSharedPreferences
for storing a reference to the shared preferences class. You only need to access it in a few places, but it will be useful to hang onto it.
Next, add the following lines to the end of onCreate
:
// 7. Greet the user, or ask for their name if new displayWelcome(); |
The new code calls a new method, displayWelcome
. So implement that by adding the following code:
public void displayWelcome() { // Access the device's key-value storage mSharedPreferences = getSharedPreferences(PREFS, MODE_PRIVATE); // Read the user's name, // or an empty string if nothing found String name = mSharedPreferences.getString(PREF_NAME, ""); if (name.length() > 0) { // If the name is valid, display a Toast welcoming them Toast.makeText(this, "Welcome back, " + name + "!", Toast.LENGTH_LONG).show(); } } |
displayWelcome
directly into onCreate
and it would still work, many people, your humble author included, prefer to keep method lengths reasonable by calling separate methods for a specific task.
This also means that if the same task needs to be performed from elsewhere in code later on, you already have a handy method in place for it :]
In the new method, the first thing you do is access SharedPreferences
, with MODE_PRIVATE
meaning that only your OMGAndroid app can access the data stored here. This means that your saved data will not get overwritten by another application which might have used the same key as you.
Then you simply ask the preferences object for whatever value is stored using the key PREF_NAME
. The second parameter for the method can be used to set a default value to be returned in case there is no value stored using the key you provide. So, you use an empty String
as the default value here.
Finally, you check to see if the retrieved String
actually has any content, and display a message if so. Your message takes the form of a Toast
, which is a short-lived pop-up message that appears for a bit and then fades away. Give the Toast
a message it should display, specify one of its built-in lengths to remain on the screen and then simply tell it to show
. Easy!
What you've set up so far will show your name if the application can retrieve it out of the preferences. But obviously, that's no use to you yet since you have no mechanism in place to save your name in the first place!
You'll use a Dialog
to achieve that. Dialogs
are small windows that alert the user. They may contain ways for the user to provide input or make choices. You're going to use an AlertDialog
, specifically.
Add the following code to the end of displayWelcome
, creating an else
branch for the if
condition that's already there:
} else { // otherwise, show a dialog to ask for their name AlertDialog.Builder alert = new AlertDialog.Builder(this); alert.setTitle("Hello!"); alert.setMessage("What is your name?"); // Create EditText for entry final EditText input = new EditText(this); alert.setView(input); // Make an "OK" button to save the name alert.setPositiveButton("OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // Grab the EditText's input String inputName = input.getText().toString(); // Put it into memory (don't forget to commit!) SharedPreferences.Editor e = mSharedPreferences.edit(); e.putString(PREF_NAME, inputName); e.commit(); // Welcome the new user Toast.makeText(getApplicationContext(), "Welcome, " + inputName + "!", Toast.LENGTH_LONG).show(); } }); // Make a "Cancel" button // that simply dismisses the alert alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) {} }); alert.show(); } |
The app will reach this else
condition when there is no valid name saved using the PREF_NAME
key. You use an AlertDialog.Builder
to give your AlertDialog
a title, a message, and an EditText
in the center for the user to type in their name.
Then, you add two buttons to the AlertDialog
: a positive and a negative button. The first thing you define for each is the text displayed on the button - "OK" and "Cancel" are pretty standard choices. The second thing you define for each button is an OnClickListener
.
This time your OnClickListener
s are specifically DialogInterface.OnClickListener
s, and you are defining them right away. Notice how the parameters for onClick
are slightly different.
For the positive button's listener, onClick
does quite a bit. First, it reads the name that the user typed into the dialog's EditText
.
It then saves that name into SharedPreferences
using a helper called a SharedPreferences.Editor
. You simply tell the editor what to save and where, tell it to commit the changes, and that's it!
Finally, it displays a Toast
identical to the other welcoming one.
The negative button's listener is far simpler: it does nothing! Nothing!
Run your app and check out your Dialog
.
Type in your name, press OK and see the Toast
greeting. From now on, your app will remember your name and greet you each time you launch it!
You covered a lot of UI concepts in this part of the tutorial! Take a few minutes to play around with your app a little, maybe sharing something with a friend!
You can get the full source code for this part of the tutorial on GitHub or as a .zip.
Those looking for a challenge should try:
Dialog
you just made as a DialogFragment
, as in the example here.EditText
to expect names as its input so that it capitalizes first letters.EditText
.Hopefully you have found this helpful! If you have any comments or questions, feel free to leave them below. And of course, don't miss Part Three, in which you set up your app to interact with data online!
The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.
Make Your First Android App: Part 2/3 is a post from: Ray Wenderlich
The post Make Your First Android App: Part 2/3 appeared first on Ray Wenderlich.
This tutorial is the third and final part of the series devoted to helping you make your first Android app! Check out Part One to get started with Android and Part Two to learn more about Android UI and project structure.
In this final part of the tutorial, you’ll learn how to leverage the powerful capabilities of the web to search and display data and images. More specifically, you’ll make an app that searches the Open Library API — a database of over 20 million books —, displays information and cover images of the books it finds, and allows you to recommend books to friends!
When you’re done, you’ll know how to:
These are very useful and transferable skills for all sorts of Android apps you’ll want to make in the future.
To begin this part of the tutorial, you should be at the point where you have an app that takes user input, lists names, and shares messages through social networks. Your app should also ask for your name when you first open the app and greet you by name thereafter.
So the personal part is done — now it’s time to interwebify!
It’s time to do a bit of rebranding. No longer is this just a little demo app — it is a book search and recommendation engine!
The default Android icon can only take you so far. Download the following files and drag them onto the src/main/res/drawable-hdpi directory to add them to your project:
Click OK when asked to confirm the move.
Your src/main/res/drawable-hdpi directory should now look something like this:
hdpi
assets are provided here. For future projects, it is a good idea to add assets for the other dpi values as well.Open AndroidManifest.xml. As you recall, this is “the boss” of your app. If you want to change the app’s icon, you need to talk to the boss.
Find the opening application
tag and change the icon
attribute line from:
android:icon="@drawable/ic_launcher" |
To:
android:icon="@drawable/ic_books" |
From now on, the app icon will be a stack of books instead of the default Android icon. Depending on your Studio version (and/or SDK version) you may also see that the application
has an attribute for setting the application name. You’re going to update the application name as well, but not in the manifest.
First, while you’re still looking at the manifest, you need to let the manifest know that you plan to start accessing the Internet. Between the uses-sdk
and application
tags, add the following:
<!-- NEED TO ADD TO BE ABLE TO GO ONLINE AND GET DATA --> <uses-permission android:name="android.permission.INTERNET"/> |
If you don’t let “the boss” know your plans, it won’t file the appropriate paperwork with Android to make the web call happen. But now you’re good to go.
Now it’s time to change your app’s name. Open res/values/strings.xml and replace the strings for everything except action_settings
with the following new values:
<string name="app_name">Bookmaster General</string> <string name="textview">Search For Books!</string> <string name="button">Search</string> <string name="hint">Title and/or Author</string> |
That’s right — I used the title Bookmaster General.
Finally, remove this line from onCreate
in MainActivity.java:
mainTextView.setText("Set in Java!"); |
Run your app, you should see the new icon in the top left corner and the new application name on the Action Bar.
Your device or emulator’s home screen should also reflect the name and icon updates. For example:
Now that you’ve rebranded your app, it’s time to start adding the web interactions!
There are a lot of things to keep in mind when adding networking capabilities to your app. For example, you need to consider how to keep all the network interactions off the UI thread, so that the user can continue to use your app without everything locking up until a download completes. If you were using an app and it became completely unresponsive for lengths of time, you’d get pretty frustrated!
Thankfully, you can solve issues such as this by simply using third-party libraries. These have been specially designed and supported by Android experts to facilitate networking. Some of the most well-regarded include Retrofit, Volley, and Android Async Http. For the purposes of this tutorial, you’ll use Android Async Http.
Image downloads are also a consideration since each image takes time to download. Also, in the case of a list of books, if you have your list set to download images as needed, you might find that you end up downloading the same image over and over as your user scrolls through the list of items. You really don’t want that type of behavior. You’ll use another third-party library called Picasso to manage image downloads for you.
A way to easily manage your project’s dependencies would sure be great! That leads us to Gradle.
When you created your project in Android Studio, you may remember mentions of Gradle and Maven. Refer to Part One if you need a reintroduction. Now you’ll see them in action.
Open OMGAndroid/build.gradle. Note that there are two build.gradle
files in your project. You want the one that’s within the OMGAndroid
folder – not the one at the project root level:
Note: The contents of your build.gradle
file might not look exactly the same as the screenshot above. Android Studio is still in a state of flux and depending on the changes to Studio since the tutorial, the contents of the build.gradle
file might be slightly different.
Some of the stuff happening in here is beyond the scope of this tutorial, but if you become serious about Android development, I recommend looking into Gradle a little more, starting of course, with the Gradle website. For now, just notice that the Android plugin is being applied to the project (apply plugin: 'android'
).
You might also see a reference to the Maven Central Repository (mavenCentral()
) but depending on on Android Studio changes, the Maven Central references could also have moved to the other build.gradle
file. Suffice it to say that if you see a line similar to mavenCentral()
, that’s setting up a reference to the Maven Central Repository.
Scroll down to the area labeled dependencies
. There might be two places this word appears (again, depending on the Android Studio version); you want the one that at the end of the file, after the android
section. There may already be a support library listed there, or it may be empty. You’re going to add two new libraries.
Add the libraries like this:
dependencies { ... // there may or may not be a support library above these compile 'com.loopj.android:android-async-http:1.4.4' compile 'com.squareup.picasso:picasso:2.1.1' } |
Then find the Sync Project with Gradle Files button on the Studio toolbar and press it. It looks like this:
Believe it or not, that’s it! Just like that, you’ve included the Android Async Http and Picasso libraries in your project and you can start using them whenever you like.
It’s so easy because both of these libraries are available via the Maven Central Repository, to which your project already contains a reference. So, when you tell Gradle which libraries you’d like to use, it simply grabs them from the source and you’re good to go.
If you need to include any libraries which are not available on the Maven Central Repository, then you’d still have to go through the old school method of copying the source (or the library) into your project. But most of the time, you won’t have to go through all that pain since the Maven Repository contains a lot of third-party libraries for you to use. So you’ll probably be able to find an alternative to the library you’re interested in. If interested, you can even browse all the libraries available on the Maven Repository.
Great! Now it’s time to meet your datasource: the Open Library API. It’s a constantly-updated database of books, searchable by author and title. The wealth of data is enormous!
Try this query as an example: http://openlibrary.org/search.json?q=hunger+games+suzanne+collins
This is a relatively simple URL to understand; whatever is typed in after the ?q=
is the query string that will be used to search the database. Feel free to change it to a different author name/title and compare results, remembering to use +
to separate words.
The result is in the form of a large JSON response. Take a minute to look around at the sort of data the response includes.
If you’re not familiar with the JSON format, I suggest a quick glance through the JSON page. The basics are that there are two ways data can be arranged in JSON: arrays and objects.
JSONArrays
list objects of the same type. For example, a list of books might look like this:
["The Hunger Games", "Harry Potter and the Sorcerer's Stone", "A Game Of Thrones"]
JSONObjects
are a collection of key-value pairs. For example, a very simple object describing a book might be:
{"title" : "The Hunger Games", "author_name" : "Suzanne Collins"}
The results from your queries to the Open Library API are really just expansions on those two basic structures. They are larger, and nested into several levels, but the ideas remain the same.
Now that you know roughly what to expect from the datasource, it’s time to set up the code to go make a sample query!
Add the following variable at the top of MainActivity.java:
private static final String QUERY_URL = "http://openlibrary.org/search.json?q="; |
The above is simply a reference to the URL you’ll be calling. It’s much better to hold this URL as a static String
, so you don’t have to go searching through your code for it. Remember that you’re going to append the search string after the ?q=
.
Next, add this new method:
private void queryBooks(String searchString) { // Prepare your search string to be put in a URL // It might have reserved characters or something String urlString = ""; try { urlString = URLEncoder.encode(searchString, "UTF-8"); } catch (UnsupportedEncodingException e) { // if this fails for some reason, let the user know why e.printStackTrace(); Toast.makeText(this, "Error: " + e.getMessage(), Toast.LENGTH_LONG).show(); } // Create a client to perform networking AsyncHttpClient client = new AsyncHttpClient(); // Have the client get a JSONArray of data // and define how to respond client.get(QUERY_URL + urlString, new JsonHttpResponseHandler() { @Override public void onSuccess(JSONObject jsonObject) {} @Override public void onFailure(int statusCode, Throwable throwable, JSONObject error) {} }); } |
queryBooks
handles the network call to the API. It encodes the input searchString
into URL format, then appends that to the base URL you specified at the top of the class.
Calling new AsyncHttpClient()
simply creates an instance of the HTTP client. It’s got a lot of great methods built-in, but the only one you need here is get(String url, ResponseHandlerInterface responseHandler)
.
The get
method takes in two parameters:
String url
is simply the URL from which you’d like to fetch data. You supply an input made of the base URL defined at the top of the class, plus the search string.JsonHttpResponseHandler
, which you define now, even though you don’t know whether the network call will succeed or fail, or how long it will take either way. It contains methods called onSuccess
and onFailure
to respond to the two cases once the handler does get a response from the server.You might have noticed that onSuccess
and onFailure
are currently method stubs with no code. Fix that by fleshing out the onSuccess
to match the following:
@Override public void onSuccess(JSONObject jsonObject) { // Display a "Toast" message // to announce your success Toast.makeText(getApplicationContext(), "Success!", Toast.LENGTH_LONG).show(); // 8. For now, just log results Log.d("omg android", jsonObject.toString()); } |
Next, implement onFailure
as follows:
@Override public void onFailure(int statusCode, Throwable throwable, JSONObject error) { // Display a "Toast" message // to announce the failure Toast.makeText(getApplicationContext(), "Error: " + statusCode + " " + throwable.getMessage(), Toast.LENGTH_LONG).show(); // Log error message // to help solve any problems Log.e("omg android", statusCode + " " + throwable.getMessage()); } |
In both cases, you simply present a Toast
and log the results. Soon, though, the success case will get a lot more exciting.
queryBooks
is a great method and all, but it still needs to be hooked up to the EditText
and Button
to complete the search capability.
Fortunately, that’s pretty simple. In MainActivity.java, find onClick
and replace the current implementation with this:
// 9. Take what was typed into the EditText and use in search queryBooks(mainEditText.getText().toString()); |
Now, every time the user taps the button, this method takes the user’s input in the EditText
control and queries the Open Library for books and authors matching that string. Run your app and try it out to see what happens!
NoClassDefFoundError
upon querying. If so, not to worry — simply select Build > Rebuild Project and try again!Remember that you haven’t yet hooked up the results to anything that will display them on the screen. But open LogCat and you can see the resulting JSON spilled out whenever the API call finishes.
This is already exciting and clearly has the potential to be something cool very soon! But it’s still just a jumble of data. Your next challenge, then, is to display this data on the screen in a more organized manner.
First, you need to set up the layout for each of the rows in your list. A simple row of text won’t cut it anymore — you need space for a thumbnail image on the left and then two rows of text for the title and author, respectively. A layout which would look something like this:
Right-click on the res/layout folder in the Studio left pane, and select New > Layout resource file.
Name your file row_book.xml with a root element of RelativeLayout
.
The new file will open in Design mode. So, switch to Text mode, as before. Now change the layout_height
attribute from this:
android:layout_height="match_parent" |
To this:
android:layout_height="75dp" |
You simply set the layout to have a specific height instead of matching the height of the parent container.
Next, you’re going to add three views inside the RelativeLayout
, and then you’ll witness a few of the capabilities of this type of layout in action. First, add the thumbnail view:
<ImageView android:id="@+id/img_thumbnail" android:layout_width="50dp" android:layout_height="50dp" android:layout_marginLeft="25dp" android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:scaleType="centerInside"/> |
The ImageView
has the width and height set and there’s a little margin to the left of the picture. This is stuff you’ve seen before.
Then it gets interesting with layout_alignParentLeft
and layout_centerVertical
. These attributes are available since this ImageView
is a child of a RelativeLayout
. Because of these two attributes, the ImageView
stays tight to the left of the cell with that margin intact, and centers vertically.
The last attribute, scaleType
, specifies how you’d like the image to display within the amount of space it’s given. Especially given the unpredictable list of screen sizes you’d have to support, it’s often important to set this beforehand.
Using centerInside
means that you’ll preserve the aspect ratio of the image and that both dimensions will fit inside the given space. You might, however, have some blank space above and below the image if it’s too short, or space on the sides if it’s too thin. If you’re interested, you can read up on the various ScaleType
options.
Next, add a TextView
for the book’s title:
<TextView android:id="@+id/text_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="25dp" android:layout_toRightOf="@+id/img_thumbnail" android:layout_alignTop="@+id/img_thumbnail"/> |
Read through the XML first and see if you can tell what’s going on. The commands should be starting to make a bit of sense by now. The only thing that might give you pause might be the @+id/img_thumbnail
bit – but that’s just a reference to another control by ID. In this case, the ID refers to the ImageView
you added previously.
Basically, the title will sit to the right of the thumbnail, such that the top of the title will be at the same height as the top of the thumbnail. There’s some space in between the two controls, as well.
Finally, add the TextView
for the author’s name:
<TextView android:id="@+id/text_author" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/text_title" android:layout_alignLeft="@+id/text_title"/> |
By now, these attributes should all make sense. The author’s name will be below the title, with its left side aligned to that of the title.
RelativeLayout
s often reference other children using IDs, as you saw just now. Just make sure that the references don’t get circular (two attributes that depend on each other), or else your XML won’t inflate!In Part Two, you made a ListView
, at which point I mentioned that ListView
s are a bit picky. They don’t want to deal with the data directly — you can hardly blame them after seeing that confusing JSON response that popped into LogCat earlier. The simple, built-in adapter you used in Part Two won’t cut it here; you need a custom one.
Right-click on the com.example.omgandroid folder (or whatever package name you used when creating the project) and select New > Java Class.
Then type in JSONAdapter as the new class name.
Once you have your new class open in the editor, add the following code so the class looks like this:
public class JSONAdapter { private static final String IMAGE_URL_BASE = "http://covers.openlibrary.org/b/id/"; Context mContext; LayoutInflater mInflater; JSONArray mJsonArray; public JSONAdapter(Context context, LayoutInflater inflater) { mContext = context; mInflater = inflater; mJsonArray = new JSONArray(); } } |
This is still just a basic class, beginning with the first part of the URL you’ll use to download images — more on that when you implement the image download code.
Next, there are three simple variables:
Context
. This is a complex topic, but basically you only need it to tell Picasso, the image downloader library, what is going on in the app when you tell Picasso to get working.LayoutInflater
. You need this to inflate a View
out of that list item XML you just wrote.JSONArray
. This is the datasource that will be coming in from the server in response to your query!The JSONAdapter
method is the class constructor – that’s what you call when you create a new instance of JSONAdapter
. So, anyone who wants to ask JSONAdapter
to do anything has got to create an instance of it first, which in turn requires submitting the Context
and LayoutInflater
via the constructor.
The constructor currently simply saves the passed in references and creates an empty JSONArray
. You’ll pass the real data to the class after the search results are in.
Now you need to convert this class into an actual Adapter
class. This is quite easy in an object-oriented programming language like Java – simply change the top line of the class from:
public class JSONAdapter { |
To:
public class JSONAdapter extends BaseAdapter { |
JSONAdapter
is going to build on the basics provided by the BaseAdapter
class.Right away, Android Studio will underline the line you just modified in red to let you know that you need to add more to JSONAdapter
before it accurately extends BaseAdapter
. Studio isn’t just a naysayer, though — it can help, too! Click underlined line, then click the red light bulb that pops up next to it, and then select Implement Methods from the menu.
When asked to select methods to implement, make sure all four methods are highlighted and click OK.
Magically, Android Studio creates four methods for you and the red underlining disappears. This means that you’ve satisfactorily extended BaseAdapter
.
But… all the methods are empty. It’s time to go through each one in turn and make them do what you want.
So, first replace the current implementation for getCount
with the following:
@Override public int getCount() { return mJsonArray.length(); } |
getCount
answers the question: How long does your ListView
need to be? In this example, the answer is simply the length of your JSONArray
. Each entry in that array represents a book and so each one gets a row in the ListView
.
Now replace getItem
with this version:
@Override public JSONObject getItem(int position) { return mJsonArray.optJSONObject(position); } |
getItem
returns the book for a given position, counting up from 0. A single book is represented by a JSONObject
. They all just happen to be store in a JSONArray
. So all you have to do is look through the array for the JSONObject
at the given position.
Next, replace the stub for getItemId
with this code:
@Override public long getItemId(int position) { // your particular dataset uses String IDs // but you have to put something in this method return position; } |
This can be a very helpful method in some situations, but in this case, you don’t really need it. So, you just set it to position
. Imagine a situation where you have a list of books, as a subset of a larger database, and each book has an ID in the larger database. If you needed to go back and query for more information based on a certain item’s ID number, this method would be helpful for you.
The last method, getView
, answers the ListView
when it comes to the adapter and asks: What should I show at position X?
To begin to answer that question, you first need to create what’s called a view holder. Add the following to the end of your JSONAdapter
code (but before the final closing curly brace):
// this is used so you only ever have to do // inflation and finding by ID once ever per View private static class ViewHolder { public ImageView thumbnailImageView; public TextView titleTextView; public TextView authorTextView; } |
This class is simply a packager of the three subviews that every row in your list will have. Think of it as a Do-It-Yourself kit for your list cells. All each row needs to do is get one of these, update it with the right data based on the row and presto: an Insta-Row!
The trick is that as you scroll around through who-knows-how-many books in your list, the app shows the data using the same cells, over and over. There are only just enough list cells to fill the screen, plus a few extras. Keeping all of the list cells in memory, even while they’re off-screen, would get crazy!
As a view scrolls out of sight, the recycling crew comes by and dumps out everything inside the view, but hangs onto the ViewHolder
. That same view, and the ViewHolder
, then get handed over to a list cell about to scroll into sight.
The re-used view is handed one of these ready-made Insta-Row kits (aka a ViewHolder
), and simply fills the contents of each subview as needed, rather than inflating a brand new view from XML and creating all those subviews from scratch every single time.
For more details on the view recycling process, here is a helpful blog post about it.
With that in mind, replace the stub for getView
with this code:
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; // check if the view already exists // if so, no need to inflate and findViewById again! if (convertView == null) { // Inflate the custom row layout from your XML. convertView = mInflater.inflate(R.layout.row_book, null); // create a new "Holder" with subviews holder = new ViewHolder(); holder.thumbnailImageView = (ImageView) convertView.findViewById(R.id.img_thumbnail); holder.titleTextView = (TextView) convertView.findViewById(R.id.text_title); holder.authorTextView = (TextView) convertView.findViewById(R.id.text_author); // hang onto this holder for future recyclage convertView.setTag(holder); } else { // skip all the expensive inflation/findViewById // and just get the holder you already made holder = (ViewHolder) convertView.getTag(); } // More code after this return convertView; } |
If it happens to be the first time for the view, then you need to use your custom row XML using mInflater
and find all your subviews using findViewById
. But as mentioned earlier, the view might already exist — in which case you want to skip all that from-scratch stuff.
You use the setTag
and getTag
methods to hang onto the ViewHolder
and easily pack/unpack it while scrolling around.
Next, you need to handle the image thumbnail of the book’s cover. Put this new code right after the // More code after this
comment line:
// Get the current book's data in JSON form JSONObject jsonObject = (JSONObject) getItem(position); // See if there is a cover ID in the Object if (jsonObject.has("cover_i")) { // If so, grab the Cover ID out from the object String imageID = jsonObject.optString("cover_i"); // Construct the image URL (specific to API) String imageURL = IMAGE_URL_BASE + imageID + "-S.jpg"; // Use Picasso to load the image // Temporarily have a placeholder in case it's slow to load Picasso.with(mContext).load(imageURL).placeholder(R.drawable.ic_books).into(holder.thumbnailImageView); } else { // If there is no cover ID in the object, use a placeholder holder.thumbnailImageView.setImageResource(R.drawable.ic_books); } |
In this section, you first get the JSONObject
for the precise book whose data you want to display. Of course, this is dependent on the item’s position in the list.
Next, you check to see if there’s a cover ID for that book. Unfortunately, many books don’t have covers in the Open Library database. So, you look to see if a cover is there by calling has("cover_i")
, which returns a true-or-false boolean
. If it returns true
, then you parse out the cover ID from the JSONObject
and use it to construct a URL specific to Open Library.
You can change the “-S.jpg” to “-L.jpg” for a larger version of the same image: http://covers.openlibrary.org/b/id/6845816-L.jpg
Once you have the URL, you simply tell Picasso to download it and display it in your ImageView
. You also specify a placeholder image to show while the cover image is downloading.
If the book doesn’t have a cover assigned, you show the standard icon.
Finally, you need to populate the book title and author name. So, add the following code immediately after the block of code you added above:
// Grab the title and author from the JSON String bookTitle = ""; String authorName = ""; if (jsonObject.has("title")) { bookTitle = jsonObject.optString("title"); } if (jsonObject.has("author_name")) { authorName = jsonObject.optJSONArray("author_name").optString(0); } // Send these Strings to the TextViews for display holder.titleTextView.setText(bookTitle); holder.authorTextView.setText(authorName); |
This step is similar to the last. As long as the JSONObject
contains the title and author name, you parse the values and set the text of each TextView
!
The last thing you need to do before you can test your newly-webified app is connect the ListView
to the JSONAdapter
.
Remove the following code from onCreate
in MainActivity.java:
// Create an ArrayAdapter for the ListView mArrayAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, mNameList); // Set the ListView to use the ArrayAdapter mainListView.setAdapter(mArrayAdapter); |
Also remove the following from onItemClick
in MainActivity.java:
// 5. Log the item's position and contents // to the console in Debug Log.d("omg android", position + ": " + mNameList.get(position)); |
You don’t need any of that simple stuff now that you’ve got your own souped-up Adapter
!
Now, to start using the your adapter class, replace this line at the beginning of the class:
ArrayAdapter mArrayAdapter; |
With this:
JSONAdapter mJSONAdapter; |
Next, add the following to the end of onCreate
:
// 10. Create a JSONAdapter for the ListView mJSONAdapter = new JSONAdapter(this, getLayoutInflater()); // Set the ListView to use the ArrayAdapter mainListView.setAdapter(mJSONAdapter); |
Great! You just created an instance of your snazzy new JSONAdapter
, feeding it a Context
and a LayoutInflater
. Now your adapter is hooked up and can provide your ListView
with the data it needs.
If you were to build and run, though, you would be rather underwhelmed by the results. Even after inputting a search String
, the ListView
remains empty. Why?
Because, if you recall, you created your Adapter
using its constructor, public JSONAdapter(Context context, LayoutInflater inflater)
. That method creates an empty JSONArray
as a placeholder.
An empty list is OK to start with, of course, but it sure would be great to update the list when your search is done! That’s not happening yet, so that’s next on your agenda.
To update the list, you need to add an update method to your adapter and then call it from your activity.
First, add the following method to JSONAdapter.java:
public void updateData(JSONArray jsonArray) { // update the adapter's dataset mJsonArray = jsonArray; notifyDataSetChanged(); } |
This method accepts a JSONArray
input, sets it as the adapter’s datasource, and calls notifyDataSetChanged
to refresh the list. The adapter is already set up to know what to do with the data, so that’s all you need!
Now go back to MainActivity.java. You’re going to use your new method to update the list when the network call comes back. Find the following code in onSuccess
, which is embedded within queryBooks
:
// 8. For now, just log results Log.d("omg android", jsonObject.toString()); |
Replace it with this instead:
// update the data in your custom method. mJSONAdapter.updateData(jsonObject.optJSONArray("docs")); |
This is simply a call to updateData
with the newly-returned query response. As soon as the data comes back, you don’t waste any time — you send it straight to the adapter, which whips it into shape for the ListView
!
It’s finally time — run your app, and search away!
Now you can type a search string into your EditText
, tap the Search button and let the ListView
(somewhat) magically populate from your web search. Not only that — you can scroll through all the results and look at book titles, author names, and thumbnails of the cover images. This is already a pretty cool app!
One nice feature you may notice missing is some kind of progress bar or spinner to let the user know your app is “thinking.” There’s a convenient place to show one on Android — the Action Bar.
Add the following to onCreate
in MainActivity.java, before everything else in the method except the call to super
:
// 11. Add a spinning progress bar (and make sure it's off) requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); setProgressBarIndeterminateVisibility(false); |
This whole request
business simply gives you easy access to the progress bar when you need it. The place to turn it on is right as your network query begins, so add this line to queryBooks
, immediately after creating your AsyncHttpClient
:
// 11. start progress bar setProgressBarIndeterminateVisibility(true); |
You want the spinner to stop when the request is over, which could actually be in one of two spots – onSuccess
or onFailure
. Add the following line at the very beginning of onSuccess
:
// 11. stop progress bar setProgressBarIndeterminateVisibility(false); |
And add the same line to the beginning of onFailure
:
// 11. stop progress bar setProgressBarIndeterminateVisibility(false); |
Now, run the app again and do another search.
This time, you’ll notice a progress indicator pop up and start spinning in the Action Bar as your networking calls happen. Much better!
Seeing the list of all the books is exciting! The next logical step is to let the user select a book from the list to see more details or a larger version of the cover.
For this app, you’ll only show a larger version of the cover. But the techniques you use will pave the way for the additional challenge of displaying any further details that may interest you. Let’s get started!
First, add the following line to res/values/strings.xml:
<string name="activity_details">Book Details</string> |
This is simply to provide a title for the activity. As mentioned before, it’s good to keep all the strings in one file!
Next is the layout XML. It won’t be complicated. Right-click on res/layout and select New > Layout Resource File.
Name it activity_detail.xml, with a Root Element of ImageView
.
That’s almost it right there. All you need to do now is give the ImageView
an id
, a default image, and a bit of margin space for good measure. Edit activity_detail.xml to look like this:
<?xml version="1.0" encoding="utf-8"?> <ImageView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/img_cover" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="25dp" android:src="@drawable/img_books_large"/> |
That’s simple enough. This screen will now show a single ImageView
with a 25dp margin all around, and the default image is img_books_large
.
Next, you need to make a new Activity
. Right-click on the com.example.omgandroid package (or the package name you set originally) and select New > Java Class, as before.
Name the class DetailActivity.
This creates a simple, empty class for you. Next, modify the class definition so that your new class extends Activity
, like this:
public class DetailActivity extends Activity { |
You already know you need to access the ImageView
from your layout, so add the following method to your activity:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Tell the activity which XML layout is right setContentView(R.layout.activity_detail); // Enable the "Up" button for more navigation options getActionBar().setDisplayHomeAsUpEnabled(true); // Access the imageview from XML ImageView imageView = (ImageView) findViewById(R.id.img_cover); } |
The above code simply tells the Activity
to use the simple XML layout you made earlier, and then grabs the ImageView
you need from it. But wait: what is that getActionBar
stuff doing in there?
You may have noticed, or simply used it without really thinking about it, the Up button in many Android apps. The proper Android design of the Up and Back buttons is well-documented here and is worth a read, but it all begins with enabling the button as you just did.
The other steps for enabling the Up button take place in your manifest. Open AndroidManifest.xml and add the launchMode
attribute to MainActivity
so that it looks something like this:
<activity android:name="com.example.omgandroid.MainActivity" android:label="@string/app_name" android:launchMode="singleTop"> |
Note: Depending on your Android Studio version, the package name you selected when you created the project, and a few other factors, the above might not match what you see in your own AndroidManifest.xml
file exactly. The only thing you need to really worry about is adding the new launchMode
attribute as shown above. You can leave the rest as is.
So, do you remember my whole spiel about how the manifest is “the boss” who takes in “jobs” in the form of Intents
and checks if there is a team member right for the task? Normally, the manifest would arrange for the system to start a brand-new instance of that Activity
every time.
But, by setting the launchMode
attribute to singleTop
, you’re telling the manifest to use an already-existing instance of that Activity
, if possible. That way, when you use either the Back or Up button to return to the main screen, your most recent search results will still be there and you won’t have to start from scratch!
Next, add a definition for the DetailActivity
to the manifest, immediately after the one for MainActivity
:
<activity android:name=".DetailActivity" android:label="@string/activity_details" android:parentActivityName=".MainActivity"> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value=".MainActivity"/> </activity> |
This looks pretty similar to the definition for MainActivity
, except for the stuff about “parent activity.” Setting the parent activity tells the manifest which activity should be displayed when there’s a request to go up from DetailActivity
.
Now that you’ve set up your manifest to be aware of your DetailActivity
, you need to send it an Intent
to start it! This Intent
is going to originate from the MainActivity
, when the user selects a book from the list.
If you recall, the method that executes when a cell is selected from the ListView
is onItemClick
in MainActivity.java. It used to log information originally but now is empty. Add the following code to it:
// 12. Now that the user's chosen a book, grab the cover data JSONObject jsonObject = (JSONObject) mJSONAdapter.getItem(position); String coverID = jsonObject.optString("cover_i",""); // create an Intent to take you over to a new DetailActivity Intent detailIntent = new Intent(this, DetailActivity.class); // pack away the data about the cover // into your Intent before you head out detailIntent.putExtra("coverID", coverID); // TODO: add any other data you'd like as Extras // start the next Activity using your prepared Intent startActivity(detailIntent); |
Here, you create an Intent
to take you from where you are now (this
) to an instance of DetailActivity
. But before you fire off the command using startActivity
, there’s one more thing to remember to pack away.
As you’ve previously seen when you created setShareIntent
, you can pack “extras” into an Intent
in the form of key-value pairs. Since your DetailActivity
needs to know the cover ID to display, you extract that ID from the book’s JSON data and send it along.
TODO
reminder for an optional challenge to you. If you want your DetailActivity
to do anything more than show an image, you should send along additional data here.Build and run your app, and you will be able to click on a list item from your search results to see your new DetailActivity
! You can also navigate back to the main screen using either the Up or Back button.
Right now, you only see the placeholder image, but you know where this is headed :]
Add the following variables at the beginning of DetailActivity.java (right after the class definition line):
private static final String IMAGE_URL_BASE = "http://covers.openlibrary.org/b/id/"; // 13 String mImageURL; // 13 |
This sets the base URL for cover images on the Open Library API. You also create mImageURL
to hang onto any specific URL so that different methods within your Activity
can use it without needing to create the image URL all over again each time.
Next, add the following code to the end of onCreate
:
// 13. unpack the coverID from its trip inside your Intent String coverID = this.getIntent().getExtras().getString("coverID"); // See if there is a valid coverID if (coverID.length() > 0) { // Use the ID to construct an image URL mImageURL = IMAGE_URL_BASE + coverID + "-L.jpg"; // Use Picasso to load the image Picasso.with(this).load(mImageURL).placeholder(R.drawable.img_books_loading).into(imageView); } |
The above code digs into the Intent
that brought you to this Activity
and sees if it contains a String
with the name coverID
. If so, you proceed to use Picasso to download the image, just as you did in all the row cells. You display a “loading” image until the desired image is ready.
Build and run, and you’ll see the actual cover for the book you chose from the list!
The last thing to do is allow your users to share these cover images. You’ve already seen sharing in action and the code is almost identical here.
First, add another variable to DetailActivity.java:
ShareActionProvider mShareActionProvider; // 14 |
This is just another variable to hold a reference to the ShareActionProvider
for your Activity
.
Next, add this new method to the class:
private void setShareIntent() { // create an Intent with the contents of the TextView Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Book Recommendation!"); shareIntent.putExtra(Intent.EXTRA_TEXT, mImageURL); // Make sure the provider knows // it should work with that Intent mShareActionProvider.setShareIntent(shareIntent); } |
This should look very familiar, as it is nearly the same as the method you added to MainActivity
earlier. The only difference is the use of mImageURL
as the text to be shared.
One final thing left to do – you need to add the share button to the Action Bar. You can again reuse code from MainActivity
. Add this method to DetailActivity.java:
@Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu // this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); // Access the Share Item defined in menu XML MenuItem shareItem = menu.findItem(R.id.menu_item_share); // Access the object responsible for // putting together the sharing submenu if (shareItem != null) { mShareActionProvider = (ShareActionProvider) shareItem.getActionProvider(); } setShareIntent(); return true; } |
Here, you could potentially use different menu XML files to populate the Action Bar on different screens/activities. But for your purposes, the same menu/main.xml file will do.
Build and run your app, and you’ll have a pretty powerful app! Your app now takes in your search query, returns a list of books,allows you to take a closer look at a book cover, and share that cover image with friends!
Congratulations on putting together your app! This tutorial has introduced you to a variety of Android capabilities and techniques, and I hope you feel comfortable with the basics of development on the Android platform.
You can get the full source code for this app on GitHub or as a .zip.
If you’re looking for a few more challenges, how about trying some of these?
contentDescription
attribute to your ImageView
s for accessibility. Here’s an explanation.android:background
and add background colors to your layouts and views.Thank you for following this tutorial series. Please leave any comments or questions below!
The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.
Make Your First Android App: Part 3/3 is a post from: Ray Wenderlich
The post Make Your First Android App: Part 3/3 appeared first on Ray Wenderlich.
Learn how to make a game like Flappy Bird for iOS using Sprite Kit in this video tutorial, starting with making the bird flap!
Video Tutorial: How To Make a Game Like Flappy Bird Part 1: Player Movement is a post from: Ray Wenderlich
The post Video Tutorial: How To Make a Game Like Flappy Bird Part 1: Player Movement appeared first on Ray Wenderlich.
Update 1/18/2014: Fully updated for iOS 7 and AFNetworking 2.0 (original post by Scott Sherwood, update by Joshua Greene).
In iOS 7, Apple introduced NSURLSession as the new, preferred method of networking (as opposed to the older NSURLConnection API). Using this raw NSURLSession API is definitely a valid way to write your networking code – we even have a tutorial on that.
However, there’s an alternative to consider – using the popular third party networking library AFNetworking.
The latest version of AFNetworking (2.0) is now built on top of NSURLSession, so you get all of the great features provided there. But you also get a lot of extra cool features – like serialization, reachability support, UIKit integration (such as a handy category on asynchronously loading images in a UIImageView), and more.
AFNetworking is incredibly popular – it won our Reader’s Choice 2012 Best iOS Library Award. It’s also one of the most widely used, open-source projects with over 10,000 stars, 2,600 forks, and 160 contributors on Github.
In this AFNetworking 2.0 tutorial, you will learn about the major components of AFNetworking by building a Weather App that uses feeds from World Weather Online. You’ll start with static weather data, but by the end of the tutorial, the app will be fully connected to live weather feeds.
Today’s forecast: a cool developer learns all about AFNetworking and gets inspired to use it in his/her apps. Let’s get busy!
First download the starter project for this AFNetworking 2.0 tutorial here.
This project provides a basic UI to get you started – no AFNetworking code has been added yet.
Open MainStoryboard.storyboard, and you will see three view controllers:
From left to right, they are:
Build and run the project. You’ll see the UI appear, but nothing works yet. That’s because the app needs to get its data from the network, but this code hasn’t been added yet. This is what you will be doing in this tutorial!
The first thing you need to do is include the AFNetworking framework in your project. Download the latest version from GitHub by clicking on the Download Zip link.
When you unzip the file, you will see that it includes several subfolders and items. Of particular interest, it includes a subfolder called AFNetworking and another called UIKit+AFNetworking as shown below:
Drag these folders into your Xcode project.
When presented with options for adding the folders, make sure that Copy items into destination group’s folder (if needed) and Create groups for any added folders are both checked.
To complete the setup, open the pre-compiled header Weather-Prefix.pch from the Supporting Files section of the project. Add this line after the other imports:
#import "AFNetworking.h" |
Adding AFNetworking to the pre-compiled header means that the framework will be automatically included in all the project’s source files.
Pretty easy, eh? Now you’re ready to “weather” the code!
AFNetworking is smart enough to load and process structured data over the network, as well as plain old HTTP requests. In particular, it supports JSON, XML and Property Lists (plists).
You could download some JSON and then run it through a parser (like the built-in NSJSONSerialization) yourself, but why bother? AFNetworking can do it all!
First you need the base URL of the test script. Add this to the top of WTTableViewController.m, just underneath all the #import lines.
static NSString * const BaseURLString = @"http://www.raywenderlich.com/demos/weather_sample/"; |
This is the URL to an incredibly simple “web service” that I created for you for this tutorial. If you’re curious what it looks like, you can download the source.
The web service returns weather data in three different formats – JSON, XML, and PLIST. You can take a look at the data it can return by using these URLS:
The first data format you will be using is JSON. JSON is a very common JavaScript-derived object format. It looks something like this:
{ "data": { "current_condition": [ { "cloudcover": "16", "humidity": "59", "observation_time": "09:09 PM", } ] } } |
Note: If you’d like to learn more about JSON, check out our Working with JSON Tutorial.
When the user taps the JSON button, the app will load and process JSON data from the server. In WTTableViewController.m, find the jsonTapped: method (it should be empty) and replace it with the following:
- (IBAction)jsonTapped:(id)sender { // 1 NSString *string = [NSString stringWithFormat:@"%@weather.php?format=json", BaseURLString]; NSURL *url = [NSURL URLWithString:string]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; // 2 AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; operation.responseSerializer = [AFJSONResponseSerializer serializer]; [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { // 3 self.weather = (NSDictionary *)responseObject; self.title = @"JSON Retrieved"; [self.tableView reloadData]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { // 4 UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil]; [alertView show]; }]; // 5 [operation start]; } |
Awesome, this is your first AFNetworking code! Since this is all new, I’ll explain it one section at a time.
As you can see, AFNetworking is extremely simple to use. In just a few lines of code, you were able to create a networking operation that both downloads and parses its response.
Now that the weather data is stored in self.weather, you need to display it. Find the tableView:numberOfRowsInSection: method and replace it with the following:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if(!self.weather) return 0; switch (section) { case 0: { return 1; } case 1: { NSArray *upcomingWeather = [self.weather upcomingWeather]; return [upcomingWeather count]; } default: return 0; } } |
The table view will have two sections: the first to display the current weather and the second to display the upcoming weather.
“Wait a minute!”, you might be thinking. What is this [self.weather upcomingWeather]? If self.weather is a plain old NSDictionary, how does it know what “upcomingWeather” is?
To make it easier to display the data, I added a couple of helper categories on NSDictionary in the starter project:
These categories add some handy methods that make it a little easier to access the data elements. You want to focus on the networking part and not on navigating NSDictionary keys, right?
Note: FYI, an alternative way to make working with JSON results a bit easier than looking up keys in dictionaries or creating special categories like this is to use a third party library like JSONModel.
Still in WTTableViewController.m, find the tableView:cellForRowAtIndexPath: method and replace it with the following implementation:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"WeatherCell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; NSDictionary *daysWeather = nil; switch (indexPath.section) { case 0: { daysWeather = [self.weather currentCondition]; break; } case 1: { NSArray *upcomingWeather = [self.weather upcomingWeather]; daysWeather = upcomingWeather[indexPath.row]; break; } default: break; } cell.textLabel.text = [daysWeather weatherDescription]; // You will add code here later to customize the cell, but it's good for now. return cell; } |
Like the tableView:numberOfRowsInSection: method, the handy NSDictionary categories are used to easily access the data. The current day’s weather is a dictionary, and the upcoming days are stored in an array.
Build and run your project; tap on the JSON button to get the networking request in motion; and you should see this:
JSON success!
Property lists (or plists for short) are just XML files structured in a certain way (defined by Apple). Apple uses them all over the place for things like storing user settings. They look something like this:
<dict> <key>data</key> <dict> <key>current_condition</key> <array> <dict> <key>cloudcover</key> <string>16</string> <key>humidity</key> <string>59</string> ... |
The above represents:
It’s time to load the plist version of the weather data. Find the plistTapped: method and replace the empty implementation with the following:
- (IBAction)plistTapped:(id)sender { NSString *string = [NSString stringWithFormat:@"%@weather.php?format=plist", BaseURLString]; NSURL *url = [NSURL URLWithString:string]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; // Make sure to set the responseSerializer correctly operation.responseSerializer = [AFPropertyListResponseSerializer serializer]; [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { self.weather = (NSDictionary *)responseObject; self.title = @"PLIST Retrieved"; [self.tableView reloadData]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil]; [alertView show]; }]; [operation start]; } |
Notice that this code is almost identical to the JSON version, except for changing the responseSerializer to the default AFPropertyListResponseSerializer to let AFNetworking know that you’re going to be parsing a plist.
That’s pretty neat: your app can accept either JSON or plist formats with just a tiny change to the code!
Build and run your project and try tapping on the PLIST button. You should see something like this:
The Clear button in the top navigation bar will clear the title and table view data so you can reset everything to make sure the requests are going through.
While AFNetworking handles JSON and plist parsing for you, working with XML is a little more complicated. This time, it’s your job to construct the weather dictionary from the XML feed.
Fortunately, iOS provides some help via the NSXMLParser class (which is a SAX parser, if you want to read up on it).
Still in WTTableViewController.m, find the xmlTapped: method and replace its implementation with the following:
- (IBAction)xmlTapped:(id)sender { NSString *string = [NSString stringWithFormat:@"%@weather.php?format=xml", BaseURLString]; NSURL *url = [NSURL URLWithString:string]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; // Make sure to set the responseSerializer correctly operation.responseSerializer = [AFXMLParserResponseSerializer serializer]; [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { NSXMLParser *XMLParser = (NSXMLParser *)responseObject; [XMLParser setShouldProcessNamespaces:YES]; // Leave these commented for now (you first need to add the delegate methods) // XMLParser.delegate = self; // [XMLParser parse]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil]; [alertView show]; }]; [operation start]; } |
This should look pretty familiar by now. The biggest change is that in the success block you don’t get a nice, preprocessed NSDictionary object passed to you. Instead, responseObject is an instance of NSXMLParser, which you will use to do the heavy lifting in parsing the XML.
You’ll need to implement a set of delegate methods for NXMLParser to be able to parse the XML. Notice that XMLParser’s delegate is set to self, so you will need to add NSXMLParser’s delegate methods to WTTableViewController to handle the parsing.
First, update WTTableViewController.h and change the class declaration at the top as follows:
@interface WTTableViewController : UITableViewController<NSXMLParserDelegate> |
This means the class will implement the NSXMLParserDelegate protocol. You will implement these methods soon, but first you need to add a few properties.
Add the following properties to WTTableViewController.m within the class extension, right after @interface WTTableViewController () :
@property(nonatomic, strong) NSMutableDictionary *currentDictionary; // current section being parsed @property(nonatomic, strong) NSMutableDictionary *xmlWeather; // completed parsed xml response @property(nonatomic, strong) NSString *elementName; @property(nonatomic, strong) NSMutableString *outstring; |
These properties will come in handy when you’re parsing the XML.
Now paste this method in WTTableViewController.m, right before @end:
- (void)parserDidStartDocument:(NSXMLParser *)parser { self.xmlWeather = [NSMutableDictionary dictionary]; } |
The parser calls this method when it first starts parsing. When this happens, you set self.xmlWeather to a new dictionary, which will hold hold the XML data.
Next paste this method right after this previous one:
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { self.elementName = qName; if([qName isEqualToString:@"current_condition"] || [qName isEqualToString:@"weather"] || [qName isEqualToString:@"request"]) { self.currentDictionary = [NSMutableDictionary dictionary]; } self.outstring = [NSMutableString string]; } |
The parser calls this method when it finds a new element start tag. When this happens, you keep track of the new element’s name as self.elementName and then set self.currentDictionary to a new dictionary if the element name represents the start of a new weather forecast. You also reset outstring as a new mutable string in preparation for new XML to be received related to the element.
Next paste this method just after the previous one:
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { if (!self.elementName) return; [self.outstring appendFormat:@"%@", string]; } |
As the name suggests, the parser calls this method when it finds new characters on an XML element. You append the new characters to outstring, so they can be processed once the XML tag is closed.
Again, paste this next method just after the previous one:
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { // 1 if ([qName isEqualToString:@"current_condition"] || [qName isEqualToString:@"request"]) { self.xmlWeather[qName] = @[self.currentDictionary]; self.currentDictionary = nil; } // 2 else if ([qName isEqualToString:@"weather"]) { // Initialize the list of weather items if it doesn't exist NSMutableArray *array = self.xmlWeather[@"weather"] ?: [NSMutableArray array]; // Add the current weather object [array addObject:self.currentDictionary]; // Set the new array to the "weather" key on xmlWeather dictionary self.xmlWeather[@"weather"] = array; self.currentDictionary = nil; } // 3 else if ([qName isEqualToString:@"value"]) { // Ignore value tags, they only appear in the two conditions below } // 4 else if ([qName isEqualToString:@"weatherDesc"] || [qName isEqualToString:@"weatherIconUrl"]) { NSDictionary *dictionary = @{@"value": self.outstring}; NSArray *array = @[dictionary]; self.currentDictionary[qName] = array; } // 5 else if (qName) { self.currentDictionary[qName] = self.outstring; } self.elementName = nil; } |
This method is called when an end element tag is encountered. When that happens, you check for a few special tags:
Now for the final delegate method! Paste this method just after the previous one:
- (void) parserDidEndDocument:(NSXMLParser *)parser { self.weather = @{@"data": self.xmlWeather}; self.title = @"XML Retrieved"; [self.tableView reloadData]; } |
The parser calls this method when it reaches the end of the document. At this point, the xmlWeather dictionary that you’ve been building is complete, so the table view can be reloaded.
Wrapping xmlWeather inside another NSDictionary might seem redundant, but this ensures the format matches up exactly with the JSON and plist versions. This way, all three data formats can be displayed with the same code!
Now that the delegate methods and properties are in place, return to the xmlTapped: method and uncomment the lines of code from before:
- (IBAction)xmlTapped:(id)sender { ... [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { NSXMLParser *XMLParser = (NSXMLParser *)responseObject; [XMLParser setShouldProcessNamespaces:YES]; // These lines below were previously commented XMLParser.delegate = self; [XMLParser parse]; ... } |
Build and run your project. Try tapping the XML button, and you should see this:
Hmm, that looks dreary, like a week’s worth of rainy days. How could you jazz up the weather information in your table view?
Take another peak at the JSON format from before, and you will see that there are image URLs for each weather item. Displaying these weather images in each table view cell would add some visual interest to the app.
AFNetworking adds a category to UIImageView that lets you load images asynchronously, meaning the UI will remain responsive while images are downloaded in the background. To take advantage of this, first add the category import to the top of WTTableViewController.m:
#import "UIImageView+AFNetworking.h" |
Find the tableView:cellForRowAtIndexPath: method and paste the following code just above the final return cell; line (there should be a comment marking the spot):
cell.textLabel.text = [daysWeather weatherDescription]; NSURL *url = [NSURL URLWithString:daysWeather.weatherIconURL]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; UIImage *placeholderImage = [UIImage imageNamed:@"placeholder"]; __weak UITableViewCell *weakCell = cell; [cell.imageView setImageWithURLRequest:request placeholderImage:placeholderImage success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) { weakCell.imageView.image = image; [weakCell setNeedsLayout]; } failure:nil]; |
UIImageView+AFNetworking makes setImageWithURLRequest: and several other related methods available to you.
Both the success and failure blocks are optional, but if you do provide a success block, you must explicitly set the image property on the image view (or else it won’t be set). If you don’t provide a success block, the image will automatically be set for you.
When the cell is first created, its image view will display the placeholder image until the real image has finished downloading.
Now build and run your project. Tap on any of the operations you’ve added so far, and you should see this:
Nice! Asynchronously loading images has never been easier.
So far you’ve been creating one-off networking operations using AFHTTPRequestOperation.
Alternatively, AFHTTPRequestOperationManager and AFHTTPSessionManager are designed to help you easily interact with a single, web-service endpoint.
Both of these allow you to set a base URL and then make several requests to the same endpoint. Both can also monitor for changes in connectivity, encode parameters, handle multipart form requests, enqueue batch operations, and help you perform the full suite of RESTful verbs (GET, POST, PUT, and DELETE).
“Which one should I use?”, you might ask.
In your weather app project, you’ll be using AFHTTPSessionManager to perform both a GET and PUT operation.
Note: Unclear on what all this talk is about REST, GET, and POST? Check out this explanation of the subject – What is REST?
Update the class declaration at the top of WTTableViewController.h to the following:
@interface WTTableViewController : UITableViewController<NSXMLParserDelegate, CLLocationManagerDelegate, UIActionSheetDelegate> |
In WTTableViewController.m, find the clientTapped: method and replace its implementation with the following:
- (IBAction)clientTapped:(id)sender { UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"AFHTTPSessionManager" delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil otherButtonTitles:@"HTTP GET", @"HTTP POST", nil]; [actionSheet showFromBarButtonItem:sender animated:YES]; } |
This method creates and displays an action sheet asking the user to choose between a GET and POST request. Add the following method at the end of the class implementation (right before @end) to implement the action sheet delegate method:
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { if (buttonIndex == [actionSheet cancelButtonIndex]) { // User pressed cancel -- abort return; } // 1 NSURL *baseURL = [NSURL URLWithString:BaseURLString]; NSDictionary *parameters = @{@"format": @"json"}; // 2 AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:baseURL]; manager.responseSerializer = [AFJSONResponseSerializer serializer]; // 3 if (buttonIndex == 0) { [manager GET:@"weather.php" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) { self.weather = responseObject; self.title = @"HTTP GET"; [self.tableView reloadData]; } failure:^(NSURLSessionDataTask *task, NSError *error) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil]; [alertView show]; }]; } // 4 else if (buttonIndex == 1) { [manager POST:@"weather.php" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) { self.weather = responseObject; self.title = @"HTTP POST"; [self.tableView reloadData]; } failure:^(NSURLSessionDataTask *task, NSError *error) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil]; [alertView show]; }]; } } |
Here’s what’s happening above:
In this example you’re requesting JSON responses, but you can easily request either of the other two formats as discussed previously.
Build and run your project, tap on the Client button and then tap on either the HTTP GET or HTTP POST button to initiate the associated request. You should see these screens:
At this point, you know the basics of using AFHTTPSessionManager, but there’s an even better way to use it that will result in cleaner code, which you’ll learn about next.
Before you can use the live service, you’ll first need to register for a free account on World Weather Online. Don’t worry – it’s quick and easy to do!
After you’ve registered, you should receive a confirmation email at the address you provided, which will have a link to confirm your email address (required). You then need to request a free API key via the My Account page. Go ahead and leave the page open with your API key as you’ll need it soon.
Now that you’ve got your API key, back to AFNetworking…
So far you’ve been creating AFHTTPRequestOperation and AFHTTPSessionManager directly from the table view controller as you needed them. More often than not, your networking requests will be associated with a single web service or API.
AFHTTPSessionManager has everything you need to talk to a web API. It will decouple your networking communications code from the rest of your code, and make your networking communications code reusable throughout your project.
Here are two guidelines on AFHTTPSessionManager best practices:
Your project currently doesn’t have a subclass of AFHTTPSessionManager; it just creates one directly. Let’s fix that.
To begin, create a new file in your project of type iOS\Cocoa Touch\Objective-C Class. Call it WeatherHTTPClient and make it a subclass of AFHTTPSessionManager.
You want the class to do three things: perform HTTP requests, call back to a delegate when the new weather data is available, and use the user’s physical location to get accurate weather.
Replace the contents of WeatherHTTPClient.h with the following:
#import "AFHTTPSessionManager.h" @protocol WeatherHTTPClientDelegate; @interface WeatherHTTPClient : AFHTTPSessionManager @property (nonatomic, weak) id<WeatherHTTPClientDelegate>delegate; + (WeatherHTTPClient *)sharedWeatherHTTPClient; - (instancetype)initWithBaseURL:(NSURL *)url; - (void)updateWeatherAtLocation:(CLLocation *)location forNumberOfDays:(NSUInteger)number; @end @protocol WeatherHTTPClientDelegate <NSObject> @optional -(void)weatherHTTPClient:(WeatherHTTPClient *)client didUpdateWithWeather:(id)weather; -(void)weatherHTTPClient:(WeatherHTTPClient *)client didFailWithError:(NSError *)error; @end |
You’ll learn more about each of these methods as you implement them. Switch over to WeatherHTTPClient.m and add the following right after the import statement:
// Set this to your World Weather Online API Key static NSString * const WorldWeatherOnlineAPIKey = @"PASTE YOUR API KEY HERE"; static NSString * const WorldWeatherOnlineURLString = @"http://api.worldweatheronline.com/free/v1/"; |
Make sure you replace @”PASTE YOUR KEY HERE” with your actual World Weather Online API Key.
Next paste these methods just after the @implementation line:
+ (WeatherHTTPClient *)sharedWeatherHTTPClient { static WeatherHTTPClient *_sharedWeatherHTTPClient = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sharedWeatherHTTPClient = [[self alloc] initWithBaseURL:[NSURL URLWithString:WorldWeatherOnlineURLString]]; }); return _sharedWeatherHTTPClient; } - (instancetype)initWithBaseURL:(NSURL *)url { self = [super initWithBaseURL:url]; if (self) { self.responseSerializer = [AFJSONResponseSerializer serializer]; self.requestSerializer = [AFJSONRequestSerializer serializer]; } return self; } |
The sharedWeatherHTTPClient method uses Grand Central Dispatch to ensure the shared singleton object is only allocated once. You initialize the object with a base URL and set it up to request and expect JSON responses from the web service.
Paste the following method underneath the previous ones:
- (void)updateWeatherAtLocation:(CLLocation *)location forNumberOfDays:(NSUInteger)number { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"num_of_days"] = @(number); parameters[@"q"] = [NSString stringWithFormat:@"%f,%f",location.coordinate.latitude,location.coordinate.longitude]; parameters[@"format"] = @"json"; parameters[@"key"] = WorldWeatherOnlineAPIKey; [self GET:@"weather.ashx" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) { if ([self.delegate respondsToSelector:@selector(weatherHTTPClient:didUpdateWithWeather:)]) { [self.delegate weatherHTTPClient:self didUpdateWithWeather:responseObject]; } } failure:^(NSURLSessionDataTask *task, NSError *error) { if ([self.delegate respondsToSelector:@selector(weatherHTTPClient:didFailWithError:)]) { [self.delegate weatherHTTPClient:self didFailWithError:error]; } }]; } |
This method calls out to World Weather Online to get the weather for a particular location.
Once the object has loaded the weather data, it needs some way to communicate that data back to whoever’s interested. Thanks to the WeatherHTTPClientDelegate protocol and its delegate methods, the success and failure blocks in the above code can notify a controller that the weather has been updated for a given location. That way, the controller can update what it is displaying.
Now it’s time to put the final pieces together! The WeatherHTTPClient is expecting a location and has a defined delegate protocol, so you need to update the WTTableViewController class to take advantage of this.
Open up WTTableViewController.h to add an import and replace the @interface declaration as follows:
#import "WeatherHTTPClient.h" @interface WTTableViewController : UITableViewController <NSXMLParserDelegate, CLLocationManagerDelegate, UIActionSheetDelegate, WeatherHTTPClientDelegate> |
Also add a new Core Location manager property:
@property (nonatomic, strong) CLLocationManager *locationManager; |
In WTTableViewController.m, add the following lines to the bottom of viewDidLoad::
self.locationManager = [[CLLocationManager alloc] init]; self.locationManager.delegate = self; |
These lines initialize the Core Location manager to determine the user’s location when the view loads. The Core Location manager then reports that location via a delegate callback. Add the following method to the implementation:
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { // Last object contains the most recent location CLLocation *newLocation = [locations lastObject]; // If the location is more than 5 minutes old, ignore it if([newLocation.timestamp timeIntervalSinceNow] > 300) return; [self.locationManager stopUpdatingLocation]; WeatherHTTPClient *client = [WeatherHTTPClient sharedWeatherHTTPClient]; client.delegate = self; [client updateWeatherAtLocation:newLocation forNumberOfDays:5]; } |
Now when there’s an update to the user’s whereabouts, you can call the singleton WeatherHTTPClient instance to request the weather for the current location.
Remember, WeatherHTTPClient has two delegate methods itself that you need to implement. Add the following two methods to the implementation:
- (void)weatherHTTPClient:(WeatherHTTPClient *)client didUpdateWithWeather:(id)weather { self.weather = weather; self.title = @"API Updated"; [self.tableView reloadData]; } - (void)weatherHTTPClient:(WeatherHTTPClient *)client didFailWithError:(NSError *)error { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather" message:[NSString stringWithFormat:@"%@",error] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alertView show]; } |
When the WeatherHTTPClient succeeds, you update the weather data and reload the table view. In case of a network error, you display an error message.
Find the apiTapped: method and replace it with the following:
- (IBAction)apiTapped:(id)sender { [self.locationManager startUpdatingLocation]; } |
Build and run your project (try your device if you have any troubles with your simulator), tap on the API button to initiate the WeatherHTTPClient request, and you should see something like this:
Here’s hoping your upcoming weather is as sunny as mine!
You might have noticed that this external web service can take some time before it returns with data. It’s important to provide your users with feedback when doing network operations so they know the app hasn’t stalled or crashed.
Luckily, AFNetworking comes with an easy way to provide this feedback: AFNetworkActivityIndicatorManager.
In WTAppDelegate.m, add this import just below the other:
#import "AFNetworkActivityIndicatorManager.h" |
Then find the application:didFinishLaunchingWithOptions: method and replace it with the following:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [AFNetworkActivityIndicatorManager sharedManager].enabled = YES; return YES; } |
Enabling the sharedManager automatically displays the network activity indicator whenever a new operation is underway. You won’t need to manage it separately for every request you make.
Build and run, and you should see the little networking spinner in the status bar whenever there’s a network request:
Now there’s a sign of life for your user even when your app is waiting on a slow web service.
If you tap on a table view cell, the app takes you to a detail view of the weather and an animation illustrating the corresponding weather conditions.
That’s nice, but at the moment the animation has a very plain background. What better way to update the background than… over the network!
Here’s the final AFNetworking trick for this tutorial: AFHTTPRequestOperation can also handle image requests by setting its responseSerializer to an instance of AFImageResponseSerializer.
There are two method stubs in WeatherAnimationViewController.m to implement. Find the updateBackgroundImage: method and replace it with the following:
- (IBAction)updateBackgroundImage:(id)sender { NSURL *url = [NSURL URLWithString:@"http://www.raywenderlich.com/wp-content/uploads/2014/01/sunny-background.png"]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; operation.responseSerializer = [AFImageResponseSerializer serializer]; [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { self.backgroundImageView.image = responseObject; [self saveImage:responseObject withFilename:@"background.png"]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { NSLog(@"Error: %@", error); }]; [operation start]; } |
This method initiates and handles downloading the new background. On completion, it returns the full image requested.
In WeatherAnimationViewController.m, you will see two helper methods, imageWithFilename: and saveImage:withFilename:, which will let you store and load any image you download. updateBackgroundImage: calls these helper methods to save the downloaded images to disk.
Find the deleteBackgroundImage: method and replace it with the following:
- (IBAction)deleteBackgroundImage:(id)sender { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *path = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"WeatherHTTPClientImages/"]; NSError *error = nil; [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; NSString *desc = [self.weatherDictionary weatherDescription]; [self start:desc]; } |
This method deletes the downloaded background image so that you can download it again when testing the application.
For the final time: build and run, download the weather data and tap on a cell to get to the detailed view. From here, tap the Update Background button. If you tap on a Sunny cell, you should see this:
You can download the completed project from here.
Think of all the ways you can now use AFNetworking to communicate with the outside world:
The power of AFNetworking is yours to deploy!
If you have any questions about anything you’ve seen here, please pay a visit to the forums to get some assistance. I’d also love to read your comments!
AFNetworking 2.0 Tutorial is a post from: Ray Wenderlich
The post AFNetworking 2.0 Tutorial appeared first on Ray Wenderlich.
Learn how to spawn obstacles periodically over time in this video tutorial series on how to make a game like Flappy Bird.
Video Tutorial: How To Make a Game Like Flappy Bird Part 2: Spawning Obstacles is a post from: Ray Wenderlich
The post Video Tutorial: How To Make a Game Like Flappy Bird Part 2: Spawning Obstacles appeared first on Ray Wenderlich.
For many years, Core Data has been an integral part of many OS X and iOS apps, supporting the persistence and querying of user data. Apple constantly tinkers with Core Data API, in an effort to make it easier for developers to use and integrate into apps.
That said, the truth is that Core Data remains a challenging API to master. Even when you know how to use Core Data, mundane, everyday tasks can feel clunky and cumbersome. Good thing there is MagicalRecord, a third-party library for Core Data created by MagicalPanda – and good that this MagicalRecord tutorial is focused on bringing you up to speed with MagicalRecord quickly and easily :]
MagicalRecord is easy to use, well developed and popular. As described by the project’s author, the point of MagicalRecord is to “clean up” Core Data code and enable simple one-line fetches of data, while still allowing custom performance optimizations. How does it do this? It provides convenience methods, which wrap common boilerplate for Core Data setup, query and update. Its design also features influences from Ruby on Rails’ ActiveRecord persistence system (an instance of the venerable Active Record design pattern).
But enough theory! Just follow along this MagicalRecord tutorial to see how MagicalRecord works its, well, magic.. In this tutorial, you’ll create an app that will keep track of your favorite beers. It will allow you to:
To follow this tutorial, you should already have a basic understanding of Core Data, at the level provided by a basic Core Data tutorial. Beyond that, you don’t need any prior experience with Core Data.
To begin, start by downloading this Starter Project. Once you have it, go ahead and run the application so you can check it out.
It has a basic navigation controller with an Add button, table view, search bar (if you pull down on the table) and a segmented control for sorting alphabetically or by rating. If you click the Add button you’ll go to the UI, where you’ll enter and view beer information. If you try to enter anything, it won’t save – yet.
Now take a look around the code. In the Project Navigator you’ll see:
While you’re poking around, you may notice there is no Core Data model, nor does AppDelegate.m contain any code to set it up. This is the perfect scenario for any project you started without Core Data, only to realize later that Core Data is the way to go.
In the Project Navigator, go ahead and expand the MagicalRecord group. Inside, you’ll find groups for Categories and Core, as well as CoreData+MagicalRecord.h. Expand the Categories group, and open the file named NSManagedObjectModel+MagicalRecord.h.
You’ll notice that the methods in the header are all prefixed MR_
. In fact, if you go through any of the files in the Categories group, you’ll notice all of the methods are prefixed with MR_
.
I don’t know about you, but I find it rather confusing to begin every method with the same two letters. Good thing MagicalRecord makes it easy to change them up with Shorthand Support.
Again, in the Project Navigator, expand the Supporting Files group, and open BeerTracker-Prefix.pch. This is the precompiled header for the project. The starter project has added two lines to this file:
#define MR_SHORTHAND #import “CoreData+MagicalRecord.h” |
These two lines enable MagicalRecord for your project:
MR_SHORTHAND
tells MagicalRecord that you do not want to have to type MR_
before any MagicalRecord method. You can look in MagicalRecord+ShorthandSupport.m to find out more how this works. As it goes beyond the scope of this tutorial, it will not be discussed here.Note:If you want to add MagicalRecord to your own project, there are a few tips on their GitHub page. You can also add MagicalRecord the same way it I added it to this project:
#define MR_SHORTHAND
#import “CoreData+MagicalRecord.h”
To start keeping track of your favorite beers, you’re going to need a model, because you can’t expect yourself to remember them, right? From the Xcode menu, select File\New\File…. From the list on the left hand side, select Core Data and choose Data Model from the options.
Name the file BeerModel.xcdatamodeld and put it in the BeerTracker group.
In the project navigator, select the new bundle BeerModel.xcdatamodeld to start editing it. Add an entity named Beer. Add an attribute named name with a type of String.
Add another Entity named BeerDetails. This entity will track of the details for a beer, such as user rating, notes and where to find the image. Add the following attributes to BeerDetails, with the corresponding types:
Next, you’re going to create a relationship between the two, so the Beer entity knows which BeerDetails belong to it. Under BeerDetails, create a new relationship and name it “beer” with the destination Beer. By selecting the destination beer, you’ve established the first part of the relationship between the entities.
Finish setting up the relationship by selecting the Beer entity. Add a relationship named beerDetails, with the destination of BeerDetails, and the inverse of Beer. Now your Beer entity will have a BeerDetails entity to call its own.
The next step is to create data classes to represent the entities. You’ll use Xcode for this, but you need to be careful because of some quirks in Xcode’s behavior.
First, edit the Core Data model by selecting it in the Xcode project navigator; ensure that you highlight the Beer entity in the “ENTITIES” pane — and NOT the BeerDetails entity.
Next, in the Xcode menu, go to Editor\Create NSManagedObject Subclass…. Check BeerModel, then select Next. Under Entities to Manage, select the checkboxes for both the Beer and BeerDetails, if not selected by default, and double-check that Beer is highlighted. Press Next and then Create. This will generate new classes of Beer and BeerDetails corresponding to the entities with those names. Take a moment to look at the new classes, and you’ll see that each class has properties corresponding to the entity attributes you defined.
In particular, check the properties that represent the relationship between entities. You should see that the Beer class holds a property beerDetails of type BeerDetails * object. But, you’ll also see that the BeerDetails class holds a property beer of type NSManagedObject*. Why doesn’t the property have a type of Beer *? This is due to a limitation in Xcode’s “Create NSManagedObject Subclass” command. It has no effect on this project, so feel free to ignore it.
However, that command is not only generating the property types of the classes. It is also generating the class names used to define those types, and the command is not clever enough to do both things fully. One workaround is to simply tweak the generated code and set the property to the precise subclass type (adding forward declarations as needed). Another workaround is just to give Xcode less to do. If you explicitly define the class name for each entity in the Data Model Inspector before generating the classes themselves, then Xcode will generate correct property types.
If you’re curious about more elaborate tools for generating classes from Xcode data models, one option is mogenerator.
Now that you’ve created the data object classes, it’s time to initialize the Core Data stack. Open AppDelegate.m, and in application:didFinishLaunchingWithOptions:
add the following lines of code just before the return statement:
// Setup CoreData with MagicalRecord // Step 1. Setup Core Data Stack with Magical Record // Step 2. Relax. Why not have a beer? Surely all this talk of beer is making you thirsty… [MagicalRecord setupCoreDataStackWithStoreNamed:@"BeerModel"]; |
If you’ve ever worked with an Xcode project created with Core Data enabled, you’ve likely seen how much code it takes to setup Core Data initialize it within the AppDelegate file. With MagicalRecord, all you need is that one line!
MagicalRecord provides a few alternative methods for setting up your Core Data stack, depending on the following:
If your model file has the same base name as your project (e.g., a model file BeerTracker.xcdatamodeld
, within a project named BeerTracker
), then you can use one of MagicalRecord’s first three convenience methods–setupCoreDataStack
, setupCoreDataStackWithInMemoryStore
, or setupAutoMigratingCoreDataStack
.
But, since your model file is named differently, then you have to use one of the other two setup methods, setupCoreDataStackWithStoreNamed:
or setupCoreDataStackWithAutoMigratingSqliteStoreNamed:
.
With the setup methods described as AutoMigrating, you can change your model, and if it is possible to auto migrate your store, MagicalRecord will handle it for you. Normally, Core Data requires you to add some code to handle small changes you make to your model.
Now that your model and Core Data stack are set up, you can start adding beers to your list. Open BeerViewController.h, and after @class AMRatingControl
, add:
@class Beer; |
Also, add a Beer property after @interface
:
@property (nonatomic, strong) Beer *beer; |
Now switch to BeerViewController.m, and import Beer.h and BeerDetails.h at the top:
#import "Beer.h" #import "BeerDetails.h" |
In viewDidLoad
, add the following block of code:
- (void)viewDidLoad { // 1. If there is no beer, create new Beer if (!self.beer) { self.beer = [Beer createEntity]; } // 2. If there are no beer details, create new BeerDetails if (!self.beer.beerDetails) { self.beer.beerDetails = [BeerDetails createEntity]; } // View setup // 3. Set the title, name, note field and rating of the beer self.title = self.beer.name ? self.beer.name : @"New Beer"; self.beerNameField.text = self.beer.name; self.beerNotesView.text = self.beer.beerDetails.note; self.ratingControl.rating = [self.beer.beerDetails.rating integerValue]; [self.cellOne addSubview:self.ratingControl]; // 4. If there is an image path in the details, show it. if ([self.beer.beerDetails.image length] > 0) { // Image setup NSData *imgData = [NSData dataWithContentsOfFile:[NSHomeDirectory() stringByAppendingPathComponent:self.beer.beerDetails.image]]; [self setImageForBeer:[UIImage imageWithData:imgData]]; } } |
Troubleshooting: If you have errors after copying the preceding code into your project, clean your project by pressing Shift+Command+K, or by going to Product\Clean.
When BeerViewController loads, it will be because you have:
Now, when the view loads you will want to do the following:
There are a few more things you need to set up so that you can edit and add new beers. First, you need to be able to add or edit the name. Edit textFieldDidEndEditing:
so it appears as below:
- (void)textFieldDidEndEditing:(UITextField *)textField { if ([textField.text length] > 0) { self.title = textField.text; self.beer.name = textField.text; } } |
Now, when you finish adding the name textField, it will set the beer’s name to whatever the contents of the textField are, so long the field is not empty.
To save the note content to the beer’s note value, find textViewDidEndEditing:
, and edit it so it appears as below:
- (void)textViewDidEndEditing:(UITextView *)textView { [textView resignFirstResponder]; if ([textView.text length] > 0) { self.beer.beerDetails.note = textView.text; } } |
Next, make sure the rating for the beer updates when the user changes it in the View Controller. Find updateRating
, and add the following:
- (void)updateRating { self.beer.beerDetails.rating = @(self.ratingControl.rating); } |
When the user taps the UIImageView on the details page, it allows them to add or edit a photo. A UIActionSheet displays, which allows the user to pick an image from their Camera Roll, or to snap a new picture. If the user wants to take a picture, you’ll need to make sure the image also saves to the disk. Instead of saving the image to Core Data (which can cause performance issues) you’ll want to save the image to the user’s documents directory, with all their other pictures, and just use the image path for Core Data.
Manage interaction between the camera and the photo library by implementing methods of the UIImagePickerControllerDelegate protocol. You need to make the BeerViewController the delegate for the UIImagePickerController, since it’s the controller handling this storyboard scene. Find imagePickerController:didFinishPickingMediaWithinfo:
, and add the following:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { // 1. Grab image and save to disk UIImage *image = info[UIImagePickerControllerOriginalImage]; // 2. Remove old image if present if (self.beer.beerDetails.image) { [ImageSaver deleteImageAtPath:self.beer.beerDetails.image]; } // 3. Save the image if ([ImageSaver saveImageToDisk:image andToBeer:self.beer]) { [self setImageForBeer:image]; } [picker dismissViewControllerAnimated:YES completion:nil]; } |
Here’s what’s happening in the code shown above:
imagePickerController:didFinishPickingMediaWithInfo:
indirectly passes a reference to the user’s image of choice, where the image itself is in the info
dictionary, under the UIImagePickerControllerOriginalImage
keyYou’ll need to modify ImageSaver just a little to get it to work. Now that you’ve got your Beer classes created, you can uncomment the import lines, and the line that sets the image’s path in the entity. Open ImageSaver.m
and modify the import statements to this:
#import "ImageSaver.h" #import "Beer.h" #import "BeerDetails.h" |
Now you’ll need to uncomment the line found within the IF statement:
if ([imgData writeToFile:jpgPath atomically:YES]) { beer.beerDetails.image = path; } |
The ImageSaver class is now fully ready to accept an image, and save it to the phone’s documents directory, and the path to the BeerDetails
object.
From here, the user has two options: Cancel or Done, which saves the new beer. When you created the view, a Beer entity was created, and inserted in the managedObjectContext. Canceling should delete the Beer object. Find cancelAdd
, and add the following:
- (void)cancelAdd { [self.beer deleteEntity]; [self.navigationController popViewControllerAnimated:YES]; } |
MagicalRecord provides a nice method for deleting an entity, which automatically removes the entity from the managedObjectContext. After deleting, the user will return to the main list of beers.
If the user chooses Done, it will save the beer and return to the main list. Find addNewBeer
. It simply pops the view controller, going back to the list. When the view disappears, it will call viewWillDisapper:
. This in turn calls saveContext
.
Right now saveContext
is empty, so you’ll need to add some code to save your entity. Add the following to saveContext
:
- (void)saveContext { [[NSManagedObjectContext defaultContext] saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) { if (success) { NSLog(@"You successfully saved your context."); } else if (error) { NSLog(@"Error saving context: %@", error.description); } }]; } |
There’s a lot going on here in just a few lines of code! In AppDelegate.m, you set up the Core Data stack with MagicalRecord. This created a default managedObjectContext that the entire app can access. When you created the Beer and BeerDetails entities, they were inserted into this defaultContext . MagicalRecord allows you to save any saveToPersistentStoreWithCompletion:
. The completion block gives you access to an NSError object, if the save failed. Here, you’ve added a simple if/else block that logs what happens after you try to save the defaultContext.
Are you ready to test it out? Go ahead and run your app! Select the + button, fill out any information you want. Then select done.
When you do, you’ll notice you don’t see your new beer show up in the list. Don’t fret, it’s not broken and you’ll learn how to fix this in just a bit. You also might notice your debugger has a whole bunch of information in it. Contrary to what your logic might say, this is really good for you!
When the app started, MagicalRecord logged four things during the Core Data stack setup. It is showing that the Core Data setup process happened, and that it created a defaultContext. This is the same defaultContext that was referenced earlier when you saved your beer object.
The next several logs come from when you selected Done and performed a Save with MagicalRecord. You can see the following were logged during your save:
MagicalRecord logs a lot of information for you. If you’re having a problem, or something isn’t behaving as expected, you should check your logs for some really useful information.
Note:
While I do not recommend it, if for you really must see what happens when you disable the MagicalRecord logging, you can go to MagicalRecord.h, and change line 17 from:
#define MR_ENABLE_ACTIVE_RECORD_LOGGING 1 |
to:
#define MR_ENABLE_ACTIVE_RECORD_LOGGING 0 |
If this app is going to be worth all this work, you’ll need to be able to see the beers you’ve added. Open MasterViewController.m, and import Beer.h, and BeerDetails.h at the top.
In order to get all of the beers’ Core Data saved, you’ll need to do a fetch. In viewWillAppear:
, add the fetch just before the call to reloadData
.
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // Check if the user's sort preference has been saved. ... [self fetchAllBeers]; [self.tableView reloadData]; } |
When the view loads the first time, or when returning from viewing or adding a beer, it will fetch and load all the beers into the table.
Find fetchAllBeers
, and add the following:
- (void)fetchAllBeers { // 1. Get the sort key NSString *sortKey = [[NSUserDefaults standardUserDefaults] objectForKey:WB_SORT_KEY]; // 2. Determine if it is ascending BOOL ascending = [sortKey isEqualToString:SORT_KEY_RATING] ? NO : YES; // 3. Fetch entities with MagicalRecord self.beers = [[Beer findAllSortedBy:sortKey ascending:ascending] mutableCopy]; } |
The MasterViewController allows a user to sort beers by rating – ordered from a “5-beer” rating to a “1-beer” (Get it? Beers instead of stars? Uber kuhl!) rating, or alphabetically (A-Z). The first time the app launches, it creates an NSUserDefault
value to sort by rating, and establishes that as the default. In this method, you have:
Yep, that’s really all there is to it!
Once again, you are using a MagicalRecord method to interact with Core Data. findAllSortedBy:ascending
is just one of the many ways to perform a fetch of Core Data entities using MagicalRecord. Some others include (pay attention – you’ll need to use one of these later):
findAllInContext:
– will find all entities of a type in context providedfindAll
– will find all entities on the current thread’s contextfindAllSortedBy:ascending:inContext:
– similar to the one used earlier, but limited to the provided contextfindAllWithPredicate:
– allows you to pass in an NSPredicate to search for objects.findAllSortedBy:ascending:withPredicate:inContext:
– allows a sort to be done, with an ascending flag, in a particular context. It also allows you to pass in an NSPredicate for filtering.There are many, many others you can take advantage of – just check out NSManagedObject+MagicalFinders.m.
To add the beer’s name and rating to a cell, find configureCell:atIndex:
, and add the following:
- (void)configureCell:(UITableViewCell*)cell atIndex:(NSIndexPath*)indexPath { // Get current Beer Beer *beer = self.beers[indexPath.row]; cell.textLabel.text = beer.name; // Setup AMRatingControl AMRatingControl *ratingControl; if (![cell viewWithTag:20]) { ratingControl = [[AMRatingControl alloc] initWithLocation:CGPointMake(190, 10) emptyImage:[UIImage imageNamed:@"beermug-empty"] solidImage:[UIImage imageNamed:@"beermug-full"] andMaxRating:5]; ratingControl.tag = 20; ratingControl.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; ratingControl.userInteractionEnabled = NO; [cell addSubview:ratingControl]; } else { ratingControl = (AMRatingControl*)[cell viewWithTag:20]; } // Put beer rating in cell ratingControl.rating = [beer.beerDetails.rating integerValue]; } |
Now find prepareForSegue:sender:
, and within the if statement that checks if the segue identifier is “editBeer,” add:
if ([[segue identifier] isEqualToString:@"editBeer"]) { NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow]; Beer *beer = self.beers[indexPath.row]; upcoming.beer = beer; } |
This will pass the Beer object to the BeerViewController, so that it displays the Beer’s information, allowing you to edit it.
Go ahead and run your project again.
You’ll now see the beer you added earlier, with its rating. You can also select the beer and edit its information. When you go back, the table will be updated! Sehr gut!
Try viewing an individual Beer, and without editing information, go back to the main list. Take a look at the log. You should see:
-[NSManagedObjectContext(MagicalSaves) MR_saveWithOptions:completion:](0x8b6bfa0) NO CHANGES IN ** DEFAULT ** CONTEXT - NOT SAVING |
When you leave the details view, the code you’ve written will save the default context in viewWillDisappear:
. However, since no changes were made, MagicalRecord recognizes there is no need to perform a save operation, and so it skips the process. The benefit of this is there is no need for you to think about whether you need to save – just try and save, and let MagicalRecord figure it out for you.
There are a few more things you’ll want the app to do for your users – like letting them delete beers, pre-populatin the list with your favorite beers and perform searches.
In MasterViewController.m, find tableView:commitEditingStyle:forRowAtIndexPath:
, and add the following code:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { Beer *beerToRemove = self.beers[indexPath.row]; // Remove Image from local documents if (beerToRemove.beerDetails.image) { [ImageSaver deleteImageAtPath:beerToRemove.beerDetails.image]; } // Deleting an Entity with MagicalRecord [beerToRemove deleteEntity]; [self saveContext]; [self.beers removeObjectAtIndex:indexPath.row]; [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; } } |
Notice that there is a call to saveContext
. You’ll need to add some code there to make sure the delete went through. You’ve already done this once – can you figure out how to make it work? Ready……? Go!
Solution Inside | SelectShow> | |
---|---|---|
Add the following code to saveContext:
Since you don’t technically need to know when it finishes, you can use one of the other MagicalRecord options to save the managedObjectContext. |
Run the app and delete a beer (using the traditional swipe of a cell). If you re-launch the app and the beer is still gone, you did something very right! It saved your change, and that means you’ve used MagicalRecord correctly. Pat yourself on the back!
It might be nice to give your users some initial data so they can see how easily they can keep track of their favorite beers. In AppDelegate.m, import Beer.h, BeerDetails.h. Then, just after you setup the Core Data stack, add the following:
// Setup App with prefilled Beer items. if (![[NSUserDefaults standardUserDefaults] objectForKey:@"MR_HasPrefilledBeers"]) { // Create Blond Ale Beer *blondAle = [Beer createEntity]; blondAle.name = @"Blond Ale"; blondAle.beerDetails = [BeerDetails createEntity]; blondAle.beerDetails.rating = @4; [ImageSaver saveImageToDisk:[UIImage imageNamed:@"blond.jpg"] andToBeer:blondAle]; // Create Wheat Beer Beer *wheatBeer = [Beer createEntity]; wheatBeer.name = @"Wheat Beer"; wheatBeer.beerDetails = [BeerDetails createEntity]; wheatBeer.beerDetails.rating = @2; [ImageSaver saveImageToDisk:[UIImage imageNamed:@"wheat.jpg"] andToBeer:wheatBeer]; // Create Pale Lager Beer *paleLager = [Beer createEntity]; paleLager.name = @"Pale Lager"; paleLager.beerDetails = [BeerDetails createEntity]; paleLager.beerDetails.rating = @3; [ImageSaver saveImageToDisk:[UIImage imageNamed:@"pale.jpg"] andToBeer:paleLager]; // Create Stout Beer *stout = [Beer createEntity]; stout.name = @"Stout Lager"; stout.beerDetails = [BeerDetails createEntity]; stout.beerDetails.rating = @5; [ImageSaver saveImageToDisk:[UIImage imageNamed:@"stout.jpg"] andToBeer:stout]; // Save Managed Object Context [[NSManagedObjectContext defaultContext] saveToPersistentStoreWithCompletion:nil]; // Set User Default to prevent another preload of data on startup. [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"MR_HasPrefilledBeers"]; [[NSUserDefaults standardUserDefaults] synchronize]; } |
The starter app you downloaded at the beginning of this tutorial includes four frosty beer images. Here, you just create four different beers, and save them. Storing a flag in NSUserDefaults makes sure the app pre-fills the data model only once when it launches for the very fist time.
Run the app again, so you can see all the new beers. Try deleting one and re-launching the app; it won’t come back. If you want to see all the sample beers again, delete the app from the simulator or device, and re-launch.
Now that you have more than 1 beer, test the search capabilities. The starter app already included a Search Bar, – scroll to the top of the table to see it. The only thing you need to add is the logic to search the list of beers.
Earlier, you used a MagicalRecord helper method to do a fetch of all beers, and it simply returned all of the beers. Now, you want to retrieve all the beers that match a specific search term.
For this, you’ll need to use an NSPredicate. Earlier, this tutorial explained a method for fetching with an NSPredicate – can you guess how to do the search? The logic should live inside of doSearch
in the MasterViewController.m file.
Solution Inside | SelectShow> | |
---|---|---|
Add the following to doSearch:
For other methods to fetch results, check the MagicalRecord header files. |
Run the app again and drag the beer list down until the search bar is revealed. Search for one of the beers in the list, then search for one that isn’t in the list. Do you get the expected behavior?
Hopefully, this MagicalRecord tutorial showed you how easy it is to get up and running with MagicalRecord. It really helps cut down the boilerplate! The fundamentals you’ve explored in this tutorial can help you develop all kinds of apps that help users keep track of things they like with pictures, notes, and ratings. Enjoy!
You can download the finished project here, which could be helpful if you get stuck somewhere.
If you want to develop further the BeerTracker project here follow few ideas to get you started:
hasAtLeastOneEntity
method in MagicalRecord and use it. countOfEntitiesWithPredicate:
method.truncateAll
method from MagicalRecordIf you have any questions or comments about this tutorial, please join the forum discussion below!
MagicalRecord Tutorial for iOS is a post from: Ray Wenderlich
The post MagicalRecord Tutorial for iOS appeared first on Ray Wenderlich.
This February, a lot of us have been snowed in. And what better to do than curl up on the couch with some great apps made by the community!
This month, we have some great apps to check out:
So put down those snow shovels, and pick up these great apps!
Game of Cakes is a very cute tetris style puzzle game.
You are a pastry chef who needs to arrange his cakes just right in the box before handing them off to the hungry customer. And wow, those cute little cakes sure are appreciative when you get it right!
With over 130 levels, it will definitely keep you entertained for a while. And bonus points for the fun music!
We all want to keep an eye on our apps after release. The Stat App helps with that.
The Stat App integrates with AppFigures offering you a clean, simple, client for your iPhone. Never be without the latest sales and downloads of your apps. You can check them by country or just get a weekly, monthly, or alltime total.
What are you waiting for? The first app is free!
Quiz Duels is fun app to brush up on your trivia.
Quiz Duels uses GameCenter to pit you against friends and random players in epic duels of knowledge. All you have to do is answer the question before your opponent and you win.
There are weekly tournaments, plenty of categories, and a slick interface to make sure this is your favorite trivia app yet.
Hexuma is mathematic puzzle game I’ve been waiting for!
Hexuma is simple. First hexgons appear holding numbers of mathematic operations. You start at the green hexagon and drag across to the red. All you have to do is make sure the equation you create is balanced when you reach the end.
Challenge GameCenter friends on the more than 300 levels while racing against the clock for the highest scores.
JoPlan lets you plan out your next trip before you go with multiple destinations.
JoPlan is a great app if you have multiple stops to make on the way as it finds the optimum route for you to hit all your stops.
JoPlan supports Google Maps for turn by turn navigation or can be used on its own as an overview navigation.
Upside Down Run is a side scrolling platformer with a twist — or should I say a flip. :P
While running, you can swipe up or down to start running above or below ground. Each side has its own obstacles and benefits. By switching back and forth intelligently you can run the farthest.
Its got some cool physics and graphics. I especially like running into snowmen because they bounce back. :]
This poor cat is the victim of some serious rat bullying. They won’t stop plaguing him. ;]
Your job is to build defenses with whatever’s available from crates, to bombs, to wrecking balls. You’ve got to build a fort for the kitty so the rats can’t get to him.
The graphics are cute and the physics keep it interesting as you’re bombarded by evil rats.
Humid is a weather app for the minimalist.
Humid gives you the most important information at a glance, but indepth temperatures, forecasts, and other information is only a tap away.
Humid also has a text to speech feature allowing you to listen to your weather rather than read it. Very cool for those quick checks while you put on your coat.
Orbsorb is an awesome arcade style game for iOS.
You control a single Orbsorber that is bombarded with all sorts of orbs. Your goal is to block the unwanted orbs and absorb the same color orbs.
The game has proven extremely addicting. Its got a fun techno soundtrack and GameCenter integration to keep things interesting.
Tommy’s Farm is a simple app for kids that lets them interact with a farm on their iPads.
Its got some very cute illustrations and animations along with its various activities from puzzles to harvesting corn.
I let my nephew have a go at it and he loved interacting with the animals like feeding the horse or washing the pig.
No ads or in app purchases keep the game safe for your little kiddos.
Zest is an app for wasting time. No other app could consume more of it needlessly. But Gifs and Memes are irresistable.
Zest has the best of trending Gifs and Memes. You can scroll through hundreds of awesome Gifs and Memes then share the best with your friends over iMessage, Twitter, and more.
Mark the classics as favorites and search for that one you just need to see again.
Kroms is an fun arcade game where your job as a chocoskyscraper builder is to capture the kroms flooding the construction site.
The easiest way for Tony to do this is to capture them in small rooms. The smaller the better to teach the Kroms a lesson.
Simple swipe controls, colorful graphics, and a fun soundtrack make this game a hit. It takes just the right amount of reflex and strategy combined to breeze through the 90+ levels.
I had some apps I couldn’t review. Don’t feel bad, it wasn’t a popularity contest or even a rating contest. Forum members get priority, among other factors. I make time to try out every app that comes across my screen, one day I’ll find time to write about them all too!
As expected, I really enjoyed your apps – it’s great fun to see what fellow readers make each month.
If you’ve never made an app, we’ve got you covered! Check out our free tutorials to become an iOS star. What are you waiting for – I want to see your app next month!
If you’ve already made the next great app, let me know about it! Submit here!
Readers’ App Reviews – February 2014 is a post from: Ray Wenderlich
The post Readers’ App Reviews – February 2014 appeared first on Ray Wenderlich.
Learn how to add detect and respond to collisions using physics shapes in this tutorial that teaches you how to make a game like Flappy Bird.
Video Tutorial: How To Make a Game Like Flappy Bird Part 3: Collision Detection is a post from: Ray Wenderlich
The post Video Tutorial: How To Make a Game Like Flappy Bird Part 3: Collision Detection appeared first on Ray Wenderlich.
Users expect a high level of polish from iOS apps, so it’s up to you to design, develop and test your apps to meet ever-rising expectations. Think about it for just a moment: How much time have you poured into conducting basic manual user interface testing? You know the drill…launching your app from Xcode, and incessantly tapping the same series of buttons to make sure no regressions slipped into your design. Surely, there are other things you’d rather do?
Instead, consider that the enhanced test UI in Xcode 5 and continuous integration support in OS X Server demonstrate Apple’s commitment to provide developers the best tools. That’s great, you might say, but how do you automate testing simple user actions, like ensuring a double-tap or swipe at the right spot brings up the correct view? Even test scripts and bots don’t have capacitive touch fingers to swipe across the screen…or…do they?
In this tutorial, you’ll learn all about KIF (“Keep it Functional”), an open-source user interface testing framework. With KIF, and by leveraging the Accessibility APIs in iOS, you’ll be able to write tests that simulate user input, like touches, swipes and text entry. These tests give your apps an automated, real-world user interface workout, and help keep your mind at ease so you can just focus on developing that killer app – not spending half a lifetime on UI testing.
Let’s get testing!
The sample project is a timer app called Solanum (named after the tomato plant species) based on the Pomodoro time-boxing method. Here’s how it works: you work for a defined number of minutes, take a break, then repeat. After several of these cycles, you take a longer break. The app is a just a simple timer that keeps track of time periods. Feel free to use the app afterwards to help make you more productive during your own development!
Download and unzip the starter project archive here. Note that KIF is a separate project, and its role is to build a library for Solanum’s test target. You’ll need to double-click solanum.xcworkspace to open the project in Xcode rather than solanum.xcodeproj. Look for two projects to open in the in the project navigator, it should like the example below.
Set the app target to solanum, and select either the 3.5-inch or 4-inch iPhone Simulator target. Don’t use the 64-bit build; at the time of writing this tutorial KIF didn’t appear to be fully compatible. Build and run the app. Look around, then switch to the Settings tab.
The app has a debug mode that accelerates time, so you can set a 20-minute timer and it will help with testing by ticking by in 10 seconds. This is just to aid testing the app. You wouldn’t want each test run to take 20 minutes!
Turn on Debug Mode switch to speed up the timer. Next, tap the Clear History button, and then tap Clear on the confirmation alert view. These steps will ensure you’re starting out in a clean, test-friendly environment. Return to Xcode and stop the app.
In the Project Navigator, expand the solanum project. Right-click the UI Tests folder and click New File… to add your first test case.
Select iOS\Cocoa Touch\Objective-C class and click Next. Name the class UITests
and make it a subclass of KIFTestCase
.
Click Next and make sure the files are be added to the UI Tests target, not the solanum target. Finally, click Create on the following screen to save the files.
KIFTestCase
is a subclass of SenTestCase
. That means you have most of the standard OCUnit testing methods and mechanisms available, in case you’re already familiar with unit testing.
Open UITests.m and add the following method after the @implementation
line:
- (void)beforeAll { [tester tapViewWithAccessibilityLabel:@"Settings"]; [tester setOn:YES forSwitchWithAccessibilityLabel:@"Debug Mode"]; [tester tapViewWithAccessibilityLabel:@"Clear History"]; [tester tapViewWithAccessibilityLabel:@"Clear"]; } |
beforeAll
is a special method that is called exactly once, before all of the tests run. You can set up any instance variables or initial conditions for the rest of your tests here.
The tester
object is a special shortcut to an instance of the KIFUITestActor
class. That class includes the methods that will simulate user activity, including tapping and swiping on views.
tapViewWithAccessibilityLabel:
might be the most common test action method. As the name suggests, it simulates tapping the view with the given accessibility label. In most cases, such as for buttons, the accessibility label matches the visible text label. If not, as you’ll see in the next section, you’ll need to set the accessibility label manually.
Some controls, such as UISwitch
, are more complicated and need more than just simple taps. KIF provides a specific setOn:forSwitchWithAccessibilityLabel:
method to change the state of a switch.
In summary, this method has four steps for the test actor:
Do these steps seem familiar? They should be! They’re what you did manually in the previous section!
Run the tests by going to Product\Test or hitting Command-U on the keyboard. You should see the app launch; then you’ll see KIF take over, automatically enable debug mode and clear the history.
If you have notifications enabled, Xcode will also tell you the test status:
Sometimes the test runner, or KIF, can get a little finicky and will refuse to run your tests, in which case you’ll just see a blank simulator screen. If this happens:
This process ensures the Simulator is running and that you’re working with the latest build. After going through the above steps, try running the tests again. The problems should be gone.
If you continue to have trouble, check out the KIF troubleshooting steps.
Now that you have a pre-test action in beforeAll
, it’s time to add your first test!
The app has a standard tab bar controller layout with a UINavigationController
inside each of the three tabs. To warm up for the next exercise, you’ll determine if:
Tab bar buttons automatically set accessibility labels to be the same as their text label, so KIF will be able to find the History, Timer, and Settings tab bar buttons with the labels “History”, “Timer”, and “Settings”.
The History tab has a table view that shows all the tasks performed with the timer. Open HistoryViewController.m from the solanum group and add these lines to the end of viewDidLoad
:
[self.tableView setAccessibilityLabel:@"History List"]; [self.tableView setIsAccessibilityElement:YES]; |
This will set the table view’s accessibility label, so that KIF can find it. Usually, a table view is only accessible if it’s empty. If there are table view cells they’re a more likely target, so the table view itself will hide from the accessibility API. Essentially, the accessibility API assumes, by default, that the table view isn’t important. This is likely the case in terms of accessibility, but if you want to reference the table view in KIF then it needs to be accessible as well. The setIsAccessibilityElement:
call ensures the table view is always accessible, regardless of its contents.
Depending on the app, accessible non-empty table views can actually make things more difficult if for users who use the accessibility features (e.g. VoiceOver). In your own apps, you can wrap lines of code between #ifdef DEBUG
and #endif
directives so they’re only compiled into your debug builds. The DEBUG
preprocessor macro is pre-defined in Xcode’s project templates.
The Timer tab has several controls you could look for, but the “Task Name” text field is conveniently at the top of the view. Rather than set the label through code, open Main.storyboard and find the Timer View Controller view. Select the task name text field.
Open the Utilities panel if it isn’t already up — and select the Identity Inspector. Hint: it’s the third icon from the left, or use the keyboard shortcut ‘⌥ ⌘ 3′.
Under Accessibility in the inspector, enter “Task Name” in the Label field. Stay sharp now, because accessibility labels are case-sensitive. Be sure to enter that exactly as shown with a capital T and N!
The Settings tab has already been set up the views with accessibility labels, so you’re all set to move to the next step!
In your own projects, you’ll need to continue to fill in the accessibility labels from code or in Interface Builder as you’ve done here. For your convenience, the rest of sample app’s accessibility labels are already set.
Back in UITests.m, add this method after beforeAll
:
- (void)test00TabBarButtons { // 1 [tester tapViewWithAccessibilityLabel:@"History"]; [tester waitForViewWithAccessibilityLabel:@"History List"]; // 2 [tester tapViewWithAccessibilityLabel:@"Timer"]; [tester waitForViewWithAccessibilityLabel:@"Task Name"]; // 3 [tester tapViewWithAccessibilityLabel:@"Settings"]; [tester waitForViewWithAccessibilityLabel:@"Debug Mode"]; } |
The test runner will look for all methods that start with the word “test” at runtime, and then run them in alphabetical order. This method starts with the name “test00″ so that it will run before the tests you’ll add later, because those names will start with “test10″, “test20″, etc.
Each of the three parts of the method will perform a similar set of actions: tap on a tab bar button, and check for the expected view to show on the screen. 10 seconds is the default timeout for waitForViewWithAccessibilityLabel:
. If the view with the specified accessibility label doesn’t show itself during that timeframe, the test will fail.
Run the tests by going to Product\Test or hitting Command-U. You’ll see the beforeAll
steps which will clear the History, and then test00TabBarButtons
will take over and switch to the History, Timer and Settings tabs in sequence.
Well, what do you know? You just wrote and ran an interface test, and saw your little app “drive” itself! Congrats! You’re on your way to mastering automated UI testing.
Sure, switching tabs is nifty, but it’s time to move on to more realistic actions: entering text, triggering modal dialogs and selecting a table view row.
The test app has some built-in presets that will change the work time, break time and number of repetitions to a set of recommended values. If you’re curious to see their definitions, have a look at presetItems
in PresetsViewController.m.
Selecting a preset could be a test of its own, but that action is more efficient when it is a part of other tests. In this case, it’s worth splitting it out into a helper method.
Add the following method to the implementation block of UITests.m:
- (void)selectPresetAtIndex:(NSInteger)index { [tester tapViewWithAccessibilityLabel:@"Timer"]; [tester tapViewWithAccessibilityLabel:@"Presets"]; [tester tapRowInTableViewWithAccessibilityLabel:@"Presets List" atIndexPath:[NSIndexPath indexPathForRow:index inSection:0]]; [tester waitForAbsenceOfViewWithAccessibilityLabel:@"Presets List"]; } |
The first step here is to switch to the Timer tab so that you’re in the right place. Then tap the Presets button in the navigation bar. When the “Presets List” table view appears, tap on the row at the specified index.
Tapping on the row will dismiss the view controller, so use waitForAbsenceOfViewWithAccessibilityLabel:
to ensure it vanishes before you continue.
Did you notice that this method doesn’t start with the word test? The test runner won’t automatically run it. Instead, you’ll manually call the method from within your own tests.
Now, add the following test method to UITests.m:
- (void)test10PresetTimer { // 1 [tester tapViewWithAccessibilityLabel:@"Timer"]; // 2 [tester enterText:@"Set up a test" intoViewWithAccessibilityLabel:@"Task Name"]; [tester tapViewWithAccessibilityLabel:@"done"]; // 3 [self selectPresetAtIndex:1]; // 4 UISlider *slider = (UISlider *)[tester waitForViewWithAccessibilityLabel:@"Work Time Slider"]; STAssertEqualsWithAccuracy([slider value], 15.0f, 0.1, @"Work time slider was not set!"); } |
KIF test actions have very readable names; see if you can figure out what’s going on here. Think of it as a…test! Yes, of course the pun is intentional. :]
OK, here’s what’s happening section by section:
In the final section of code, you’ll find a handy trick: waitForViewWithAccessibilityLabel:
. Not only will it wait for the view to appear, but it actually returns a pointer to the view itself! Here, you cast the return value to UISlider
to match up the proper type.
Since KIF test cases are also regular OCUnit test cases, you can call the standard STAssert
assertion macros. Assertions are run-time checks that cause the test to fail if some condition isn’t met. The simplest assertion is STAssertTrue
, which will pass if the parameter passed in is true.
STAssertEquals
will check that the first two parameters are equal. The slider value is a float
, so be careful about matching up types. Thus, the 15.0f
appears in the assertion. You also need to be careful about small inaccuracies in floating point representations. This is because floating point values cannot necessarily be stored 100% accurately. 15.0 might end up actually being stored as 15.000000000000001 for example. So STAssertEqualsWithAccuracy
is a better choice; its third parameter is the allowed variance. In this case, if the values are within +/- 0.1 of each other, the assertion will still pass.
Run the tests using Command-U. You should now see three sequences: beforeAll
clears the history, test00TabBarButtons
switches through each tab, and then your latest masterpiece in test10PresetTimer
will enter a task name and select a preset.
Another successful test! At this point, your test mimics users by tapping all kind of things and even typing on the keyboard, but there’s even more to come!
Here’s an example timer cycle a user of the app might choose: work for 8 minutes, take a 2 minute break, work for 8 minutes, take a 2 minute break, then work a final 8 minutes. At this point, you take a longer break and then restart the app when you’re ready.
The parameters for the above example are:
The next KIF test will enter these parameters, and then tap the “Start Working” button to start the timer. Add the following method to UITests.m, directly below the previous tests you added:
- (void)test20StartTimerAndWaitForFinish { [tester tapViewWithAccessibilityLabel:@"Timer"]; [tester clearTextFromAndThenEnterText:@"Test the timer" intoViewWithAccessibilityLabel:@"Task Name"]; [tester tapViewWithAccessibilityLabel:@"done"]; [tester setValue:1 forSliderWithAccessibilityLabel:@"Work Time Slider"]; [tester setValue:50 forSliderWithAccessibilityLabel:@"Work Time Slider"]; [tester setValue:1 forSliderWithAccessibilityLabel:@"Work Time Slider"]; [tester setValue:8 forSliderWithAccessibilityLabel:@"Work Time Slider"]; [tester setValue:1 forSliderWithAccessibilityLabel:@"Break Time Slider"]; [tester setValue:25 forSliderWithAccessibilityLabel:@"Break Time Slider"]; [tester setValue:2 forSliderWithAccessibilityLabel:@"Break Time Slider"]; } |
Because this test will run immediately after test10PresetTimer
(which sets the task name), you can use the clearTextFromAndThenEnterText:intoViewWithAccessibilityLabel:
variant rather than plain old enterText:intoViewWithAccessibilityLabel:
to clear out any existing text first.
Finally, there are several calls to setValue:forSliderWithAccessibilityLabel:
. This is the UISlider
specific method to set the new value. Note that the accuracy isn’t always very good. KIF actually simulates the touch event and swipes to set the new value; sometimes the pixel calculations are a little off. But that’s okay, because fingers aren’t all that accurate either!
You only need to set each slider’s value once. The multiple calls are just for kicks, and so you can see KIF changing the value over and over.
Run the tests using Command-U.
The remaining things to test in the UI are the UIStepper
control to set the number of repetitions, and the “Start Working” button. The “Start Working” button is easy – you can use tapViewWithAccessibilityLabel:
to simulate a tap. But for the UIStepper
, we need to take a little detour.
The UIStepper
control has two halves, as shown below, so at this point it’s unclear what will happen if you just call tapViewWithAccessibilityLabel:
.
KIF will start out by trying to tap the center of the control. If it’s not a tappable area, it then tries points at the top-left, top-right, bottom-left, and then bottom-right. It turns out that tapping that center border between the plus and minus triggers the plus, so it will increment the stepper.
But what if you wanted to decrement the stepper? There are some workarounds, such as digging into the subviews to find the minus button. The other alternative is to use KIF’s tapScreenAtPoint:
test action, which will simulate a tap at any arbitrary point on the screen.
How’s your CGGeometry
knowledge? Can you figure out how to calculate the window coordinates of the plus and minus buttons? Ready to prove you’re awesome? Why not try this challenge? It is totally optional, and you can skip ahead to the code with the calculations below. But if you want to test your skills, try to code the calculations before peeking at the answer. By the way, the answer and full explanation are in the solution box.
[Note that you're going to add this code to the test shortly. This task is just to test your math and CGGeometry skills!]
Solution Inside: UIStepper Geometry | SelectShow> | |||
---|---|---|---|---|
First, you need a reference to the UIStepper :
Then you can find the center point:
The Now that you have the center point relative to the window, you can decrease the x-coordinate to get the minus button, or increase it to get the plus button.
If you increase or decrease by one-fourth the width of the Voilà! Two points, representing the center of the plus and minus buttons inside a |
Tapping the screen at an arbitrary point is sort of a last resort, but sometimes it is the only way to test your UI. For example, you might have your own custom controls that don’t implement the UIAccessibility
Protocol.
OK, so now you’re at the last steps of this Timer test. Are you realizing the awesome potential of UI testing with KIF? Good!
The final steps are to set the number of repetitions, and then start the timer. Add the following code to the end of test20StartTimerAndWaitForFinish
in UITests.m:
// 1 UIStepper *repsStepper = (UIStepper*)[tester waitForViewWithAccessibilityLabel:@"Reps Stepper"]; CGPoint stepperCenter = [repsStepper.window convertPoint:repsStepper.center fromView:repsStepper.superview]; CGPoint minusButton = stepperCenter; minusButton.x -= CGRectGetWidth(repsStepper.frame) / 4; CGPoint plusButton = stepperCenter; plusButton.x += CGRectGetWidth(repsStepper.frame) / 4; // 2 [tester waitForTimeInterval:1]; // 3 for (int i = 0; i < 20; i++) { [tester tapScreenAtPoint:minusButton]; } [tester waitForTimeInterval:1]; [tester tapScreenAtPoint:plusButton]; [tester waitForTimeInterval:1]; [tester tapScreenAtPoint:plusButton]; [tester waitForTimeInterval:1]; // 4 [KIFUITestActor setDefaultTimeout:60]; // 5 [tester tapViewWithAccessibilityLabel:@"Start Working"]; // the timer is ticking away in the modal view... [tester waitForViewWithAccessibilityLabel:@"Start Working"]; // 6 [KIFUITestActor setDefaultTimeout:10]; |
Here’s what’s going to happen in this final phase:
UIStepper
. The explanation for this code is contained in the spoiler box above.waitForTimeInterval:
add a delay so you can see the stepper value change – otherwise it goes by too fast for human eyes.Once you save the file, you’ll see a little diamond-shaped icon next to the method declaration:
This is a button to run a single test. So rather than firing up the entire test suite, you can run this one method in the test environment. Neat! Click on the diamond to run the test; you should see the simulator start up and then see the test run. Sit back and watch as it enters the task name, the sliders move and the timer ticks away. Without lifting a finger, you’re testing the UI.
Success! Now you can add successfully setting up tests to manipulate a variety of UI controls to your list of accomplishments.
If you run a single test with the diamond button, it only runs the single method and doesn’t call beforeAll
at the beginning. If your test depends on beforeAll
, you’ll still need to run the full test suite.
The “Get to Work!” modal view controller has a Give Up button that lets the user cancel the timer cycle. You’ll still be able to measure the number of minutes worked, even if you pull the plug on the test early. This data still gets logged in the history, but marked with a flag to indicate the full cycle didn’t finish.
But you don’t have to take my word for it, you can test it for yourself. Just do something very similar to the previous test. Set the timer parameters, tap the Start Working button, and then tap the Give Up button.
Don’t tap “Give Up” right away – the timer needs to tick for a bit so the app can create the history. So you can either hover over the test and kill it manually, or, you can program it to stop at a time and place of your choosing. If you enjoy hovering, feel free to skip this challenge. However, if you like to program apps to do the dirty work for you, try it out! Do you know how would add a little delay in between “Start Working” and “Give Up”?
Solution Inside: Adding a little delay | SelectShow> |
---|---|
You can call your friend waitForTimeInterval: to add a delay in your test.
|
Add the following test method to UITests.m directly below your other tests:
- (void)test30StartTimerAndGiveUp { [tester tapViewWithAccessibilityLabel:@"Timer"]; [tester clearTextFromAndThenEnterText:@"Give Up" intoViewWithAccessibilityLabel:@"Task Name"]; [tester tapViewWithAccessibilityLabel:@"done"]; [self selectPresetAtIndex:2]; [tester tapViewWithAccessibilityLabel:@"Start Working"]; [tester waitForTimeInterval:3]; [tester tapViewWithAccessibilityLabel:@"Give Up"]; [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"Start Working"]; } |
After making sure you’re on the right tab, set the task name and select a preset. Then, start the timer and wait 3 seconds before giving up.
The final line of the method waits for the modal view controller to go away and for the main interface to return. Remember, the default timeout is 10 seconds, but it really shouldn’t take that long – tapping the “Give Up” button should dismiss the modal view controller immediately.
In the previous test, you used the class method setDefaultTimeout:
to set the timeout value globally for all test actions. Here, you’re calling usingTimeout:
to set a custom timeout for just this single step.
Save the file and click on the test’s diamond icon to run only this test. When the timer starts, you’ll see it tick for three seconds before it gives up and returns to the main screen.
The History tab has not received much attention yet, but its time in the limelight is now. If you’ve worked through the exercises, you should have at least one entry in the history. Build and run the app, and switch to the History tab.
The History table view implements the new iOS 7 delete gesture — the one where you swipe the row to the left, and then tap on the “Delete” button that appears. There’s your next test! This requires at least one item in the History, so you need to be careful about running this one test individually. You need to ensure there is something in the history, otherwise there would be nothing to test! To be safe, you’ll run the entire test suite, since the earlier tests will create some history items for you to play around with. Remember the tests will run in alphabetical order. So you can be assured that the previous tests will create something to test the history screen with.
If you take a peek at tableView:cellForRowAtIndexPath:
in HistoryViewController.m, you’ll see that each cell gets an accessibility label such as “Section 0 Row 3″. This just helps KIF find the row. It is a poor label for real-world accessibility, so don’t use this in a real app. In this sample project, this has been #ifdef
‘d to debug builds only. You should do similar if you use this technique in your own apps. Be sure in release builds to set the accessibility label to something useful for users of accessibility features.
Now, open UITests.m and add the following test method:
- (void)test40SwipeToDeleteHistoryItem { // 1 [tester tapViewWithAccessibilityLabel:@"History"]; // 2 UITableView *tableView = (UITableView *)[tester waitForViewWithAccessibilityLabel:@"History List"]; NSInteger originalHistoryCount = [tableView numberOfRowsInSection:0]; STAssertTrue(originalHistoryCount > 0, @"There should be at least 1 history item!"); // 3 [tester swipeViewWithAccessibilityLabel:@"Section 0 Row 0" inDirection:KIFSwipeDirectionLeft]; [tester tapViewWithAccessibilityLabel:@"Delete"]; // 4 [tester waitForTimeInterval:1]; NSInteger currentHistoryCount = [tableView numberOfRowsInSection:0]; STAssertTrue(currentHistoryCount == originalHistoryCount - 1, @"The history item was not deleted :["); } |
Here’s what’s going on in the above method:
Run the entire test suite using Command-U to see everything run in sequence.
Your tests now cover the basic flow of the app – from resetting data to running the timer a few times, to verifying and deleting history.
If you were to continue developing the app, these tests would be a good baseline to ensure there are no regressions in the interface. That means you also need to update the tests every time the interface changes – tests are like specifications for your app and they need to stay up-to-date to be useful.
By now, you should have a good sense of the possibilities of KIF and your mind should be racing with ideas about how to utilize this highly functional testing for your own apps. To get started with adding KIF to your project, check out the documentation. You can add KIF manually or use the very convenient CocoaPods dependency manager.
What’s cool about KIF is that it is an open-source project and new features are constantly in development. For example, at the time of writing this tutorial, the next release will likely include a feature to take a screenshot of your app programmatically and save it to disk. This means you should be able to run the tests and review key points in the process by reviewing screenshots at your leisure. Doesn’t that sound a million times better than hovering and watching KIF tap and swipe its way through your app? KIF just keeps getting better and better, so learning how to use it is a wise investment in yourself.
Finally, since KIF test cases are subclasses of OCUnit and run within the standard Xcode 5 test framework, you can run the tests through your continuous integration process. Then you can truly have bots with quasi-touch fingers testing your app while you do other things! Woo-hoo!
You can download the final project archive here. If you have any questions or comments, please join the forum discussion below. Happy testing!
iOS UI Testing with KIF is a post from: Ray Wenderlich
The post iOS UI Testing with KIF appeared first on Ray Wenderlich.
Learn how display a "juicy" scoreboard screen including social sharing in this tutorial that shows you how to make a game like Flappy Bird.
Video Tutorial: How To Make a Game Like Flappy Bird Part 4: Keeping Score is a post from: Ray Wenderlich
The post Video Tutorial: How To Make a Game Like Flappy Bird Part 4: Keeping Score appeared first on Ray Wenderlich.
NSURLProtocol is like a magic key to the URL. It lets you redefine how Apple’s URL Loading System operates, by defining custom URL schemes and redefining the behavior of existing URL schemes.
Does that sound magical? It should. Because, if you look for it, I’ve got a sneaky feeliing you’ll find URLs — much like love — are all around us. What does UIWebView use? URLs. What’s used for video streaming with MPMoviePlayer? URLs. How do you send someone to your app on iTunes, initiate FaceTime or Skype, launch another app on the system, or even embed an image in an HTML file? With a URL. Have a peek at NSFileManager and notice what many of its file-manipulation methods require and return — URLs.
NSURLProtocol
is awesome because it lets your app speak the language of love…um, URLs. It can also be used to make deep changes to how they are processed. You can add new networking protocols that existing URL-based components and libraries can automatically use. Or, you can modify how existing protocols work. For instance, you can log all network requests, modify outgoing and incoming information streams or service some requests differently and transparently, e.g., from a cache.
By default, iOS URL Loading System supports the following schemes:
In this NSURLProtocol tutorial, you’ll learn how to define a protocol handler that modifies URL schemes. It will add a rough and ready transparent caching layer, by storing retrieved resources in Core Data. By enabling it, an ordinary UIWebView can then take on the role of a browser by caching downloaded pages for offline viewing at a later time.
Before you dive in head first, you’ll need a basic understanding of networking concepts and familiarity with how NSURLConnection
works. If you are not currently familiar with NSURLConnection
then I suggest reading this tutorial and/or this document by Apple.
However, nothing about NSURLProtocol
is specific to NSURLConnection
. You can implement a custom protocol that uses:
Custom protocols can configure the behavior of NSURLConnection
, and also of the new NSURLSession
-based networking facilities. Cool!
So are you ready to learn what you can do with NSURLProtocol
? Good, go pour yourself a cuppa something and settle in for a meaty, mind-broadening discussion and step-by step exercise. The first thing to discuss in what it is and how it works.
A set of classes known as the URL Loading System, handles URL requests. You need to know them to find out how iOS handles your app’s requests to load a URL-based resource.
At the heart of the URL Loading System is the NSURL
class. For network requests, this class tells what host your app is trying to reach and path to the resource at that host. In addition the NSURLRequest
object adds information like HTTP headers, the body of your message, etc.. The loading system provides a few different classes you can use to process the request, the most common being NSURLConnection
and NSURLSession
.
When you receive a response it comes back in two parts: metadata and data. The metadata is encapsulated in a NSURLResponse
object. It will tell you the MIME type, the text encoding (when applicable), the expected amount of data of your response and the URL that is the source of the information. The data arrives as NSData
objects.
Behind the scenes, when the loading system downloads information using a NSURLRequest
, it will create an instance of a NSURLProtocol
subclass, which represents your custom URL protocol. NSURLProtocol
is an abstract class that exists only to be extended in this way. You should never instantiate an NSURLProtocol
object directly.
Note: Remember that Objective-C doesn’t actually have abstract classes as a first class citizen. It’s only by definition and documentation that a class is marked as abstract.
Given the name of NSURLProtocol
you could be forgiven for thinking that it’s an Objective-C protocol. Strangely, it isn’t. It’s a class. But it is used in a way that is very similar to a protocol as it’s defining a set of methods that must be implemented by something that conforms to NSURLProtocol
. A protocol was probably not flexible enough for Apple, so they chose to use an abstract base class.
When a subclass of NSURLProtocol
handles a request, it’s the subclass’ job to create the NSURLResponse
objects to encapsulate the response. Once you have registered your own NSURLProtocol
, the loading system will search for the first one equipped to handle a specific NSURLRequest
.
How can you use NSURLProtocol
to make your app cooler, faster, stronger and jaw-droppingly awesome? Here are a few examples:
Provide Custom Responses For Your Network Requests:
It doesn’t matter if you’re making a request using a UIWebView
, NSURLConnection
or even using a third-party library (like AFNetworking, MKNetworkKit, your own, etc, as these are all built on top of NSURLConnection
). You can provide a custom response, both for metadata and for data. You might use this if you wanted to stub out the response of a request for testing purposes, for example.
Skip Network Activity and Provide Local Data:
Sometimes you may think it’s unnecessary to fire a network request to provide the app whatever data it needs. NSURLProtocol can set your app up to find data on local storage or in a local database.
Redirect Your Network Requests:
Have you ever wished you could redirect requests to a proxy server — without trusting the user to follow specific iOS setup directions? Well, you can! NSURLProtocol gives you what you want — control over requests. You can set up your app to intercept and redirect them to another server or proxy, or wherever you want to. Talk about control!!
Change the User-agent of Your Requests:
Before firing any network request, you can decide to change its metadata or data. For instance, you may want to change the user-agent. This could be useful if your server changes content based on the user-agent. An example of this would be differences between the content returned for mobile versus desktop, or the client’s language.
Use Your Own Networking Protocol:
You may have your own networking protocol (for instance, something built on top of UDP). You can implement it and, in your application, you still can can keep using any networking library you prefer.
Needless to say, the possibilities are many. It would be impractical (but not impossible) to list all the possibilities you have with NSURLProtocol
in this tutorial. You can do anything you need with a given NSURLRequest
before it’s fired by changing the designated NSURLResponse. Better yet, just create your own NSURLResponse
. You’re the developer, after all.
While NSURLProtocol
is powerful, remember that it’s not a networking library. It’s a tool you can use in addition to the library you already use. In short, you can take advantage of NSURLProtocol
‘s benefits while you use your own library.
Right, now on to some code…
Now it’s time to get your hands dirty! Next, you’ll develop a simple project so you have somewhere to get to know NSURLProtocol
. After completing this exercise you’ll know how to customize how an app loads URL data.
You’ll build an elementary mobile web browser, such as one that you might add to your next app. It will have a basic user interface that lets the user enter and go to a URL. The twist is that your browser will cache successfully retrieved results. This way the user can load pages he’s already visited in the twinkle of an eye, because the page won’t load from a network request, but from the app’s local cache.
You already know that fast page loads = happy users, so this is a good example of how NSURLProtocol
can improve your app’s performance.
These are the steps you’re going to go through:
If you’re not familiar with Core Data, you can take a look into our tutorial. However, the code in this tutorial should be enough to understand the possibilities of NSURLProtocol
. Using Core Data is just a simple way to implement the local cache, so it’s not essential to learn something useful here.
You can download the starter project here. As soon as the download is finished, unzip it and open the project file. It will look like this:
When you open the project, there are two main files. The first one is the Main.storyboard file. It has the UIViewController set up the way you need for implementation. Notice the UITextField (for URL input), UIButton (for firing the web requests) and UIWebView.
Open BrowserViewController.m. Here you’ll see the basic behavior set up for the UI components. This UIViewController implements the UITextViewDelegate, so you can fire the request when the user taps the return key. The IBAction for the button is pre-set to behave the same way as the return key. Last, the sendRequest method just takes the text from the textfield, creates a NSURLRequest object and sends calls the loadRequest: method from UIWebView to load it.
Once you’re familiarized with the app, build and run! When the app opens, enter “http://raywenderlich.com” and press the “Go” button. The UIWebView will load the response and display the results in the app. Pretty simple for a starting point. Now it’s time for you to stretch those finger muscles. Up next….coding!
Now it’s time to start intercepting all NSURLRequest’s fired by the app. For that, you’ll need to create your own NSURLProtocol
implementation.
Click File\New\File…. Select Objective-C class and hit the Next button. In the Class field, enter MyURLProtocol and in Subclass of field, enter NSURLProtocol. Finally press Next and then Create when the dialog appears.
Open MyURLProtocol.m and add this method to the class implementation:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { static NSUInteger requestCount = 0; NSLog(@"Request #%u: URL = %@", requestCount++, request.URL.absoluteString); return NO; } |
Every time the URL Loading System receives a request to load a URL, it searches for a registered protocol handler to handle the request. Each handler tells the system whether it can handle a given request via its +canInitWithRequest:
method.
The parameter to this method is the request that the protocol is being asked if it can handle. If the method returns YES
, then the loading system will rely on this NSURLProtocol
subclass to handle the request, and ignore all other handlers.
If none of the custom registered handlers can handle the request, then the URL Loading System will handle it by itself, using the system’s default behavior.
If you want to implement a new protocol, like foo://, then this is where you should check to see if the request’s URL scheme was foo. But in the example above, you’re simply returning NO
, which tells you your app cannot handle the request. Just hold on a minute, we’ll start handling them soon!
Now, register this protocol with the loading system. Open AppDelegate.m. Then add an import for MyURLProtocol.h at the top and insert the following code within the -application:didFinishLaunchingWithOptions:
method:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [NSURLProtocol registerClass:[MyURLProtocol class]]; return YES; } |
Now that you’ve registered the class with the URL Loading System, it will have the opportunity to handle every request delivered to the URL Loading system. This includes code which calls the loading system directly, as well as many system components that rely on the URL loading framework, such as UIWebView.
Now, build and run. Insert “http://raywenderlich.com” as the website, click on “Go” and check the Xcode console. Now, for every request the app needs to perform, the URL Loading System asks your class if it can handle it.
In the console you should see something like this:
2014-01-19 06:56:02.671 NSURLProtocolExample[903:70b] Request #0: URL = http://www.raywenderlich.com/ 2014-01-19 06:56:02.672 NSURLProtocolExample[903:70b] Request #1: URL = http://www.raywenderlich.com/ 2014-01-19 06:56:02.672 NSURLProtocolExample[903:70b] Request #2: URL = http://www.raywenderlich.com/ 2014-01-19 06:56:02.672 NSURLProtocolExample[903:70b] Request #3: URL = http://www.raywenderlich.com/ 2014-01-19 06:56:02.680 NSURLProtocolExample[903:70b] Request #4: URL = http://www.raywenderlich.com/ 2014-01-19 06:56:02.684 NSURLProtocolExample[903:1303] Request #5: URL = http://www.raywenderlich.com/ 2014-01-19 06:56:02.828 NSURLProtocolExample[903:330b] Request #6: URL = http://www.raywenderlich.com/ 2014-01-19 06:56:02.829 NSURLProtocolExample[903:330b] Request #7: URL = http://www.raywenderlich.com/ 2014-01-19 06:56:02.829 NSURLProtocolExample[903:330b] Request #8: URL = http://www.raywenderlich.com/ 2014-01-19 06:56:02.830 NSURLProtocolExample[903:330b] Request #9: URL = http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 2014-01-19 06:56:02.830 NSURLProtocolExample[903:1303] Request #10: URL = http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 2014-01-19 06:56:02.830 NSURLProtocolExample[903:330b] Request #11: URL = http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.8 2014-01-19 06:56:02.831 NSURLProtocolExample[903:1303] Request #12: URL = http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.8 2014-01-19 06:56:02.831 NSURLProtocolExample[903:330b] Request #13: URL = http://www.raywenderlich.com/wp-content/plugins/wp-polls/polls-css.css?ver=2.63 2014-01-19 06:56:02.839 NSURLProtocolExample[903:1303] Request #14: URL = http://www.raywenderlich.com/wp-content/plugins/wp-polls/polls-css.css?ver=2.63 2014-01-19 06:56:02.840 NSURLProtocolExample[903:330b] Request #15: URL = http://cdn1.raywenderlich.com/wp-content/plugins/powerpress/player.js?ver=3.8 2014-01-19 06:56:02.840 NSURLProtocolExample[903:1303] Request #16: URL = http://cdn1.raywenderlich.com/wp-content/plugins/powerpress/player.js?ver=3.8 2014-01-19 06:56:02.843 NSURLProtocolExample[903:330b] Request #17: URL = http://cdn5.raywenderlich.com/wp-content/plugins/swiftype-search/assets/install_swiftype.min.js?ver=3.8 |
For now, you’re just logging the string representation of the request’s URL and returning NO
, which means your custom class cannot handle the request. But if you look into the logs, you’ll see all the requests made from the UIWebView. It includes the main website (.html) and all the assets, such as JPEGs and CSS files. Every time the UIWebView needs to fire a request, it’s logged to the console before it’s actually fired. The count should show you a mountain of requests — likely over five hundred — because of all the assets on the Ray Wenderlich page.
“I love it when pages take forever to load” said no user, ever. So now you need to make sure your app can actually handle the requests. As soon as you return YES
in your +canInitWithRequest:
method, it’s entirely your class’s responsibility to handle everything about that request. This means you need to get the requested data and provide it back to the URL Loading System.
How do you get the data?
If you’re implementing a new application networking protocol from scratch (e.g. adding a foo:// protocol), then here is where you embrace the harsh joys of application network protocol implementation. But since your goal is just to insert a custom caching layer, you can just get the data by using a NSURLConnection.
Effectively you’re just going to intercept the request and then pass it back off to the standard URL Loading System through using NSURLConneciton.
Data is returned from your custom NSURLProtocol
subclass through a NSURLProtocolClient
. Every NSURLProtocol
object has access to it’s “client”, an instance of NSURLProtocolClient
. (Well, actually NSURLProtocolClient
is a protocol. So it’s an instance of something that conforms to NSURLProtocolClient
).
Through the client, you communicate to the URL Loading System to pass back state changes, responses and data.
Open MyURLProtocol.m. Add the following class continuation category at the top of the file:
@interface MyURLProtocol () <NSURLConnectionDelegate> @property (nonatomic, strong) NSURLConnection *connection; @end |
Next, find +canInitWithRequest:
. Change the return to YES
, like this:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { static NSUInteger requestCount = 0; NSLog(@'Request #%u: URL = %@', requestCount++, request.URL.absoluteString); return YES; } |
Now add four more methods:
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { return [super requestIsCacheEquivalent:a toRequest:b]; } - (void)startLoading { self.connection = [NSURLConnection connectionWithRequest:self.request delegate:self]; } - (void)stopLoading { [self.connection cancel]; self.connection = nil; } |
+canonicalRequestForRequest:
is an abstract method from NSURLProtocol
. Your class must implement it. It’s up to your application to define what a “canonical request” means, but at a minimum it should return the same canonical request for the same input request. So if two semantically equal (i.e. not necessarily ==) are input to this method, the output requests should also be semantically equal.
To meet this bare minimum, just return the request itself. Usually, this is a reliable go-to solution, because you usually don’t want to change the request. After all, you trust the developer, right?! An example of something you might do here is to change the request by adding a header and return the new request.
+requestIsCacheEquivalent:toRequest:
. is where you could take the time to define when two distinct requests of a custom URL scheme (i.e foo:// are equal, in terms of cache-ability. If two requests are equal, then they should use the same cached data. This concerns URL Loading System’s own, built-in caching system, which you’re ignoring for this tutorial. So for this exercise, just rely on the default superclass implementation.
-startLoading
and -stopLoading
are what the loading system uses to tell your NSURLProtocol
to start and stop handling a request. The start method is called when a protocol should start loading data. The stop method exists so that URL loading can be cancelled. This is handled in the above example by cancelling the current connection and getting rid of it.
Woo-hoo! You’ve implemented the interface required of a valid NSURLProtocol
instance. Checkout out the official documentation describing what methods an valid NSURLProtocol
subclass can implement, if you want to read more.
But your coding isn’t done yet! You still need to do the actual work of processing the request, which you do by handling the delegate callbacks from the NSURLConnection you created.
Open MyURLProtocol.m. Add the following methods:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.client URLProtocol:self didLoadData:data]; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self.client URLProtocolDidFinishLoading:self]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { [self.client URLProtocol:self didFailWithError:error]; } |
These are all NSURLConnection delegate methods. They are called when the NSURLConnection you’re using to load the data has a response, when it has data, when it finishes loading and when it fails. In each of these cases, you’re going to need to hand this information off to the client.
So to recap, your MyURLProtocol handler creates its own NSURLConnection and asks that connection to process the request. In the NSURLConnection delegate callbacks methods above, the protocol handler is relaying messages from the connection back to the URL Loading System. These messages talk about loading progress, completion, and errors.
Look and you’ll see the close family resemblance in message signatures for the NSURLConnectionDelegate and the NSURLProtocolClient
— they are both APIs for asynchronous data loading. Also notice how MyURLProtocol uses its client
property to send messages back to the URL Loading system.
Build and run. When the app opens, enter the same URL and hit Go.
Uh-oh! Your browser isn’t loading anything anymore! If you look at the Debug Navigator while it’s running, you’ll see memory usage is out of control. The console log should show a racing scroll of innumerable requests for the same URL. What could be wrong?
In the console you should see lines being logged forever and ever like this:
2014-01-19 07:15:59.321 NSURLProtocolExample[992:70b] Request #0: URL = http://www.raywenderlich.com/ 2014-01-19 07:15:59.322 NSURLProtocolExample[992:70b] Request #1: URL = http://www.raywenderlich.com/ 2014-01-19 07:15:59.329 NSURLProtocolExample[992:70b] Request #2: URL = http://www.raywenderlich.com/ 2014-01-19 07:15:59.329 NSURLProtocolExample[992:70b] Request #3: URL = http://www.raywenderlich.com/ 2014-01-19 07:15:59.330 NSURLProtocolExample[992:70b] Request #4: URL = http://www.raywenderlich.com/ 2014-01-19 07:15:59.333 NSURLProtocolExample[992:570b] Request #5: URL = http://www.raywenderlich.com/ 2014-01-19 07:15:59.333 NSURLProtocolExample[992:570b] Request #6: URL = http://www.raywenderlich.com/ 2014-01-19 07:15:59.333 NSURLProtocolExample[992:570b] Request #7: URL = http://www.raywenderlich.com/ 2014-01-19 07:15:59.333 NSURLProtocolExample[992:570b] Request #8: URL = http://www.raywenderlich.com/ 2014-01-19 07:15:59.334 NSURLProtocolExample[992:570b] Request #9: URL = http://www.raywenderlich.com/ 2014-01-19 07:15:59.334 NSURLProtocolExample[992:570b] Request #10: URL = http://www.raywenderlich.com/ ... 2014-01-19 07:15:60.678 NSURLProtocolExample[992:570b] Request #1000: URL = http://www.raywenderlich.com/ 2014-01-19 07:15:60.678 NSURLProtocolExample[992:570b] Request #1001: URL = http://www.raywenderlich.com/ |
Think again about the URL Loading System and protocol registration, and you might have a notion about why this is happening. When the UIWebView wants to load the URL, the URL Loading System asks MyURLProtocol if it can handle that specific request. Your class says YES
, it can handle it.
So the URL Loading System will create an instance of your protocol and call startLoading
. Your implementation then creates and fires its NSURLConnection. But this also calls the URL Loading System. Guess what? Since you’re always returning YES
in the +canInitWithRequest:
method, it creates another MyURLProtocol instance.
This new instance will lead to a creation of one more, and then one more and then an ifinite number of instances. That’s why you app doesn’t load anything! It just keeps allocating more memory, and shows only one URL in the console. The poor browser is stuck in an infinite loop! Your users could be frustrated to the point of inflicting damage on their devices.
Review what you’ve done and then move on to how you can fix it. Obviously you can’t just always return YES
in the +canInitWithRequest:
method. You need to have some sort of control to tell the URL Loading System to handle that request only once. The solution is in the NSURLProtocol
interface. Look for the class method called +setProperty:forKey:inRequest:
that allows you to add custom properties to a given URL request. This way, you can ‘tag’ it by attaching a property to it, and the browser will know if it’s already seen it before.
So here’s how you break the browser out of infinite instance insanity. Open MyURLProtocol.m. Then change the -startLoading
and the +canInitWithRequest:
methods as follows:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { static NSUInteger requestCount = 0; NSLog(@"Request #%u: URL = %@", requestCount++, request); if ([NSURLProtocol propertyForKey:@"MyURLProtocolHandledKey" inRequest:request]) { return NO; } return YES; } - (void)startLoading { NSMutableURLRequest *newRequest = [self.request mutableCopy]; [NSURLProtocol setProperty:@YES forKey:@"MyURLProtocolHandledKey" inRequest:newRequest]; self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self]; } |
Now the -startLoading
method sets a NSNumber instance () for a given key (
@"MyURLProtocolHandledKey"
) and for a given request. It means the next time it calls +canInitWithRequest:
for a given NSURLRequest instance, the protocol can ask if this same property is set.
If it is set, and it's set to YES, then it means that you don't need to handle that request anymore. The URL Loading System will load the data from the web. Since your MyURLProtocol instance is the delegate for that request, it will receive the callbacks from NSURLConnectionDelegate.
Build and run. When you try it now, the app will successfully display web pages in your web view. Sweet victory! You might be wondering why you did all of this just to get the app to behave just like it was when you started. Well, because you need to prepare for the fun part!
The console should now look something like this:
2014-01-19 07:22:42.260 NSURLProtocolExample[1019:70b] Request #0: URL = <NSMutableURLRequest: 0x9c17770> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.261 NSURLProtocolExample[1019:70b] Request #1: URL = <NSMutableURLRequest: 0x8b49000> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.270 NSURLProtocolExample[1019:70b] Request #2: URL = <NSURLRequest: 0xea1cd20> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.271 NSURLProtocolExample[1019:70b] Request #3: URL = <NSURLRequest: 0xea1c960> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.271 NSURLProtocolExample[1019:70b] Request #4: URL = <NSURLRequest: 0xea221c0> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.274 NSURLProtocolExample[1019:4113] Request #5: URL = <NSURLRequest: 0xea2c610> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.274 NSURLProtocolExample[1019:4113] Request #6: URL = <NSURLRequest: 0xea2c610> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.274 NSURLProtocolExample[1019:4113] Request #7: URL = <NSURLRequest: 0xea2c610> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.274 NSURLProtocolExample[1019:4113] Request #8: URL = <NSURLRequest: 0xea294c0> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.275 NSURLProtocolExample[1019:4113] Request #9: URL = <NSURLRequest: 0xea2c610> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.275 NSURLProtocolExample[1019:4113] Request #10: URL = <NSURLRequest: 0xea294c0> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.276 NSURLProtocolExample[1019:6507] Request #11: URL = <NSURLRequest: 0x8c46af0> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.276 NSURLProtocolExample[1019:1303] Request #12: URL = <NSURLRequest: 0x8a0b090> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.277 NSURLProtocolExample[1019:4113] Request #13: URL = <NSURLRequest: 0x8a0c4a0> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:42.277 NSURLProtocolExample[1019:4113] Request #14: URL = <NSURLRequest: 0x8a0c4a0> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:43.470 NSURLProtocolExample[1019:330b] Request #15: URL = <NSURLRequest: 0x8b4ea60> { URL: http://www.raywenderlich.com/ } 2014-01-19 07:22:43.471 NSURLProtocolExample[1019:330b] Request #16: URL = <NSURLRequest: 0x8d38320> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 } 2014-01-19 07:22:43.471 NSURLProtocolExample[1019:330b] Request #17: URL = <NSURLRequest: 0x8d386c0> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 } 2014-01-19 07:22:43.471 NSURLProtocolExample[1019:330b] Request #18: URL = <NSURLRequest: 0x8d38ad0> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 } 2014-01-19 07:22:43.471 NSURLProtocolExample[1019:4113] Request #19: URL = <NSURLRequest: 0x8b50250> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 } 2014-01-19 07:22:43.472 NSURLProtocolExample[1019:4113] Request #20: URL = <NSURLRequest: 0x8b50250> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 } 2014-01-19 07:22:43.472 NSURLProtocolExample[1019:330b] Request #21: URL = <NSURLRequest: 0xea9c420> { URL: http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.8 } 2014-01-19 07:22:43.472 NSURLProtocolExample[1019:4113] Request #22: URL = <NSURLRequest: 0x8b50250> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 } 2014-01-19 07:22:43.472 NSURLProtocolExample[1019:330b] Request #23: URL = <NSURLRequest: 0xea9c3f0> { URL: http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.8 } 2014-01-19 07:22:43.472 NSURLProtocolExample[1019:4113] Request #24: URL = <NSURLRequest: 0x8b4fcb0> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 } 2014-01-19 07:22:43.472 NSURLProtocolExample[1019:330b] Request #25: URL = <NSURLRequest: 0xea9c4d0> { URL: http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.8 } 2014-01-19 07:22:43.472 NSURLProtocolExample[1019:4113] Request #26: URL = <NSURLRequest: 0x8b50250> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 } 2014-01-19 07:22:43.472 NSURLProtocolExample[1019:4113] Request #27: URL = <NSURLRequest: 0x8b4fcb0> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 } ... |
Now you have all the control of the URL data of your app and you can do whatever you want with it. It's time to start caching your app's URL data.
Remember the basic requirement for this app: for a given request, it should load the data from the web once and cache it. If the same request is fired again in the future, the cached response will be provided to the app without reloading it from the web.
Now, you can take advantage of Core Data (already included in this app). Open NSURLProtocolExample.xcdatamodeld. Select the Event entity, then click on it again so that it lets you rename it. Call it CachedURLResponse.
Next, click on the + button under Attributes to add a new attribute and name it data with Type set to Binary Data. Do the same thing again to create the properties encoding (String), mimeType (String) and url(String). Rename timeStamp to timestamp. At the end, your entity should look like this:
Now you're going to create your NSManagedObject subclass for this entity. Select File\New\File…. On the left side of the dialog, select Core Data\NSManagedObject. Click on Next, leave the checkbox for NSURLProtocolExample selected and hit Next. In the following screen, select the checkbox next to CachedURLResponse and click Next. Finally, click Create.
Now you have a model to encapsulate your web data responses and their metadata!
It's time to save the responses your app receives from the web, and retrieve them whenever it has matching cached data. Open MyURLProtocol.h and add two properties like so:
@property (nonatomic, strong) NSMutableData *mutableData; @property (nonatomic, strong) NSURLResponse *response; |
The response
property will keep the reference to the metadata you'll need when saving the response from a server. The mutableData
property will be used to hold the data that the connection receives in the -connection:didReceiveData:
delegate method. Whenever the connection finishes, you can cache the response (data and metadata).
Let's add that now.
Open MyURLProtocol.m. Change the NSURLConnection delegate methods to the following implementations:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; self.response = response; self.mutableData = [[NSMutableData alloc] init]; } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.client URLProtocol:self didLoadData:data]; [self.mutableData appendData:data]; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self.client URLProtocolDidFinishLoading:self]; [self saveCachedResponse]; } |
Instead of directly handing off to the client, the response and data are stored by your custom protocol class now.
You'll notice a call to an unimplemented method, saveCachedResponse. Let's go ahead and implement that.
Still in MyURLProtocol.m, add imports for AppDelegate.h and CachedURLResponse.h. Then add the following method:
- (void)saveCachedResponse { NSLog(@"saving cached response"); // 1. AppDelegate *delegate = [[UIApplication sharedApplication] delegate]; NSManagedObjectContext *context = delegate.managedObjectContext; // 2. CachedURLResponse *cachedResponse = [NSEntityDescription insertNewObjectForEntityForName:@"CachedURLResponse" inManagedObjectContext:context]; cachedResponse.data = self.mutableData; cachedResponse.url = self.request.URL.absoluteString; cachedResponse.timestamp = [NSDate date]; cachedResponse.mimeType = self.response.MIMEType; cachedResponse.encoding = self.response.textEncodingName; // 3. NSError *error; BOOL const success = [context save:&error]; if (!success) { NSLog(@"Could not cache the response."); } } |
Here is what that does:
NSManagedObjectContext
from the AppDelegate
instance.CachedURLResponse
and set its properties based on the references to the NSURLResponse
and NSMutableData
that you kept.Build and run. Nothing changes in the app's behavior, but remember that now successfully retrieved responses from the web server save to your app's local database.
Finally, now it's time to retrieve cached responses and send them to the NSURLProtocol
's client. Open MyURLProtocol.m. Then add the following method:
- (CachedURLResponse *)cachedResponseForCurrentRequest { // 1. AppDelegate *delegate = [[UIApplication sharedApplication] delegate]; NSManagedObjectContext *context = delegate.managedObjectContext; // 2. NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"CachedURLResponse" inManagedObjectContext:context]; [fetchRequest setEntity:entity]; // 3. NSPredicate *predicate = [NSPredicate predicateWithFormat:@"url == %@", self.request.URL.absoluteString]; [fetchRequest setPredicate:predicate]; // 4. NSError *error; NSArray *result = [context executeFetchRequest:fetchRequest error:&error]; // 5. if (result && result.count > 0) { return result[0]; } return nil; } |
Here's what it does:
saveCachedResponse
.NSFetchRequest
saying that we want to find entities called CachedURLResponse. This is the entity in the managed object model that we want to retrieve.CachedURLRepsonse
object that relates to the URL that we're trying to load. This code sets that up.Now it's time to look back at the -startLoading
implementation. It needs to check for a cached response for the URL before actually loading it from the web. Find the current implementation and replace it withe the following:
- (void)startLoading { // 1. CachedURLResponse *cachedResponse = [self cachedResponseForCurrentRequest]; if (cachedResponse) { NSLog(@"serving response from cache"); // 2. NSData *data = cachedResponse.data; NSString *mimeType = cachedResponse.mimeType; NSString *encoding = cachedResponse.encoding; // 3. NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL MIMEType:mimeType expectedContentLength:data.length textEncodingName:encoding]; // 4. [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; [self.client URLProtocol:self didLoadData:data]; [self.client URLProtocolDidFinishLoading:self]; } else { // 5. NSLog(@"serving response from NSURLConnection"); NSMutableURLRequest *newRequest = [self.request mutableCopy]; [NSURLProtocol setProperty:@YES forKey:@"MyURLProtocolHandledKey" inRequest:newRequest]; self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self]; } } |
Here's what that does:
NSURLResponse
object is created with the data we have saved.Build and run your project again. Browse a couple of web sites and then stop using it (stop the project in Xcode). Now, retrieve cached results. Turn the device's Wi-Fi off (or, if using the iOS simulator, turn your computer's Wi-Fi off) and run it again. Try to load any website you just loaded. It should load the pages from the cached data. Woo hoo! Rejoice! You did it!!!
You should see lots of entries in the console that look like this:
2014-01-19 08:35:45.655 NSURLProtocolExample[1461:4013] Request #28: URL = <NSURLRequest: 0x99c33b0> { URL: http://www.raywenderlich.com/wp-content/plugins/wp-polls/polls-css.css?ver=2.63 } 2014-01-19 08:35:45.655 NSURLProtocolExample[1461:6507] serving response from cache |
That's the log saying that the response is coming from your cache!
And that's that. Now your app successfully caches retrieved data and metadata from web page requests. Your users will enjoy faster page loads and superior performance! :]
Here is where you can download the final code for this tutorial.
This example covered a simple usage of NSURLProtocol
, but don't mistake it as a complete solution for caching. There is a lot more to implementing a production-quality caching browser. In fact, the loading system has built-in caching configurations, which are worth getting to know. The goal of this tutorial is simply to show you the possibilities. Because NSURLProtocol
has access to the data going in and out of so many components, it's very powerful! There are almost no limits to what you can do implementing the -startLoading
method.
While IETF's RFC 3986 may modestly define URLs as a "...compact sequence of characters that identifies an abstract or physical resource..." the truth is that the URL is its own mini language. It's the domain-specific language (DSL) for naming and locating things. It's probably the most pervasive domain-specific language in the world, considering that URLs have crawled out of the screen and are now broadcast in radio and TV advertisements, printed in magazines and splashed on shop signs all over the world.
NSURLProtocol
is a language you can use in a myriad of ways. When Twitter wanted to implement the SPDY protocol on iOS, an optimized successor to HTTP 1.1, they did it with NSURLProtocol
. What you use it for, is up to you. NSURLProtocol
gives you power and flexibility at the same time requires a simple implementation to accomplish your goals.
Please, feel free to leave any questions or suggestions about this tutorial in our forum discussion. It's right below!
NSURLProtocol Tutorial is a post from: Ray Wenderlich
The post NSURLProtocol Tutorial appeared first on Ray Wenderlich.
Add the finishing touches to your Flappy Bird game in this final tutorial in the series!
Video Tutorial: How To Make a Game Like Flappy Bird Part 5: Finishing Touches is a post from: Ray Wenderlich
The post Video Tutorial: How To Make a Game Like Flappy Bird Part 5: Finishing Touches appeared first on Ray Wenderlich.
In these days of big data, data is stored in a multitude of formats, which poses a challenge to anyone trying to consolidate and make sense of it. If you’re lucky, the data will be in an organized, hierarchical format such as JSON, XML or CSV. If you’re not so lucky, the data is more freeform and unstructured and you may have to struggle with endless if/else cases or regular expressions.
You can also use automated parsers such as NSScanner to analyze string data in any form, from natural written languages to computer programming languages. In this NSScanner tutorial, you’ll learn about the parser included in Cocoa and how to use its powerful methods to extract information and manipulate strings in really neat ways. You’ll use what you learn to build an OS X application that works like Apple Mail’s interface, as shown below:
Although you’ll be building an OS X application in this tutorial, NSScanner is available on both OS X and iOS. By the end of this tutorial, you’ll be ready to parse text on either platform. Let’s get started!
Download the starter project, extract the contents of the ZIP file and open NSScannerTutorial.xcodeproj in Xcode.
You’ll find three folders named MasterViewController, Custom Cell and comp.sys.mac.hardware. In the View Controller folder you’ll find a simple xib
file with a TableView
on the left that contains a custom cell with a bunch of labels, and a TextView
on the right hand side.
MasterViewController.m contains a pre-made structure that sets up the delegate/data source for a Table View
. The Custom Cell folder contains PostCellView.h and PostCellView.m which form a subclass of NSTableCellView
. The cell has all the properties that you need to set each individual data item.
As for the data to parse: the comp.sys.mac.hardware folder contains 49 data files for you to parse with your app; take a minute and browse through the data to see how it’s structured.
UITableViews
in OS X are quite similar to those in iOS apps.Build and run the project to see it in action; you’ll see the following appear:
The basic framework is there: on the left hand side, the table view currently has placeholder labels with the prefix [Field]Value. These labels will be replaced with parsed data.
Before going straight into parsing, it’s important to understand what you’re trying to parse.
Below is a sample file of the 49 files you have to parse; you’ll be parsing the items outlined in red below:
The set of parsed items includes the From, Subject, Date, Organization, Lines, and Message fields. Out of the six fields, you’ll do something extra special with the “From” and “Message” fields, as follows:
“From” Field
For the “From” field, you’ll split the email and the name. This is trickier than it looks, as the name may come before the email, or vice versa. The “From” field might not even have a name or email, or it might have one but not the other.
“Message” segment
For the message segment, you’ll see if a message contains anything cost related. You’ll search the message for prices such as $1000
or $1.00
, as well as particular keywords in the message.
The keywords you’ll search for are: apple, macs, software, keyboard, printer, video, monitor, laser, scanner, disks, cost, price, floppy, card and phone.
Other Fields
For the other fields, you’ll simply separate the field from its value.
The values of the fields are delimited by colons. Also note that the data’s field text segment is separated from the message text segment by a new line.
First off, you’ll need two classes to parse and hold the data to be displayed.
Navigate to File\New\File… (or simply press Command+N). Select Mac OS > Cocoa and then Objective-C class and click Next. Set the class name to MacHardwarePost
and the subclass to NSObject. Click Next and then Create.
Open MacHardwarePost.h and add the following properties and method prototype between @interface
and @end
:
//The field’s values once extracted placed in the properties. @property (nonatomic, strong) NSString *fileName; @property (nonatomic, strong) NSString *fromPerson; @property (nonatomic, strong) NSString *email; @property (nonatomic, strong) NSString *subject; @property (nonatomic, strong) NSString *date; @property (nonatomic, strong) NSString *organization; @property (nonatomic, strong) NSString *message; @property int lines; //Does this post have any money related information? E.g. $25, $50, $2000 etc. @property (nonatomic, strong) NSString *costSearch; //Contains a set of distinct keywords. @property (nonatomic, strong) NSMutableSet *setOfKeywords; - (NSString *) printKeywords; |
printKeywords
returns an instance of NSString
that places all keywords in one single string separated by commas. Think of this like Java’s toString
method.
Open MacHardwarePost.m and add the following code between @implementation
and @end
:
- (id)init { if (self = [super init]) { _setOfKeywords = [[NSMutableSet alloc] init]; //1 } return self; } |
init
sets up NSMutableSet
and its various properties. In line 1 above, _setOfKeywords
, which is an instance of NSMutableSet
, tracks all keywords found. You’re using NSMutableSet
over NSMutableArray
because it’s not necessary to store duplicate keywords in this context.
Still working in the same file, add the following code segment right after init
:
- (NSString *) printKeywords { NSMutableString *result = [[NSMutableString alloc] init]; //1 NSUInteger i = 0; //2 NSUInteger numberOfKeywords = [self.setOfKeywords count]; //3 if (numberOfKeywords == 0) return @"No keywords found."; //4 for (NSString *keyword in self.setOfKeywords) //5 { //6 [result appendFormat:(i != numberOfKeywords - 1) ? @" %@," : @" %@", keyword]; i++; //7 } return result; } |
Here’s what’s going on in the code above:
NSMutableString
named result
and is used to append keywords together.self.setOfKeywords
.i
is equal to the last index in the list. If it is not, append a comma after the keyword; otherwise, don’t add a comma after the last word. You have finished implementing the MacHardwarePost
object which will store the data you extract from the files. Now, on to creating the parser!
Navigate to File\New\File… (or simply press Command+N). Select Mac OS > Cocoa and then Objective-C class and click Next. Set the class name to MacHardwareDataParser and the subclass to NSObject. Click Next and then Create.
Open MacHardwareDataParser.h and add the following imports before the @interface
tag:
#import "MacHardwarePost.h" |
Next, add the following method prototype between @interface
and @end
:
- (void)constructSelectorDictionary; - (MacHardwarePost *)parseRawDataWithData:(NSData *)rawData; - (id)initWithKeywords:(NSArray *)listOfKeywords fileName:(NSString *)fileName; |
Now open MacHardwareDataParser.m and add the following code just before @implementation
:
@interface MacHardwareDataParser () //Object that contain the fully extracted information. @property (nonatomic, strong) MacHardwarePost *macHardwarePost; //1 //Stores selector methods that may be called by the parser. @property (nonatomic, strong) NSDictionary *selectorDict; //2 //Contains the list of keywords to search @property (nonatomic, strong) NSArray *listOfKeywords; //3 //Keeps track of the current file we are extracting information from @property (nonatomic, strong) NSString *fileName; //4 @end |
The properties between @interface
and @end
aren’t exposed to the caller of this class; they’re meant for private and/or internal methods and properties for the use of MacHardwareDataParser
alone.
macHardwarePost
is where all the extracted field’s information will be stored. This property will be returned to the client using our parser once the parsing is complete.selectorDict
is an NSDictionary with its key the field you’re parsing and its value a selector method. It’s really important to have different functions for different tasks and not do everything in one method. Each selector method will be explained later on; check out this StackOverflow post for more information on selectors.listOfKeywords
stores the list of keywords you will use to search the message portion for matching keywords.fileName
stores the data file you are currently parsing. It’s generally a good idea to store the file name mainly for debugging purposes. If there is some error with the data you have just parsed, you can easily pinpoint and examine the file to see what the issue is.Open MacHardwareDataParser.m add the following code between
@implementation
and @end
:
#pragma mark - Initialization Phase - (id)initWithKeywords:(NSArray *)listOfKeywords fileName:(NSString *)fileName { if ( self = [super init] ) { [self constructSelectorDictionary]; self.listOfKeywords = listOfKeywords; self.fileName = fileName; } return self; } // build scanner selectors - (void)constructSelectorDictionary { self.selectorDict = @{ @"From" : @"extractFromWithString:", @"Subject" : @"extractSubjectWithString:", @"Date" : @"extractDateWithString:", @"Organization" : @"extractOrganizationWithString:", @"Lines" : @"extractNumberOfLinesWithString:", @"Message" : @"extractMessageWithString:" }; } |
initWithKeywords:fileName:
is an object initializer; when you create a MacHardwareDataParser
object, you will pass in a listOfKeywords
to be searched when parsing the message. You also need to pass in the filename that you are extracting data from to keep track of what you are parsing.
Invoking constructSelectorDictionary
creates an instance of NSDictionary
initialized with six key/value pair items. Whenever you see any one of these keys while parsing, selector
will automatically call the corresponding method. For example, if you find the field “Subject”, the corresponding method extractSubjectWithString:
will be called to extract the “Subject” field’s information.
Still working in the same file, add the following code after constructSelectorDictionary
and before @end
:
#pragma mark - Build Object Phase // construct MacHardwarePost, and return object. - (MacHardwarePost *)parseRawDataWithData:(NSData *)rawData // 1 { if (rawData == nil) return nil; // 2 //Extracted information from raw data placed in MacHardwarePost fields. self.macHardwarePost = [[MacHardwarePost alloc] init]; // 3 //Set the fileName within a MacHardwarePost object //to keep track of which file we extracted information from. self.macHardwarePost.fileName = self.fileName; // 4 //Contains every field and message NSString *rawString = [[NSString alloc] initWithData:rawData encoding:NSUTF8StringEncoding]; // 5 //Split Sections, so we deal with only fields, and then messages NSArray *stringSections = [rawString componentsSeparatedByString:@"\n\n"]; // 6 if (stringSections == nil) // 7 { return nil; } //Don't consider data that doesn't have a message. So stringSection must be > 1 if ([stringSections count] >= 1) // 8 { //Only need to extract the fields. (Located in the 0 index) NSString *rawFieldString = stringSections[0]; // 9 //place extracted fields into macHardwarePost properties. [self extractFieldsWithString:rawFieldString]; // 10 //Place contiguous message blocks back together in one string. NSString *message = [self combineContiguousMessagesWithArray:stringSections withRange:NSMakeRange(1, [stringSections count])]; // 11 //Set macHardwarePost message field. [self extractMessageWithString:message]; // 12 //Analyze the message for $money money, every amount searched we will record all the amounts // concatenate a string of $ e.g. $25, $60, $1250 in one whole string // Empty string if no amount of money was talked about. [self extractCostRelatedInformationWithMessage: message]; // 13 //We are going to loop through the message string and look for the "keywords". [self extractKeywordsWithMessage: message]; // 14 } return self.macHardwarePost; // 15 } |
Taking each numbered comment in turn, you’ll find the following:
parseRawDataWithData
takes an instance of NSData
as a parameter that contains your data. Once it has parsed all the fields and the message body, the method returns a MacHardwarePost
object in line 15.nil
before you begin parsing.MacHardwarePost
object and initialize it as empty. You’ll set all the properties’ values once you start extracting information.NSData
object into a raw string format. fields
text segment from the message's
text segment. The array could have a size larger than 2 since messages may also have newline
breaks. componentsSeparatedByString
will split the messages into segments if they’re separated by a newline
— check the example given below for an example of this.rawFieldString
into extractFieldsWithString
to extract all the relevant fields and set properties appropriately in the MacHardwarePost
object.extractMessageWithString:
to be set in the MacHardwarePost
object.extractCostRelatedInformationWithMessage
extracts and finds cost-related information.extractKeywordsWithMessage
finds the keywords in the message. Below is an example of how componentsSeparatedByString
splits up the text segments:
parseRawDataWithData
is the first line of attack, to break up the incoming data into manageable chunks. This gives a clear outline of how the data is structured, and how it can be parsed step by step.
Next you'll see how the individual fields and messages are parsed — this is where the fun begins! :]
Consider, if you will, the following sample field text segment:
Here is where NSScanner comes in. You know that each field and its value is separated by the delimiter :
. The image below gives a visual representation of how each section is split up:
An NSScanner object interprets and converts the characters of an NSString
object into number and string values. You assign the scanner’s string on creating it, and the scanner progresses through the characters of that string from beginning to end as you request items.
Open MacHardwareDataParser.m and add the following code just after parseRawDataWithData
and before @end
:
/* * extractFieldsWithString, extracts the necessary fields for a data set, * and places them in the mac hardware post object. */ - (void) extractFieldsWithString: (NSString *)rawString { NSScanner *scanner = [NSScanner scannerWithString:rawString]; // 1 //Delimiters NSCharacterSet *newLine = [NSCharacterSet newlineCharacterSet]; // 2 NSString *currentLine = nil; // 3 NSString *field = nil; // 4 NSString *fieldInformation = nil; // 5 [scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@":"]]; // 6 while (![scanner isAtEnd]) // 7 { //Obtain the field if([scanner scanUpToString:@":" intoString:¤tLine]) { // 8 //for some reason \n is always at the front. Probably because we setCharacterToBeSkipped to ":" field = [currentLine stringByTrimmingCharactersInSet: newLine]; // 9 } //Obtain the value. if([scanner scanUpToCharactersFromSet:newLine intoString:¤tLine]) // 10 { fieldInformation = currentLine; // 11 } BOOL containsField = (self.selectorDict[field] != nil) ? YES : NO; // 12 //Only parse the fields that are defined in the selectorDict. if (containsField) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self performSelector:NSSelectorFromString(self.selectorDict[field]) withObject:fieldInformation]; // 13 #pragma clang diagnostic pop } } } |
Here is a comment-by-comment tour of the above code:
scannerWithString
initializes the scanner with a given string and returns an NSScanner
object."\n"
NSCharacterSet object. This is used when you read each field/value pair one at a time.currentLine
stores the current field/value pair string.field
to be used to retrieve selector methods from selectorDict
.fieldInformation
to be used to obtain the field's information which will be passed into the selector's parameters to be analyzed and extracted. setCharactersTobeSkipped:
provided by NSScanner
defines the set of characters to be ignored when scanning for a value representation. Recall that a field and its value are separated by a colon ":"
; the colon is ignored when extracting the value. The returned string will not include the colon.stringByTrimmingCharactersInSet
to remove the newline at the end of the string. Later on you'll need to retrieve the selector using the field as a key to the dictionary selectorDict
fieldInformation.
selectorDict
.
selectorDict
, execute the method by invoking performSelector
. This line is inside pragma tags simply to avoid warnings since the selectors
are unknown at run-time.
Recall that your selector dictionary is constructed as follows:
@"From" : @"extractFromWithString:", @"Subject" : @"extractSubjectWithString:", @"Date" : @"extractDateWithString:", @"Organization" : @"extractOrganizationWithString:", @"Lines" : @"extractNumberOfLinesWithString:", @"Message" : @"extractMessageWithString:" |
Now that you have the field and the field’s information, you also have the corresponding method executing automatically to perform the data extraction. You now need to implement the six methods that will be called to extract each field’s value.
Open MacHardwareDataParser.m and add the following code after extractFieldsWithString
and before @end
:
//Extracts the subject field's value, and update post object. - (void)extractSubjectWithString: (NSString *)rawString { self.macHardwarePost.subject = rawString; } //Place date string into date property. - (void)extractDateWithString: (NSString *)rawString { self.macHardwarePost.date = rawString; } //Place the organization field value into organization property. - (void)extractOrganizationWithString: (NSString *)rawString { self.macHardwarePost.organization = rawString; } //Teaches you how to extract an entire message. - (void)extractMessageWithString: (NSString *)rawString { self.macHardwarePost.message = rawString; } |
The methods above simply place the field information you extracted into the MacHardwarePost
object.
Still working in the same file, add the following code immediately after extractMessageWithString:
:
//Teaches you how to extract a number. - (void)extractNumberOfLinesWithString:(NSString *)rawString { int numberOfLines; NSScanner *scanner = [[NSScanner alloc] initWithString:rawString]; //scans the string for an int value. [scanner scanInt:&numberOfLines]; self.macHardwarePost.lines = numberOfLines; } |
For extractNumberOfLinesWithString
, NSScanner
initializes the string that contains the number of lines. It then invokes scanInt:
which scans for an int value from a decimal representation and returns the value found by reference.
NSScanner
has various other methods you can explore at your leisure:
Okay folks, brace yourselves: you're getting deep into the guts of NSScanner and regular expressions. The first bit to parse is the "From" field.
Here you can combine your regular expression skills from the NSRegularExpression Tutorial on this site with your mad NSScanner
skills. Regular expressions are a great way to establish string-splitting patterns.
Still working in the same file, add the following code after extractNumberOfLinesWithString:
and before @end
:
- (void)extractFromWithString: (NSString *)rawString { //An advantage of regular expressions could be used here. //http://www.raywenderlich.com/30288/ //Based on the cases stated, we need to establish some form of pattern in order to split the strings up. NSString *someRegexp = @".*[\\s]*\\({1}(.*)"; //1 // ROGOSCHP@MAX.CC.Uregina.CA (Are we having Fun yet ???) // oelt0002@student.tc.umn.edu (Bret Oeltjen) // (iisi owner) // mbuntan@staff.tc.umn.edu () // barry.davis@hal9k.ann-arbor.mi.us (Barry Davis) NSString *someRegexp2 = @".*[\\s]*<{1}(.*)"; //2 // "Jonathan L. Hutchison" <jh6r+@andrew.cmu.edu> // <BR4416A@auvm.american.edu> // Thomas Kephart <kephart@snowhite.eeap.cwru.edu> // Alexander Samuel McDiarmid <am2o+@andrew.cmu.edu> // Special case: // Mel_Shear@maccomw.uucp // vng@iscs.nus.sg NSPredicate *fromPatternTest1 = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", someRegexp]; //3 NSPredicate *fromPatternTest2 = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", someRegexp2]; //Run through the patterns //Format: Email (Name) if ([fromPatternTest1 evaluateWithObject: rawString]) //4 { [self extractFromParenthesesWithString: rawString]; } //Format: Name <Email> || <Email> else if ([fromPatternTest2 evaluateWithObject: rawString]) //5 { [self extractFromArrowWithString: rawString]; } //Format: Email else { [self extractFromEmailWithString: rawString]; //6 } } |
After examining the 49 data sets, you end up with three cases to consider:
email ( name )
name < email >
Email
with no Name.Here's a step-by-step explanation of the above code:
"("
and finally zero or more occurrences of a string."<"
and finally zero or more occurrences of any character. NSPredicate
object that defines logical conditions used to constrain a search. The MATCHES
operator uses the regular expression package. You can read more about NSPredicate
in the official Apple documentation.
extractFromParenthesesWithString
which extracts the Email and the Name. extractFromArrowWithString
which extracts the Email and/or Name. extractFromEmailWithString
.Still working in the same file, add the following code after extractFromWithString
and before @end
:
#pragma mark - extractFromWithString helper methods //Extract the email, when the pattern is Format: email (No name specified) - (void)extractFromEmailWithString:(NSString *)rawString { self.macHardwarePost.email = rawString; self.macHardwarePost.fromPerson = @"unknown"; } |
extractFromEmailWithString
handles the special case where you don't match on pattern 1 or pattern 2; this is the case that only has the email but no name. In this case you just set MacHardwarePost
object's email and ser the name of the person to “unknown”.
Add the following code after extractFromEmailWithString
and before @end
:
//Extract the name of the person and email, when the pattern is Format: Name <Email> - (void)extractFromArrowWithString:(NSString *)rawString { NSScanner *scanner = [NSScanner scannerWithString:rawString]; //1 NSString *resultString = nil; //2 [scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@"<>"]]; //3 while (![scanner isAtEnd]) //4 { [scanner scanUpToString:@"<" intoString:&resultString]; //5 self.macHardwarePost.fromPerson = resultString; //6 [scanner scanUpToString:@">" intoString:&resultString]; //7 self.macHardwarePost.email = resultString; //8 } } |
Here is a step-by-step explanation of the code above:
NSScanner
that scans the given string with the pattern Name
resultString
; the extracted name and email will be placed in this string."<"
and ">"
to be ignored when scanning for a value representation."<"
. This cuts off everything following, leaving only the Name, since you ignored "<"
and “>”
in line 3. The diagram below illustrates this in detail:fromPerson
field in MacHardwarePost
.">"
which will give you the email. This cuts out everything before "<"
and after ">"
, like so:MacHardwarePost
You're not done yet! Add the following code after extractFromArrowWithString:
and before @end
:
//Extract the name of the person and email, when the pattern is Format: Email (Name) - (void)extractFromParenthesesWithString:(NSString *)rawString { NSScanner *scanner = [NSScanner scannerWithString:rawString]; NSString *resultString = nil; [scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@"()"]]; while (![scanner isAtEnd]) { [scanner scanUpToString:@"(" intoString:&resultString]; self.macHardwarePost.email = resultString; [scanner scanUpToString:@")" intoString:&resultString]; self.macHardwarePost.fromPerson = resultString; } } |
This is essentially the same as extractFromArrowWithString
, except this method deals with parentheses.
Add the following code after extractFromParenthesesWithString
and before @end
:
#pragma mark- Utilities - (NSString *)combineContiguousMessagesWithArray:(NSArray *)array withRange:(NSRange)range { NSMutableString *resultString = [[NSMutableString alloc] init]; //1 for(int i = (int)range.location; i < range.length; i++) //2 { [resultString appendString: array[i] ]; //3 } return [NSString stringWithString:resultString]; //4 } |
Think back to the diagram showing how to split the text:
You had to split the text segment with field-related information from the message segment with portions of the messages — now you need to recombine the message portion into one instead of multiple segments.
Here is a step-by-step explanation of the above code:
NSMutableString
so you can edit the string whenever you try to combine a portion of text. message portion
, start from index 1 (index 0 is the field portion
) and loop toarray length - 1
. You'll loop through each index containing the message portion.Now that you have the message portion in one place, you can start parsing the message for some useful information.
Your keyword search strategy is to look at every word and check your keyword’s dictionary to see if it matches. If so, add it to MacHardwarePost
keywords array that stores all keywords found relating to this message.
Add the following code to the end of MacHardwarePost.m, just before @end
:
//Extract keywords from the message. - (void)extractKeywordsWithMessage:(NSString *)rawString { NSScanner *scanner = [NSScanner scannerWithString:rawString]; //1 NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet]; //2 NSString *keyword = @""; //3 while (![scanner isAtEnd]) //4 { [scanner scanUpToCharactersFromSet:whitespace intoString:&keyword]; //5 NSString *lowercaseKeyword = [keyword lowercaseString]; //6 if([self.listOfKeywords containsObject: lowercaseKeyword]) //7 { [self.macHardwarePost.setOfKeywords addObject:lowercaseKeyword]; //8 } } } |
The above code is fairly straightforward:
NSScanner
.NSCharacterSet
for whitespace; this let you scan up to the next set of characters separated by a whitespace, as shown below:keyword
string. setOfKeywords
. MacHardwarePost
's keywords array.To search for cost related information, use NSScanner
to search each word separated by a whitespace. This is similar to the keywords strategy, but instead you're now searching for an occurrence of a dollarCharacter
($).
Add the following method code to the end of MacHardwarePost.m, just before @end
:
// Extract amount of cost if the message contains "$" symbol. - (void)extractCostRelatedInformationWithMessage:(NSString *)rawString { NSScanner *scanner = [NSScanner scannerWithString:rawString]; //1 NSMutableString *costResultString = [[NSMutableString alloc] init]; //2 NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet]; //3 NSCharacterSet *dollarCharacter = [NSCharacterSet characterSetWithCharactersInString:@"$"]; //4 NSString *dollarFound; float dollarCost; while (![scanner isAtEnd]) //5 { //Have the scanner find the first $ token if (![scanner scanUpToCharactersFromSet:dollarCharacter intoString:nil]) //6 { [scanner scanUpToCharactersFromSet:whitespace intoString:&dollarFound]; //7 NSScanner *integerScanner = [NSScanner scannerWithString:dollarFound]; //8 [integerScanner scanString: @"$" intoString:nil]; //9 [integerScanner scanFloat: &dollarCost]; //10 if (!(int)dollarCost == 0) //11 { [costResultString appendFormat:@"$%.2f ", dollarCost]; } } } self.macHardwarePost.costSearch = costResultString; //12 } |
Here's what's going on in the code above:
scannerWithString:
initializes the scanner with a given string — in this case your message — and returns an instance of NSScanner
.NSMutableString
so you can append all the cost related information into a single string.NSCharacterSet
so you can jump to the next word after analyzing the previous one.dollarCharacter
NSCharacterSet
so you can scan up to a string that starts with a $ symbol.scanUpToCharactersFromSet
scans the string until it finds a $
symbol.$
symbol, scan up to the next whitespace to give you the cost related portion.NSScanner
to scan the cost-related string.NSScanner
scans past the $
symbol, leaving you with only the amount.NSScanner
scans the cost-related string for a float value; if you find one, store it in dollarCost
.scanFloat:
fails it returns zero, so check dollarCost
to see if you actually found a valid amount. If so, append it to the costResultString
.MacHardwarePost
costSearch field to the cost-related information extracted from the message. There you have it — your parser is finally complete. Time to put your parser to good use and start extracting information from the 49 data files.
The last things to do are run all 49 files through your parser to create the MacHardwarePost
objects, pass these objects into your masterViewController
and set up your delegate and data source for the tableview to display the results.
Open AppDelegate.h and replace the code between @interface
and @end
with the following code:
@property (assign) IBOutlet NSWindow *window; //Stores a reference to the data set's file path. E.g. /Users/userName/Documents/comp.sys.mac.hardware @property (nonatomic, strong) NSString *dataSetFilePath; //Stores an array of all data file names. E.g. 50419, 50420, 50421, ... @property (nonatomic, strong) NSArray *listOfDataFileNames; @property (nonatomic, strong) NSMutableArray *listOfPost; @property (nonatomic, strong) IBOutlet MasterViewController *masterViewController; |
Here's an explanation of what these properties will be used for:
dataSetFilePath
stores the path to the 49 data files so you can easily obtain each individual file to be parsed.listOfDataFileNames
stores all 49 data file names in an array; each file name will be appended to dataSetFilePath
to get an individual file.listOfPost
stores all MacHardwarePost
objects once you're done parsing.masterViewController
contains the TableView
and TextView
for your app.Open AppDelegate.m and add the following code just before @implementation
:
#import "MacHardwareDataParser.h" #import "MacHardwarePost.h" |
You'll need to include these imports to reference those classes in the next bit.
Still in the same file, replace applicationDidFinishLaunching
with the following code:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { NSError *error = nil; //Obtain the file path to the resource folder. NSString *folderPath = [[NSBundle mainBundle] resourcePath]; //1 //Get all the fileNames from the resource folder. self.listOfDataFileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:folderPath error:&error]; //2 //Keywords we are passing into the scanner to check if a message contains one or more of these words. NSArray *keywords = @[ @"apple", @"macs", @"software", @"keyboard", @"printers", @"printer", @"video", @"monitor", @"laser", @"scanner", @"disks", @"cost", @"price", @"floppy", @"card", @"phone" ]; //3 self.listOfPost = [[NSMutableArray alloc] init]; //4 //Loops through the list of data files, and starts scanning and parsing them and converts them //to MacHardwarePost objects. for (NSString *fileName in self.listOfDataFileNames) //5 { //ignore system files, fileName we are interested in are numbers. if ([fileName intValue] == 0) continue; //6 NSString *path = [folderPath stringByAppendingString: [NSString stringWithFormat:@"/%@", fileName]]; //7 NSData *data = [NSData dataWithContentsOfFile: path]; //8 MacHardwareDataParser *parser = [[MacHardwareDataParser alloc] initWithKeywords:keywords fileName:fileName]; //9 MacHardwarePost *post = [parser parseRawDataWithData:data];//10 if (post != nil) { [self.listOfPost addObject:post];//11 } } //Create the masterViewController self.masterViewController = [[MasterViewController alloc] initWithNibName:@"MasterViewController" bundle:nil]; //12 self.masterViewController.listOfMacHardwarePost = self.listOfPost;//13 //Add the view controller to the Window's content view [self.window.contentView addSubview:self.masterViewController.view]; self.masterViewController.view.frame = ((NSView*)self.window.contentView).bounds; } |
Taking each numbered comment in turn, you'll find the following:
contentsOfDirectoryAtPath:
which returns an array of file names that could be either files or directory names.NSArray
named keywords
and set it up with all the keywords to search in our message.NSMutableArray
called listOfPost
to store all the MacHardwarePost
objects.fileName
to see if it’s an integer as all 49 of the file names are integers. If it isn’t, check the next file to see if it’s a data file to parse.NSData
using the data file path.MacHardwareDataParser
and pass in the keywords
to search for and the fileName
of the data file to parse.parserRawDataWithData
which extracts all the important data. Once complete, the method returns a MacHardwarePost
object ready to use.MacHardwarePost
objects if the parsing was successful.masterViewController
.MacHardwarePost
objects into your masterViewController
which will be used later to set your data source.At this point, you've parsed all 49 data files and passed the MacHardwarePost
objects to your masterViewController
— it’s finally time to display the results of all your hard effort! :]
Open up MasterViewController.m and add the following imports before @implementation
:
#import "MacHardwarePost.h" |
Find numberOfRowsInTableView:
and replace the implementation with the following:
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { return [self.listOfMacHardwarePost count]; } |
numberOfRowsInTableView
is part of the table view’s data source protocol; it sets the number of rows in a section of the table view. In this case you only have one section, with the number of rows being the number of data files you parsed.
Next, find tableView:viewForTableColumn:row:
. Replace the comment that says //TODO: Set up cell view
with the code below:
PostCellView *cellView = [tableView makeViewWithIdentifier:tableColumn.identifier owner:self]; //1 if ( [tableColumn.identifier isEqualToString:@"PostColumn"] ) //2 { MacHardwarePost *post = [self.listOfMacHardwarePost objectAtIndex:row]; //3 NSString *unknown = @"Unknown"; //4 NSString *costRelated = @"NO"; //5 cellView.from.stringValue = (post.fromPerson == nil) ? unknown : post.fromPerson; //6 cellView.subject.stringValue = (post.subject == nil) ? unknown : post.subject; cellView.email.stringValue = (post.email == nil) ? unknown : post.email; cellView.costRelated.stringValue = (post.costSearch.length == 0) ? costRelated : post.costSearch; cellView.organization.stringValue = (post.organization == nil) ? unknown : post.organization; cellView.date.stringValue = (post.date == nil) ? unknown : post.date; cellView.lines.stringValue = [NSString stringWithFormat:@"%d", post.lines]; cellView.keywords.stringValue = [post printKeywords]; } |
NSTableViewDelegate
has a method tableView:viewForTableColumn:row:
which is a part of the table view’s delegate protocol; this is where you set up every individual cell. There's a custom cell named PostCellView
for your use which contains labels such as from, subject, email, costRelated, organization, date, lines, and keywords for you to set.
Here's a detailed look at the code above:
PostCellView
.tableColumn
is indeed PostColumn
.MacHardwarePost
object that you parsed based on the current row.NSString
variable unknown
in case the property within MacHardwarePost
turns out to be nil.costRelated
and initialize to “NO”.MacHardwarePost
field is nil. If so, set the label to "unknown"
; otherwise set the label to what you received from MacHardwarePost
.In MasterViewController.m, replace the starter implementation of tableViewSelectionDidChange:
with the following:
- (void)tableViewSelectionDidChange:(NSNotification *)aNotification { NSInteger selectedRow = [self.tableView selectedRow]; //1 if( selectedRow >= 0 && [self.listOfMacHardwarePost count] > selectedRow ) //2 { MacHardwarePost *post = [self.listOfMacHardwarePost objectAtIndex:selectedRow]; //3 self.messageTextView.string = post.message; //4 } } |
tableViewSelectionDidChange
instructs the delegate
that the table view’s selection has changed. This method executes whenever the user selects a different cell.
Here's the details of the above code:
selectedRow
is in-bounds.MacHardwarePost
corresponding to the selected row.Build and run your project; you'll see all the parsed fields in the table view. Select a cell on the left and you'll see the corresponding message on the right.
These data files grow up so fast! :] They were just raw data when you found them, and after you groomed them a little with your parser, they look all grown up now. Aww.
Here’s the source code for the finished project: NSScannerTutorialFinal.zip
There is so much more you can do with the data you have parsed. You could write a formatter that converts all MacHardwarePost
into JSON, XML, CSV or any other formats you can think of! With your new-found flexibility to represent data in different forms, you can share your data across different platforms.
Using NSScanner is a great way to quickly manipulate and search for different strings. I hope this new skill gives you the power to parse all that meaningful data in your own apps!
If you're really interested in the study of computer languages and how they are implemented, take a class in comparative languages. Your course will likely cover formal languages and BNF grammars - all important concepts in the design and implementation of parsers.
For more information on NSScanner and other parsing theory, check out the following resources:
If you have any questions or comments, please join the discussion below and share them!
NSScanner Tutorial: Parsing Data in Mac OS X is a post from: Ray Wenderlich
The post NSScanner Tutorial: Parsing Data in Mac OS X appeared first on Ray Wenderlich.