File System: read/write

In iOS each app lives inside of its sandbox - it means that developer does not have access to any other files and folders outside of the application.

But it is not a problem! It is fine to live inside of this sandbox - you can create all types of files and directories you want. However, you need to know where exactly it can be created.

There are lots of places (directories) where a developer can access file system (inside of the sandbox, of course). For now, we will talk only about most important ones:

It is important to remember the difference between all these directories and use a correct one for saving your data. For example, you don't want to save some important documents that user wanted to save into /tmp directory, because it is periodically cleaned by iOS, and all data inside it can be lost.

To save or read a file, a developer first needs to get a path to a correct directory where this file will be (or already) stored. As usual, you can do it in a plain Objective-C way, or you can use some gems like sugarcube:

        #
        # Objective-C way:
        #

        # path to a file in a Documents directory:
        directories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)
        directory = directories[0]
        file = directory.stringByAppendingPathComponent("some_file.pdf")

        # path to a file in a Caches directory:
        directories = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, true)
        directory = directories[0]
        file = directory.stringByAppendingPathComponent("some_file.pdf")

        # path to a file in a tmp directory:
        directory = NSTemporaryDirectory()
        file = directory.stringByAppendingPathComponent("some_file.pdf")

        #
        # sugarcube way:
        #

        # path to a file in a Documents directory:
        "some_file.pdf".document_path

        # path to a file in a Caches directory:
        "some_file.pdf".cache_path

        # path to a file in a tmp directory:
        "some_file.pdf".temporary_path
      

Let's use tutorial_app to learn how one can work with files in iOS. We will add a new screen with a text field where a user can paste a URL. It will also have a button that will start downloading an image from a given URL and then save it into documents folder with a file name image.png. To display an image screen will have an imageView. If the app already has some files downloaded, it will load it by default. Otherwise, it will be empty, and will wait for an image to be downloaded.

First we need to add afmotion gem to the Gemfile, and then run bundle install to install a new gem, and rake pod:install after, to install AFNetworking cocoapod, which is a afmotion's dependency.

As usual, we need to add a new cell to the HomeScreen and add an action to it. Then we will create a layout for a new screen, and the screen itself. Let's start with HomeScreen:

        class HomeScreen < PM::Screen
          title "Home"

          # HomeScreen code ...

          def set_table_data
            # set_table_data code ...
              {
                title: "Files and Images",
              }
            # rest of set_table_data code ...
          end

          # HomeScreen code ...
          def tableView(table, didSelectRowAtIndexPath: index)
            # ...
            elsif index.row == 5
              open FilesScreen.new
            end
          end
        end
      
Now we are going to create FilesScreenLayout in /app/layouts/screens/files_screen_layout.rb:
        class FilesScreenLayout < MotionKit::Layout
          def layout
            add UITextField, :url
            add UIButton, :get
            add UIImageView, :image
          end

          def url_style
            text "https://www.google.com/logos/doodles/2015/ukraine-independence-day-2015-5171096236064768-hp2x.jpg"
            border_style UITextBorderStyleRoundedRect
            constraints do
              center_x.equals(:superview, :center_x)
              top.equals(:superview, :top).plus(90)
              left.equals(:superview, :left).plus(10)
              right.equals(:superview, :right).minus(10)
            end
          end

          def get_style
            title "Get!"
            title_color :blue.uicolor
            constraints do
              center_x.equals(:superview, :center_x)
              top.equals(:url, :bottom).plus(20)
            end
          end

          def image_style
            content_mode UIViewContentModeScaleAspectFit
            constraints do
              center_x.equals(:superview, :center_x)
              top.equals(:get, :bottom).plus(20)
              width(300)
              height(300)
            end
          end
        end
      
Layout code should be easy to understand - just three elements, constraints, nothing new.
        class FilesScreen < PM::Screen
          title "Files and Images"

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

          def on_load
            self.view.backgroundColor = 0xffffff.uicolor
            @layout.get(:get).on_tap do
              download_image_from @layout.get(:url).text.to_s
            end

            @image_view = @layout.get(:image)
            @get = @layout.get(:get)
            load_image
          end

          def image_file_name
            "image.png"
          end

          def load_image
            data = NSData.read_from(image_file_name.document_path)

            if data.is_a?(NSData)
              @image_view.image = data.uiimage
            end
          end

          def save_image(image)
            image.nsdata.write_to(image_file_name.document_path)
          end

          def download_image_from(url)
            return if url.empty?

            mp url
            @get.setTitle "Wait for it...", forState: UIControlStateNormal

            AFMotion::Image.get(url) do |result|
              @get.setTitle "Get", forState: UIControlStateNormal
              mp result
              if result.success? && result.object.is_a?(UIImage)
                @image_view.image = result.object
                save_image(result.object)
              else
                UIAlertController.alert self, "Error",
                  message: "Failed to download image",
                  buttons: ["OK"]
              end
            end
          end
        end
      
Let's check what's going on on the screen. load_view method is simple as usual - we just render our layout. Then in on_load, we save main elements into instance variables to access them later. We also set an action to a Get button - each time it tapped, we will call a download_image_from method, and will pass it text from a :url text field. At the end of on_load, you can find load_image method. It checks whether the user already has an image saved as a file and if true image gets rendered in an image view. First it tried to read a file from a given path with NSData.read_from(image_file_name.document_path). If it contains some data, it translates it into an image with data.uiimage code, and show it in the image view.

In iOS, all images are stored in files as NSData, which is a binary data. Before saving an image we need to translate it into NSData first, and then we can save it to the file system. Same story with reading - when we read a file, we receive data first. To show it somewhere developer will need to convert it into UIImage.

save_image method should be very easy to understand. We pass a UIImage object (which is an image), translating it into data with image.nsdata, and then writing the result to a given file.

download_image_from may look complicated, but since we already learned networking, you should understand what is going on there. In the very beginning, we check passed url. We continue only if it is not empty. To show user that we are in process of downloading we should show some loading indicator, but for now, we just change the title of a button to Wait for it.... When afmotion receives a response, we change button's title back and checking what we have received. If everything is fine, and result.object is an image, we show it to the user and save it with a save_image method. Otherwise, we display a simple alert that tells the user that some error occurred.

Everything seems to work well, and it is a time for a traditional git commit!

        git add .
        git commit -m "added files screen"
      

Summary

Usually writing or reading a file in iOS requires three simple steps:

Book Index | Next