Adding calendar

We are now familiar with all main iOS features, and we only need to finish superapp to complete this Rubymotion tutorial.

Right now superapp looks pretty weird, and can only add new Mood objects, and show Mood history in the table. I'm afraid nobody would use this app.

To make it more useful, let's show a simple calendar in a HomeScreen. It will show selected month, and each day cell will represent user's mood with a color: awesome day would be displayed with green color, a good day with blue, average with yellow, and bad with red. Something like this:

Still does not look good. Unfortunately, it is not a design tutorial so that we will continue with this.

It would take a lot of time to make this calendar by ourselves. Let's go to Cocoapods and look for some calendar charts. After some seconds of searching, I've found TEAChart. It looks just like we want it too.

Note: Unfortunately TEAChart does not work well with AutoLayout, so I had to add some changes to it. In this tutorial, we will use this fork.

Let's think how HomeScreen will look like. I see it as a screen with Navigation Bar, Calendar, and some labels that show statistics for a selected month, for example, a number of good and bad days. Navigation bar would have current date as a Title, and buttons to navigate between months.

Let's start with the hardest part - integrating TEAChart into the project. To do it, we need to add this pod into Rakefile. Since we are going to use customized version, we need to tell cocoapods where to look for a repository:

        app.pods do
          pod 'TEAChart', git: 'https://github.com/savytskyi/TEAChart'
        end
      
And now we just need to install it with rake pod:install.

Now we are going to change HomeScreenLayout. We will add a calendar to it, small visual line separator (just for a better UI), and labels that will show a number of bad and good days. For an easier Layout customization, these labels will be inside a plain UIView. Here is a mockup:

Blue line shows borders of a container view which will have two labels inside. The line on the top is a separator.

To add the calendar, we need to read some TEAChart Docs. There we can find out that Calendar Graph's view name is a TEAContributionGraph. So to add it to the layout, we would need to write something like add TEAContributionGraph, :calendar. Let's start!
        class HomeScreenLayout < MotionKit::Layout
          def layout
            # adding calendar:
            add TEAContributionGraph, :chart

            # separator:
            add UIView, :line

            # bad days labels container:
            add UIView, :bad do
              add UILabel, :bad_number
              add UILabel, :bad_label
            end

            # good days labels container:
            add UIView, :good do
              add UILabel, :good_number
              add UILabel, :good_label
            end
          end

          # we want to use same padding values everywhere:
          def padding
            15
          end

          def chart_style
            weekday_label_format "EEEEEE"

            # from TEAChart docs: use showDayNumbers to display day number
            # in each day cell:
            show_day_numbers true

            constraints do
              top.equals(:superview, :top).plus(80)
              right.equals(:superview, :right).minus(padding)
              left.equals(:superview, :left).plus(padding)
              height('330')
            end
          end

          def line_style
            background_color 0xc4c4c4.uicolor
            constraints do
              top.equals(:chart, :bottom).plus(30)
              left.equals(:superview, :left).plus(padding * 2)
              right.equals(:superview, :right).minus(padding * 2)
              height(1)
            end
          end

          ###
          ### reusable style - bad and good day number labels look almost the same
          ###

          def mood_number
            text "10"
            font "Helvetica-Light".uifont(30)
            text_alignment NSTextAlignmentCenter
            constraints do
              top.equals(:superview, :top).plus(5)
              center_x.equals(:superview, :center_x)
            end
          end

          # reusable style - bad and good day labels look almost the same
          def mood_label
            text_alignment NSTextAlignmentCenter
            constraints do
              bottom.equals(:superview, :bottom).minus(5)
              center_x.equals(:superview, :center_x)
            end
          end

          ###
          ### labels container views
          ###
          def good_style
            constraints do
              top.equals(:bad, :top)
              left.equals(:superview, :center_x).plus(padding)
              right.equals(:superview, :right).minus(padding)
              height(65)
            end
          end

          def bad_style
            constraints do
              top.equals(:line, :bottom).plus(10)
              left.equals(:superview, :left).plus(padding)
              right.equals(:superview, :center_x).minus(padding)
              height(65)
            end
          end

          ###
          ### Bad and Good day statistics labels:
          ###

          # number of days:
          def bad_number_style
            # using reusable style added earlier:
            mood_number
          end

          # number of days:
          def good_number_style
            # using reusable style added earlier:
            mood_number
          end

          # static label:
          def bad_label_style
            text "Bad days"

            # using reusable style added earlier:
            mood_label
          end

          # static label:
          def good_label_style
            text "Good days"

            # using reusable style added earlier:
            mood_label
          end
        end
      
Here you can find two new things: reusable styles and calendar properties. Reusable styles are super easy: if you have multiple elements that have same styles (constraints, fonts, texts, colors), you can create one method that describes all typical styles, and then it can be called in all methods that have same visual attributes. In this example, bad days and good days labels are absolutely the same, and the only thing that changes from label to label is its text. Therefore, we have created mood_label and mood_number methods that define all common attributes for days labels.

Calendar properties can be found in TEAChart Docs. If you are an experienced developer, you can check header files as well. In docs, you can find that show_day_numbers will add day number to each day cell, and more.

Okay, we have the layout ready. Now we need to set the screen. We have many elements here, so let's deal with calendar first. As you can see in the docs, its main delegate methods are:

But first we need to get a calendar chart from the layout and set its delegate. In will_appear, we would need to get Mood data for the chart as well. And only after we will be able to show something in the graph:
        def on_load
          self.view.backgroundColor = :white.uicolor
          check_local_notifications

          @chart = @layout.get(:chart)
          @chart.delegate = self

          @bad_days = @layout.get(:bad_number)
          @good_days = @layout.get(:good_number)
        end

        def will_appear
          set_current_date
        end
      
As you can see, here we got :chart, :bad_number and good_number elements from the layout, and saved them for future use. In will_appear, we added calls to new methods that aren't written yet - set_current_date that will set up screen for a given date. Let's implement this method:
        def set_current_date(date=nil)
          date = NSDate.now if date.nil?
          @date = date

          # make string look like "July '15":
          string = date.string_with_format("MMMM") + " '" +
            date.string_with_format("YY")
          self.title = string

          set_data
          @chart.reloadData
        end
      
Let's talk about the code above. In the method definition, you can see date=nil, which means that method can be called without any arguments, and the app won't crash with a usual wrong number of arguments (1 for 0) error - they will be set to a predefined value, which is in this example is nil. Then if the date is nil, we will set it to a current date. It is important to save the date for a future use, so we create an instance variable @date that can be accessed outside of the current method. Then we want to set current date as a screen's title, which is shown in a navigation bar. We want the title to look like "June '15". Unfortunately, we can't make with one call of a string_with_format because of the ' symbol so that we will concatenate three strings: string with a month name ( MMMM), string with a ' symbol, and string with a short year ( YY). Now we can change current screen's title to a new string.

But this is not the end of the method. Since date has been modified, we need to get Mood objects that were created for a given month. We are going to do it in a new method called set_data:

        def get_mood_objects(date)
          min_date = @date.start_of_month
          max_date = min_date.end_of_month

          predicate = NSPredicate.predicateWithFormat "created_at < %@ AND created_at => %@", max_date, min_date
          moods = Mood.with_predicate(predicate).to_a

          # group moods by day
          moods.group_by do |mood|
            mood.created_at.string_with_format("dd")
          end
        end

        def set_data
          # reset all values:
          @days = {}
          bad_days = 0
          good_days = 0

          # get mood objects for a current date:
          moods = get_mood_objects(@date)

          # fill @days with new values for a given date:
          moods.each do |day, day_moods|

            # calculate average mood level for each day:
            total = 0
            day_moods.each { |mood| total += mood.level }
            avg = total.to_f / day_moods.count.to_f

            @days[day] = avg

            # update number of good and bad days:
            if avg < 2
              bad_days += 1
            elsif avg >= 3
              good_days += 1
            end
          end

          # show number of a good and bad days:
          @bad_days.text = bad_days.to_s
          @good_days.text = good_days.to_s

          @chart.reloadData
        end
      
Since calendar shows info for one month only, we need to get Mood objects for this particular month. To do it, we will need to get those object from the DB that have the created_at value between the beginning and the end of a current month. Since a user can create multiple Mood objects per one day, it would be a good idea to use average mood value for the day. For example, the user has been sad in the morning (mood.level == 1). In the afternoon he felt better (mood.level == 3), and in the evening after some exciting news he started to feel great (mood.level == 4), it would not be right to use first available value for a day, and average value would show more accurate data. In this case, the average would be equal 2.66, which is higher than "average" level.

We don't want to pollute set_date method, so we are going to retrieve Mood objects and group them by a date in a separate get_mood_objects(date) method. With sugarcube's start_of_month and end_of_month we get date ranges, and then with a predicate created_at < max_date AND created_at => min_date we retrieve Mood objects created during the current month.

To get average values for each day, we would need to group results first. In Ruby, we can do it with a group_by operator. It iterates over all elements and groups them by a value returned in a block. In our case we group them by a day number string - string_with_format("dd") converts the date to a string that represents day number. Result would look like this:

        @grouped
        => {
          "1" => [MoodObject1, MoodObject2],
          "2" => [MoodObject3],
          ...
          "31" => [MoodObjectXX]
        }
      

When we have all objects grouped by day, we can finally get average values for each day and calculate a number of good and bad days as well. Inside of a set_data method we create @days hash, that will store mood values for each date. To calculate average mood values and the number of good and bad days we iterate over just retrieved moods hash. When iterating over a hash, Ruby will pass a key and its value into the block. In our case key is a day number string, and value is an array of mood objects for this day.

        moods.each do |day, day_moods|
          ...
        end
      

Calculating average value for each day should be pretty clear: we create a total variable set to 0. Then when iterating over all mood objects for a current day, we add mood's level to a total variable. Then we divide it by a number of mood objects. And now we can recognize whether the day is good or bad - if the average is less than 2, the day was, unfortunately, bad. If the average is 3 or higher - the day was good. Mood levels from 2 to 3 are average so that we won't count them.

        total = 0
        day_moods.each { |mood| total += mood.level }
        avg = total.to_f / day_moods.count.to_f

        @days[day] = avg

        if avg < 2
          bad_days += 1
        elsif avg >= 3
          good_days += 1
        end
      

When we have all values calculated, we just need to set them and reload a calendar.

        @bad_days.text = bad_days.to_s
        @good_days.text = good_days.to_s

        @chart.reloadData
      

Now we need to set TEAChart delegate methods to make the calendar work. There are 4 of them:

Should not be that difficult:
        def monthForGraph
          @date
        end

        def valueForDay(day)
          @days[day.to_s].to_f
        end

        def numberOfGrades
          5
        end

        def colorForGrade(grade)
          if grade == 0
            0xB3B3B3.uicolor
          elsif grade == 1
            0xBC5C59.uicolor
          elsif grade == 2
            0xBCAF36.uicolor
          elsif grade == 3
            0x58B9BC.uicolor
          else
            0x48BC63.uicolor
          end
        end

        def minimumValueForGrade(grade)
          grade
        end
      
The easiest method is monthForGraph - here we just return our current date, because we want the calendar to render itself for a current month.

valueForDay(day) is not that difficult as well. We just need to access @days hash and retrieve a value for a given day. Here we need to remember that day variable passed to the method is a number, but Hash keys are strings. If we don't convert the number to a string, we will not be able to get the value, so we have to convert it first. Another tricky thing here is that some days may not have values, and nil will be returned. Calendar expects its delegate to return a number, so we would need to figure something out. We could add if/else condition, but in this case, it is easier to convert everything to Float. If the value exists, it will be float anyway, since it is an average value. If the value is nil, it will be converted to float as zero.

numberOfGrades is also easy - it just returns a plain number. We have four mood levels, and one other grade for empty days.

colorForGrade(grade) is where we can customize calendar's palette. You can try to use your own colors:

And last delegate method - minimumValueForGrade(grade). Here we can specify minimum values for each grade. In our case minimum value for each grade is a grade itself:

Now we can try to launch the app:

Date Navigation

Now we need to add a button to the navigation bar that will change current date. I an going to use UIStepper - simple control with a + and - buttons. Each time user taps on one of them, we will get its new value, and process it. Let's add a method that generates it to will_appear:

        def will_appear
          set_current_date
          set_bar_button
        end

        def set_bar_button
          stepper = UIStepper.new
          stepper.stepValue = 1
          stepper.maximumValue = 0
          stepper.minimumValue = -36
          stepper.on(:touch) do |event|
            value = stepper.value
            new_date = NSDate.now + value.month
            set_current_date new_date
          end

          bar_btn = UIBarButtonItem.alloc.initWithCustomView(stepper)
          self.navigationItem.setRightBarButtonItems [ bar_btn ]
        end
      
This small part of the code will help users to navigate between months. First we create a stepper object. Stepper's value will be used to navigate between months: Because future months don't have any Mood objects, we will set maximum stepper value to 0 - it will restrict users from browsing future months. Its step is set to 1 because calendar can show only one month, and we can't go back to half month back. The minimum value is -36 that allows a user to browse 36 previous months or 3 years.

Now we need to process stepper's taps. One tap on stepper equals one month:

Each time user taps on a stepper, we get current date and stepper's current value. Then we convert stepper's value into number of months with value.month, and calculating new date:
        value = stepper.value
        new_date = NSDate.now + value.month
      
Now when we have the date that user want to display, we can pass it to a previously written set_current_date method, which will update screen's title to a name of a new month, will retrieve new Mood objects for a given date and will render them into a calendar:
        value = stepper.value
        new_date = NSDate.now + value.month
        set_current_date new_date
      

Don't forget to add stepper into the navigation bar. To do it we need to create UIBarButtonItem with a custom view, where the custom view is the stepper.

It is time to open the app. User should now be able to change the date:

Now when we have a calendar working, let's commit all changes with git:

        git add .
        git commit -m "calendar added"
      

Summary

In this chapter we have successfully installed a third-party library with Cocoapods, learned how to reuse layout code, worked with a database, and more. At this point, we have a working application that allows a user to track his mood and shows him statistics for each month.

Book Index | Next