Introduction

Recipository is an application used to store your own recipes. Most recipe apps out there are built as inspiration applications with pre-existing recipes. If they do give you the ability to create recipes, it typically shares it with others. Recipository wants to be a simple recipe repository for your family recipes that are yours, and no-one else’s. It has only one function, and therefore will be easier to use and have a cleaner and easier to learn user interface.

The idea came from specific request from family and friends, that wanted a way to digitize their current recipes; current applications did not seem to satisfy their needs. In the last few years they have been digitizing their life for a modern lifestyle and moved movies, books, music, documents and moved it to the cloud. Now it is the recipes turn.

To meet the expectations, the application should have a website counterpart, as the general consensus is a computer is easier to work with when creating or editing content, while the iOS client primarily is the consumption devise; in the kitchen or for inspiration in the living room, or anywhere really. Printing functionality and email functionality was requested as features, as was offline functionality and general usability.

The aim with this project is to have a functional alpha version of the application ready at the end of this project for further development after course completion. Some features will be missing, but will be added later. The focus will be on iOS related functionality like Core Data, Processing JSON, Communicating with the server, and make a functional user interface with a high usability.

The following sections will describe the process in which the application was designed, evaluated and built.


Methods and Materials

As tools to design this application the following applications will be used:

  • Xcode – For the development and interactive/high fidelity prototypes.
  • Asset Catalog Generator – For generating the required sizes for icons in the application.
  • Pixelmator – For generating the graphics, visual representation and brand.
  • OmniGraffle – For creating diagrams.
  • Visual Studio – For creating the Web API, and the website for content creation and editing.
  • Windows Server 2012 R2 and MSSQL Server 2012 – For hosting the Web API.
  • Pen and paper for notes and low fidelity prototypes.

The following parts in methods will be numbered for easier reference in results. The steps will take place in accordance with the numbering.

1. Conceptual design and Requirements

The conceptual design will be created based on interviews with one of the end users requesting the application. This will include a discussion about what features are needed, what are the goals, and what environment should it work in. This also includes looking into similarities between the application we are building, and current applications on the market. For requirements elicitation of nonfunctional requirements i use the model described in Bass, Clements and Kazman (2013).

2. Data Modeling

Data Modeling is done to better understand what information needs to be contained in the application as this is going to be a data centric application. OmniGraffle is used to create a database diagram, to visualize the model at hand. A UML notation is used. Methods used in this section can be found in Larman (2005).

3. Visual Identity and Brand

The visual identity and branding will be developed using Pixelmator. Simple color theory was used, and common aesthetic sense. The theory involved in graphical design is out of the scope of this assignment, but the visuals themselves are not as the part of the user experience. Most color theory used can found in Williams (2014).

4. Low Fidelity Prototyping and Evaluation

From the conceptual design, requirements and data model, paper prototypes are created. It is important these prototypes are not too detailed. The ambiguity is used to encourage conversation that would otherwise not have been possible if the interface was too refined. A too refined interface confines many users to the current set frames, and could discourage feedback for possibly better solutions. As Buxton (2007, p. 115) writes:

For sure, designers want to get something concrete out of their sketches. As Gedenryd asks:

Why would sketches be created from the very beginning of design, when just about everything is likely to change before you arrive at the final product? Hence, their purpose must be to serve the inquiry that creates them. And since they are created so early on, they have to serve this purpose right from the start. (Gedenryd 1998; p. 151)

The essence of this quote and that of urber’s is that one can get more out of a sketch than was put into making it because of its ambiguity. The fact that the sketch is, well, sketchy—that is, leaves a lot out, or leaves a lot to the imagination—is fundamental to the process. My take on this is:

If you want to get the most out of a sketch, you need to leave big enough holes.

Ambiguity creates the holes. It is what enables a sketch to be interpreted in different ways, even by the person who created it. To answer Gedenryd’s question, one of the key purposes of sketching in the ideation phase of design is to provide a catalyst to stimulate new and different interpretations. Hence, sketching is fundamental to the cognitive process of design, and it is manifest through a kind of conversation between the designer(s) and their sketches.

These prototypes are tested with two of the end users, plus an outsider that never does not know the application to get better feedback. The prototypes will also be presented in the iOS Course for varied feedback together with the conceptual design. Prototyping and sketching methodology used can be found in Buxton (2007), including definitions on prototype types. Evaluation methods used can be read about in detail in Krug (2009).

5. High Fidelity / Interactive Prototype and Evaluation

This prototype is interactive, and build with Xcode. The prototype will have navigational functionality only, and include placeholder information for the interface. Evaluation will work in the same way as with the low fidelity prototype, but with extra emphasis on the think aloud test.

6. Implementation

The implementation includes several subsections:

  • Server: Server implementation in Asp.Net MVC with Web API’s and a web interface for editing data. This will only be shortly described as the server development is not a primary goal with this course.
  • Network Communication: Client side network communication is going to be implemented using the Alamofire Framework (Alamofire, 2015).
  • Parsing: Parsing of JSON data will be implemented using the SwiftyJSON Framework (SwiftyJSON, 2015). Some parts of this source code does not support Swift 2 at the time of writing, and will need to be rewritten.
  • Core Data: As the application needs offline functionality Core Data will be used as a local storage solutions for our database model. Care will be taken to use Core Data’s built in relations to create a true copy of the online data.
  • ScrollViews: These are hard to use in iOS with AutoLayout and needs special attention, as auto calculating the height of the contentView needs specific criteria fulfilled. Therefore a section is devoted specifically for this purpose. UIScrollViewDelegate is used to implement this functionality.
  • Print: As users requested printing functionality, AirPrint will be implemented. For this the functionality UIPrintInteractionController is needed.
  • Emailing: MFMailComposeViewControllerDelegate
  • Image Handling: To implement the ability to pick images from the gallery or to snap new ones from the camera  UIImagePickerControllerDelegate will be implemented.

7. Final Evaluation

This will be done by looking back at what requirements was not fulfilled, and by an evaluation by presentation in the iOS Course, plus an evaluation of the alpha client with the end users, that was also used to generate the requirements. This will generate a current state of the product for use in the future when the application will need to be fully developed. First a think aloud test will be conducted, followed by a presentation on the current features. This evaluation section includes a video demo of the application running on iPad and iPhone. This video demo is the same that was presented in the iOS course.


Results

1. Conceptual design and Requirements

The interview with the user (one of the application requesters) was interesting. She is a young woman in her late twenties. She described her problem with the current recipe applications on the market. Most applications she could find was either hard to use, or was overly bloated with “inspiration” content. She felt the applications were too complicated to use because of this. Also she disliked the sometimes forced sharing of own recipes with third parties or users. She described her and her friends preferred usage pattern of a simple to use application with their own recipes in the center of the application, and not an afterthought. The application should be secure and guarantee safety from data loss as it would replace all of their current paper versions.

Some of the applications on the market she had tested was Nigerian Food, Epicurious and many more. The two applications mentioned are interesting examples. Let’s look at the Nigerian Food example first:

nigerianfood1 nigerianfood2

 

The two above examples use outdated interface elements, and improvised interface elements. At the same time, it is only used for inspiration, and not for storage of own applications. She described this as a common problem, as it was hard to find applications only targeted towards recipe storage, and especially with a web interface for recipe management.

epicurious1 epicurious2

Epicurious was a better alternative. Again the application was designed for inspiration, but did have a custom recipe component. The component was hidden, and mostly seemed like an afterthought, then a main feature. The design is a lot better and more modern. Epicurious is a great application, but too bloated for the interviewee.

At this point the functional requirements came into play. To describe the functional and nonfunctional requirements a simplified version of the method described in “Software Architecture in Practice” by Bass, Clements and Kazman, (2013). Especially the tactics to solve the nonfunctional requirements that can be found on the back cover of the book. These requirements were derived from the interview:

Functional Requirements

  • Users, so having a login system.
  • Associate users with social networks for easy login
  • Search for recipes or ingredients used in recipes
  • CRUD Categories (Create Read Update Delete)
  • CRUD Recipes
  • Rate recipes by difficulty and how much the user likes it
  • Share via Email
  • Print Recipe
  • Share within the App with another person using the App
  • Use pictures from phone / taken with the camera
  • Mobile implementation
  • Web Implementation
  • Work offline
  • Constant backup in the cloud, and access no matter the location
  • Attach Gallery to recipe

Accepted constraints

Items out of scope will be pulled into the application if time permits. Items permitted not being implemented in the alpha version are as follows.

  • Nonfunctional login system due to complexity and time constraints
  • Missing Gallery
  • Lacking design and implementation of the web interface as it is out of the scope of this exercise.
  • Search Functionality not implemented
  • Print, Email and App share functionality not implemented
  • Ratings not implemented

Nonfunctional Requirements

  • Be able to restore data within 2 hours.
  • Auto detect downtime within 30 min.
  • Recover from downtime within 4 hours.
  • Visual response within 1 second of pressing a feature.
  • Easy to implement new property on main data elements within 1 day including
  • Be able to easily change the data source with only a few days of implementation.

Nonfunctional Requirements Solutions

As solutions to the nonfunctional requirements above, the following solutions were selected by using the tactics from Bass, Clements and Kazman, (2013, Back Cover)

  • Availability:
    • Ping / Notifications of downtime, to be able to respond to downtime if the server is defective
    • Disk Redundancy to decrease risk of defective disks reducing
  • Security:
    • Daily Backups, separate for Server, Database, and the Website.
    • Backups are also backed up externally with web access for quick restore.
    • Server placed in a hosting center with limited access and redundant internet connections. This makes sure the chance for downtime is limited due to human error.
  • Modifiability:
    • Reduce Coupling / Increase Cohesion: Use interfaces, and divide responsibility into separate classes. For example use a DataManager for handling the data layer, and an ImageHandler to handle image manipulations. This also covers responsibilities between server and client. This would also enable changing the data layer with short notice, as long as the model is similar.
  • Performance:
    • Reduce Overhead / Maintain multiple copies of data: Cache files/information in local database to keep interface fast, and not require the server to respond for most actions.

2. Data Modeling

The data modeling was straightforward based on the details from the conversation. The login system would properly be implemented using Entity Framework, and would require a User, UserClaim, UserRole and UserLogin table. The rest of the tables would be Categories, Recipes, and Ingredients. The diagram below show the cardinalities between the tables, using UML. The model is simplified compared to the complete version with all data found in Larman (2005). As the model is extremely straightforward, and only includes 3 content tables, it will not be explained further.

Database Diagram

3. Visual Identity and Brand

For the naming of the application, a simple but catchy name that described the application functionality was the intention. The name Recipository was chosen as an indirect metaphor. The name is a simple conjunction of the two words Recipe and Repository. To highlight this the “ci” letters should be highlighted to underline the intent behind the name further, and at the same time create a play and contrast in the logo. As the logo is clean text, it is also easy to reproduce in SVG format, and large banners later without requiring an experienced graphic designer.

The slogan became “Safe recipe storage and sharing”, to underline the feelings the user described in the interview earlier. This caters to the fears and emotions she had, and shows off the core values of the application.

To create a visual identity of the application and to have a brand, colors needs to be selected. To get a fitting color scheme a split complementary color selection (Williams, 2014, p. 96) was used as the base, and then skewed the darker colors in favor of a gradient background and higher contrast.

The result from from the name, slogan and color choices came together in the visual identity below. 

FullLogoAndSlogan

For the application icon space needed to be saved and the name was split in two. The slogan was removed.

Icon

To make it easier to create the icon and assets for the application the Asset Catalog Generator was used. An example of all the files created automatically for different devices can be seen below. The application comes highly recommended and will save the developer quite some time.

Screenshot 2015-11-29 00.52.40Screenshot 2015-11-29 00.53.37

4. Low Fidelity Prototyping and Evaluation

Picture1Picture2Picture3Picture4

The entry page (the first sketch) should just contain the logo, and the different login options, or an option to create a new user. It was discussed if only using Facebook would simplify the process. No decision was made at this point though, especially as the login implementation would not be implemented in this version if the application. When logged in (the second sketch) the user should be presented with a search field, paired with a button to bring the user to the categories selection. The category selection (third sketch), shows how the list overview of both categories and recipes would look. A category press would lead to another table view, and a press on a recipe would bring the user to the recipe details page (fourth sketch).

The evaluation with the users key followed guidelines from the earlier mentioned books (Buxton, 2007; Krug, 2009). During the evaluation with the users, the second sketch was deemed unnecessary, as search functionality could be integrated directly into the application via a pull down function that would bring the search field down. The button alone would not make any sense also. In the table overview concern was brought forth regarding the small picture besides the cells in sketch 3. They were concerned the pictures would be too small to make sense putting there. As a result pictures would not be implemented in the table view cells. Otherwise the sketches were received well with the user testing.

During the class evaluation, ideas were brought up regarding adding an integrated social network, just for the friends that knew each other. This could be in the form of a chat room, or a wall where members could share their recipes and talk about food ideas. The idea was written down as a “nice to have” feature.

In both groups the name, slogan and visual identity was well received. The general feedback on the logo was in the lines of “Ahh i see what you did there! Clever!”. This was regarded as a clear success.

5. High Fidelity / Interactive Prototype and Evaluation

Unfortunately, no pictures or video was saved from the high fidelity prototype. As a result, only the storyboard can be shown to illustrate the flow.

Screenshot 2015-11-28 21.36.04

For the interactive prototype a split view was chosen as it was no longer needed to have a welcome screen. This also provides a more nuanced functionality on the iPad in the form of a slide in menu, or a side menu if the iPad is in landscape mode. To the right of the split view controller, two navigation controllers are found. These are to provide automatic Back buttons to the previous view. The views with the orange headers were not implemented at this time. All elements in the interface used standard buttons, table views and text/picture elements. This was to facilitate faster learning as the application generally works as the rest of the iOS ecosystem. The user knows what a back icon means, and the plus sign.

The feedback on this model was positive, and enthusiastic, but mostly referred back to currently missing functionality. This again shows why a low fidelity prototype is needed when discussing ideas to allow for further interface innovations. No feedback worth noting was added, other than the user’s ease of navigation through the system.

6. Implementation

This section is mostly about the technical implementation of the core functionality of the system, and includes some architecture, and much code. Most of the code base is derived from lessons from Percival, (2015), that teaches an excellent online course. The link can be found in the references. The course is mostly for beginners but shows Xcode functionality in a rather easy way. For information not found in the online course, Smyth (2015) was mostly used, but in cases of high complexity or information out of the scope of event his book, Nahavandipoor (2015) provided excellent guidance. Any content not found in these sources and used in the code below is referenced in their respective sections.

A note on software architecture. The Implementation uses a standard three-layer architecture. Part of it is enforced with the MVC framework, but an extra layer is enforced to handle the data layer called DataManager. DataManager has several other functions as  static classes below handling requests for Categories, Recipes and Ingredients. This is standard architecture, and no further space will be used to explain this.

Architecture diagram

 

General relevant links for the implementation chapter are as follows:

6.1 Server

As the server itself is outside of the scope of this course this section will be brief. The server side is implemented in Asp.Net MVC, using Web API 2 for REST communication. The REST interface supports both XML and JSON based on the requests header. If the “content-type” is “application/json” it returns json, if it is set to “application/xml” the objects are retuned as XML.  The login system is implemented using Entity Framework for controlling the database, with the Identity Framework on top. This allows for integrated two factor authentication, hashing of passwords, integration with third parties like Google authentication and Facebook authentication.

The Web API 2 interface exposes a REST interface. For the documentation on Recipository’s Web API take a look here:  http://www.recipository.dk/Help. The idea of the REST interface is for the server to communicate the data back and forth with the iOS application in JSON format. A short explanation of this relationship can be seen in the illustration below.

json-rest3

Illustration from Hammad (2015)

 

6.2 Network Communication

To facilitate faster implementation of the network communication protocol, and as all the primitives (GET, PUT, POST, DELETE) were needed Alamofire (2015) was a sure bet to save time. Alamofire allows the user to make a request with the following code.

Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
         .responseJSON { response in
             print(response.request)  // original URL request
             print(response.response) // URL response
             print(response.data)     // server data
             print(response.result)   // result of response serialization

             if let JSON = response.result.value {
                 print("JSON: \(JSON)")
             }
         }

As can be seen in the first parameter, the primitives can be set directly here. The 3rd. parameter allows for a dictionary element to be passed with the request. All the data goes into this parameter.

For larger uploads another function was needed to track uploaded content progress. Alamofire also has built in functionality for this.

Alamofire.upload(.POST, "https://httpbin.org/post", file: fileURL)
         .progress { bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in
             print(totalBytesWritten)

             // This closure is NOT called on the main queue for performance
             // reasons. To update your ui, dispatch to the main queue.
             dispatch_async(dispatch_get_main_queue()) {
                 print("Total bytes written on main queue: \(totalBytesWritten)")
             }
         }
         .responseJSON { response in
             debugPrint(response)
         }

The function above is ideal for uploading the pictures we need. Both examples are taken directly from Alamofire (2015).

As an example of both functions used several times, directly from the application, the code below is provided. The example below is from the addRecipe function. It shows how a picture is uploaded first, the returned data is parsed and reused for the next request.

class func addRecipe (categoryId: Int, name: String, picture: UIImage, diff: Int, hearts: Int, ingredients:[Dictionary<String,String>],instructions:String, completionHandler: () -> Void) {
            
            let imageJsonObject: Dictionary = [
                "ImagePost" : "Please Return Image Data",
            ]
            
            let imageData = UIImagePNGRepresentation(picture)
            
            // CREATE AND SEND REQUEST ----------
            
            let imageUrlRequest = AlamofireExtentions.urlRequestWithComponents("http://recipository.dk/imageupload/PostImage", parameters: imageJsonObject, imageData: imageData!)
            
            Alamofire.upload(imageUrlRequest.0, data: imageUrlRequest.1)
                .progress { bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in
                    print(totalBytesWritten)
                    dispatch_async(dispatch_get_main_queue()) {
                        print("Total bytes written on main queue: \(totalBytesWritten) Expected: \(totalBytesExpectedToWrite)")
                    }
                }
                .responseJSON { responseFromImagePost in
                    if let imageDataValidated = responseFromImagePost.result.value {
                        
                        let imageData = JSON(imageDataValidated)
                        
                        //now we got the image links - time to build the recipe creation request
                        
                        let jsonObject: Dictionary = [
                            "CategoryId" : "\(categoryId)",
                            "Name" : "\(name)",
                            "Difficulty" : "\(diff)",
                            "Hearts" : "\(hearts)",
                            "Directions" : "\(instructions)",
                            "PictureBase64" : "\(imageData["Base64"].string!)",
                            "PicturePath" : "\(imageData["Path"].string!)",
                        ]
                        
                        Alamofire.request(.POST, "http://recipository.dk/api/recipes", parameters: jsonObject)
                            .responseJSON { response in
                                
                                if let data = response.result.value {
                                    let jsonResponse = JSON(data)
                                    
                                    print("The New Recipe Added And Got Recipe Id: " + String(jsonResponse["Id"].int))
                                    
                                    //print(response.request!.description)  // original URL request
                                    //print(response.response) // URL response
                                    //print(JSON(response.data!))     // server data
                                    //print(response.result)   // result of response serialization
                                    
                                    let recipeid = jsonResponse["Id"].int
                                    
                                    if let rid = recipeid {
                                        for i in ingredients {
                                            
                                            let ingredientJsonObject: Dictionary = [
                                                "RecipeId" : "\(rid)",
                                                "Amount" : "\(i[Ingredients.CoreDataKeys.Amount]!)",
                                                "Ingredient" : "\(i[Ingredients.CoreDataKeys.Ingredient]!)",
                                            ]
                                            
                                            Alamofire.request(.POST, "http://recipository.dk/api/ingredients/", parameters: ingredientJsonObject)
                                                .responseJSON { response in
                                                    
                                                    if let data = response.result.value {
                                                        let ingredientJsonObjectResponse = JSON(data)
                                                        
                                                        print("The New Ingredient Added And Got Ingredient Id: " + String(ingredientJsonObjectResponse["Id"].int))                                    }
                                            }
                                        }
                                    } else {
                                        print("Failed to save recipe")
                                    }
                                    
                                    //Den her linje er nu nødvendig når man opdaterer UI fra bagrunden.
                                    dispatch_async(dispatch_get_main_queue(), {
                                        DataManager.updateCoreDataFromServer(completionHandler)
                                    })
                                    
                                }
                        }
                    } else {
                        print("Image Too Large for the Server API - JSON Malformed")
                    }
                    
            }
        }

 

6.3 Parsing

As seen in the example above, SwiftyJSON, (2015) is used to parse the JSON response from the server. Unfortunately SwiftyJSON does not support SWIFT 2, and much of the framework needed to be rewritten for this project. SwiftyJSON makes fetching data and casting them to the correct type trivial compared to the built in method in Swift. The two most important elements are shown below. First the data is parsed using the JSON() function. The resulting data is then defined as an array by using the .array identifier.

let categories = JSON(data: json).array

The next layer can now be accessed without the JSON() function as it has already been converted. In this case the type is defined to be .string. Notice the “!” mark. This is returned as an optional string, and makes it even easier for our code to play nice with Core Data as will be illustrated later. The Categories.JsonKeys.Name is a reference to a static string in the Core Data class. This is to ensure random strings around the system is not written with errors, and makes it easier to change these in the future without changing the entire system. It also makes it harder to misspell as most errors would get caught at compile time. The same is done for Core Data.

category[Categories.JsonKeys.Name].string!

To end this section, an excerpt of the code below shows how the parsing of the entire dataset is done from the server.

class func updateCoreDataFromServer(completionHandler: () -> Void ) {
        let context = getContext()
        
        emptyCoreData()
        
        loadDataFromURL(NSURL(string:"http://recipository.dk/api/categories/")!) { (data, error) -> Void in
            //tjekker for den har hentet noget.
            if let json = data {
                
                let categories = JSON(data: json).array //categories er json i deres verden.
                
                //first go through categories
                for category in categories! {
                    
                    //Create category for later addition to tempData
                    let newCategory = NSEntityDescription.insertNewObjectForEntityForName(Categories.CoreDataKeys.EntityName, inManagedObjectContext: context) as! Categories
                    newCategory.objectId = category[Categories.JsonKeys.Id].int!
                    newCategory.name = category[Categories.JsonKeys.Name].string!
                    
                    do {
                        try context.save()
                    } catch {
                        print("datamanager.updatecoredatafromserver could not save category: \(category[Categories.JsonKeys.Name].string!)")
                    }
                    
                    //now go through recipes
                    let extractedRecipes = category[Recipes.JsonKeys.Array].array
                    let recipeRelation = newCategory.mutableSetValueForKey("recipes")
                    
                    for recipe in extractedRecipes! {
                        
                        let newRecipe = NSEntityDescription.insertNewObjectForEntityForName(Recipes.CoreDataKeys.EntityName, inManagedObjectContext: context) as! Recipes
                        newRecipe.objectId = recipe[Recipes.JsonKeys.Id].int!
                        newRecipe.categoryId = recipe[Recipes.JsonKeys.CategoryId].int!
                        newRecipe.name = recipe[Recipes.JsonKeys.Name].string!
                        newRecipe.hearts = recipe[Recipes.JsonKeys.Hearts].number!
                        newRecipe.difficulty = recipe[Recipes.JsonKeys.Difficulty].number!
                        newRecipe.directions = recipe[Recipes.JsonKeys.Directions].string!
                        newRecipe.image = recipe[Recipes.JsonKeys.PictureBase64].string!
                        newRecipe.imageUrl = recipe[Recipes.JsonKeys.PicturePath].string!
                        
                        recipeRelation.addObject(newRecipe)
                        
                        do {
                            try newRecipe.managedObjectContext?.save()
                            //try context.save()
                        } catch {
                            print("datamanager.updatecoredatafromserver could not save recipe: \(recipe[Recipes.JsonKeys.Name].string!)")
                        }
                        
                        //now go through ingredients
                        let extractedIngredients = recipe[Ingredients.JsonKeys.Array].array
                        let ingredientRelation = newRecipe.mutableSetValueForKey("ingredients")
                        for ingredient in extractedIngredients! {
                            
                            let newIngredient = NSEntityDescription.insertNewObjectForEntityForName(Ingredients.CoreDataKeys.EntityName, inManagedObjectContext: context) as! Ingredients
                            newIngredient.objectId = ingredient[Ingredients.JsonKeys.Id].int!
                            newIngredient.recipeId = ingredient[Ingredients.JsonKeys.RecipeId].int!
                            newIngredient.ingredient = ingredient[Ingredients.JsonKeys.Ingredient].string!
                            newIngredient.amount = ingredient[Ingredients.JsonKeys.Amount].string!
                            
                            ingredientRelation.addObject(newIngredient)
                            
                            do {
                                try newIngredient.managedObjectContext?.save()
                                //try context.save()
                            } catch {
                                print("datamanager.updatecoredatafromserver could not save recipe: \(ingredient[Ingredients.JsonKeys.Ingredient].string!)")
                            }
                        }
                    }
                }
            }
            
            print("Reloading data from server completed")
            
            //Den her linje er nu nødvendig når man opdaterer UI fra bagrunden.
            dispatch_async(dispatch_get_main_queue(), {
                completionHandler()
            })
            
        }
    }

6.4 Core Data

Core data was a bit of a headache to figure out as the documentation in swift was quite lacking, or only touched on the basics. Smyth (2015) was used to get the grasps of the basics, while Nahavandipoor (2015) touched on the more complex topics, and finally Jacobs (2015) explained how relations worked. It is a general problem currently to find updated materials for Swift.

Recipository.xcdatamodeld was defined as below, with similar cardinalities as the database. Note the relationship properties at the bottom of each table. These contain the relation so it is possible to move from an ingredient, to the recipe that contained it, and on the the category by simply writing “Ingredient.recipe.category”. This is extremely useful. Notice Id’s from the server database are called objectId to separate them from the internal database id.

Screenshot 2015-11-28 21.45.52

To make sure we do not use an extensive amount of time passing the data from the NSManagedObject to our own classes, Core Data supports doing this automatically. To do this an extension to the Recipes class is needed. Each property is annotated with @NSManaged. This tells Core Data the property is to be filled with the attribute with the same name. With this implementation one can simply cast a fetched object to the managed class directly.

extension Recipes {

    @NSManaged var categoryId: NSNumber?
    @NSManaged var difficulty: NSNumber?
    @NSManaged var directions: String?
    @NSManaged var hearts: NSNumber?
    @NSManaged var objectId: NSNumber?
    @NSManaged var image: String?
    @NSManaged var imageUrl: String?
    @NSManaged var name: String?
    @NSManaged var category: Categories?
    @NSManaged var ingredients: NSSet?

}

An example of the casting can be found below on line 17. The example below also shows how to fetch data from Core Data. Notice the predicate on line 8. This is the filter option, or the where statement is traditional SQL.

        class func getRecipes(categoryId: NSNumber) -> [Recipes] {
            
            let context = DataManager.getContext()
            
            //reading from
            let request = NSFetchRequest(entityName: Recipes.CoreDataKeys.EntityName)
            request.returnsObjectsAsFaults = false
            request.predicate = NSPredicate(format: "\(Recipes.CoreDataKeys.CategoryId) == %@", categoryId as NSNumber)
            
            var returnThis = [Recipes]()
            
            do {
                let results = try context.executeFetchRequest(request)
                
                //går igennem resultaterne
                if results.count > 0 {
                    returnThis = results as! [Recipes]
                }
                
            } catch {
                print("error - could not load recipes")
            }
            
            return returnThis
            
        }

To access the relationships between objects created with Core Data, the example below shows how simply accessing the relation property and casting the result solves this problem.

for i in recipe.ingredients! {
                let ingredient = i as! Ingredients
                
                ingredients = ingredients + "<p>\(ingredient.amount!) - \(ingredient.ingredient!)</p>\n"
                
            }

 

To delete from Core Data simply fetch the object from the database, and use the context to delete the object as shown below.

let context = DataManager.getContext()
//Find category and delete
let categoryToDelete: Categories = DataManager.CategoryHandler.getCategory(id as NSNumber)
context.deleteObject(categoryToDelete)
                                
do {
    try context.save()                                
} catch {
    print("failed deleting category from core data - reloading data from server")
}

 

6.5 ScrollViews

As some problems were experienced implementing a scroll view with auto layout, it seemed prudent to walk through it in a separate section in the implementation. The solution below was inspired by Natasha The Robot (2014) in regards to the general constraints, and Skywinder (2015) in regards to the added constraint to get the content view inside of the scroll view to automatically infer the height.

To get started the controller needs to have UIScrollViewDelegate added.

class RecipeController: UIViewController, UIScrollViewDelegate, MFMailComposeViewControllerDelegate {

Screenshot 2015-11-28 21.41.59

The illustration above shows how the layout was constructed, while the one below show the hierarchy of the views. Notice the scroll view is directly below the main view. Below the scroll view is another standard view called Content View. This is an important part. It is not defined anywhere, but ScrollView needs a single container inside it to be able to calculate the height of the content. Next take a look at the constraints. The scroll view, and Content view has set “Content View.width = width” and the same with height. Same constraint are present on the Content view. This makes sure they are aligned perfectly. All of the components inside the view are constrained to the top and the sides. Even the hearts and difficulty labels are inside a view to the view can have the same constraints. All of this is described by Natasha The Robot (2014). 

Screenshot 2015-11-28 21.42.37

The final factor for the constraints was described by Skywinder (2015).  For the height to be calculated correctly, as the example has text fields that move in size dependent on content, the final item in the view needs to have a bottom constraint as seen below (Bottom Space to). While this might not make sense, just do it. Another workaround would just be defining the height to a specific number of pixels, though this would not be a good solution for changing content sizes. If all of these settings are not set, the view will simply not move, or will look strange. Natasha The Robot (2014), even included an anecdote about apple engineers trained in this took 40 min to solve it, while none of her professional relations had been able to solve the problem. This is a real problem with Scroll Views.

Screenshot 2015-11-28 21.42.11

 

6.6 Print

The printing functionality is easy to implement with a bit of HTML, and a quick guide by Knopper (2015).  The example below shows how this is done. The most complicated part is creating the HTML, and that is easy. Mind line 35 that defines the margins around the page, the rest is self explanatory.

    // MARK: Print functionality
    
    @IBAction func printRecipe(sender: UIButton) {
        let printController = UIPrintInteractionController.sharedPrintController()
        
        let printInfo = UIPrintInfo(dictionary:nil)
        printInfo.outputType = UIPrintInfoOutputType.General
        printInfo.jobName = "print Job"
        printController.printInfo = printInfo
        
        
        // MARK: Prepareing Data for Print
        
        var ingredients = ""
        
        for i in recipe.ingredients! {
            let ingredient = i as! Ingredients
            
            ingredients = ingredients + "<p>\(ingredient.amount!) - \(ingredient.ingredient!)</p>\n"
            
        }
        
        let html: String = "<html>" +
            "<body>" +
            "<h2>\(recipe.name!)</h2>" +
            "<img src='http://recipository.dk/\(recipe.imageUrl!)' style='max-height: 300px'>" +
            ingredients +
            "<p>\(recipe.directions!)</p>" +
            "</body>" +
        "</html>"
        
        // MARK: Finishing up print job.
        
        let formatter = UIMarkupTextPrintFormatter(markupText: html)
        formatter.contentInsets = UIEdgeInsets(top: 35, left: 35, bottom: 35, right: 35)
        printController.printFormatter = formatter
        
        printController.presentAnimated(true, completionHandler: nil)
    }

 

6.7 Emailing

class RecipeController: UIViewController, UIScrollViewDelegate, MFMailComposeViewControllerDelegate {

For emails to work, two things needs to be in order. First include the MFMailComposeViewControllerDelegate in your View Controller as shown above. Next create a method to create the object as below. For a more thorough explanation follow the guide by Knopper (2014). Notice the center area is just the HTML creation again, exactly as in the print implementation. Remember most email clients does not support base64 images as normal web browsers do, so be sure to link to an actual image. Alternatively attach the base64 image as an email attachment as a workaround. Unfortunately the image will not be placeable in the email content if attached in this way.  

    @IBAction func emailRecipe(sender: AnyObject) {
        //Check to see the device can send email.
        if( MFMailComposeViewController.canSendMail() ) {
            print("Can send email.")
            
            let mailComposer = MFMailComposeViewController()
            mailComposer.mailComposeDelegate = self
            
            var ingredients = ""
            
            for i in recipe.ingredients! {
                let ingredient = i as! Ingredients
                
                ingredients = ingredients + "<p>\(ingredient.amount!) - \(ingredient.ingredient!)</p>\n"
                
            }
            
            //Set the subject and message of the email
            mailComposer.setSubject(recipe.name!)
            
            mailComposer.setMessageBody(
                "<html>" +
                    "<body>" +
                    "<h2>\(recipe.name!)</h2>" +
                    "<img src='http://recipository.dk/\(recipe.imageUrl!)' style='max-height: 400px; max-width: 100%;'>" +
                    ingredients +
                    //"<p>Difficulty: \(recipe.difficulty!)</p>" +
                    //"<p>Hearts: \(recipe.difficulty!)</p>" +
                    "<p>\(recipe.directions!)</p>" +
                    "</body>" +
                "</html>", isHTML: true)
            
            /*
            // Til hvis jeg senere vil bruge attachments i stedet for html.
            if let fileData = NSData(base64EncodedString: recipe.image!, options: NSDataBase64DecodingOptions.IgnoreUnknownCharacters) {
                print("File data loaded.")
                mailComposer.addAttachmentData(fileData, mimeType: "image/png", fileName: "RecipePicture")
            }
            */
            
            self.presentViewController(mailComposer, animated: true, completion: nil)
        }
    }

6.8 Image Handling

class AddRecipeViewController: UITableViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

In the add recipe view, an image picker was implemented to enable choosing content from the devices image library. For it to work, first add UIImagePickerControllerDelegate to your class as above. Now just implement the imagePickerController as below on line 17, and call it as implemented in the two IBAction’s below. Take a close look at line 5 and 12, as they define where the image comes from. The first calls the photo library, while the second popups the camera. Inspiration for implementation from Hanan (2015)

// MARK: - UIImagePickerController related
    
    @IBAction func ChoosePictureAction(sender: AnyObject) {
        imagePicker.allowsEditing = false
        imagePicker.sourceType = .PhotoLibrary
        
        presentViewController(imagePicker, animated: true, completion: nil)
    }
    
    @IBAction func TakePictureAction(sender: AnyObject) {
        imagePicker.allowsEditing = false
        imagePicker.sourceType = .Camera
        
        presentViewController(imagePicker, animated: true, completion: nil)
    }
    
    func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
        if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
            ImageViewOutlet.image = pickedImage
            imageToPass = pickedImage
        }
        
        dismissViewControllerAnimated(true, completion: nil)
    }

As the camera snapping feature above was implemented, issues with the size of the image occurred. To solve this an extinction to UIImage was implemented to be able to resize the image before sending it to the server for processing. The code below is credited to Dabus (2015)

extension UIImage {
    func resize(scale:CGFloat)-> UIImage {
        let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: size.width*scale, height: size.height*scale)))
        imageView.contentMode = UIViewContentMode.ScaleAspectFit
        imageView.image = self
        UIGraphicsBeginImageContext(imageView.bounds.size)
        imageView.layer.renderInContext(UIGraphicsGetCurrentContext()!)
        let result = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return result
    }
    func resizeToWidth(width:CGFloat)-> UIImage {
        let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: width, height: CGFloat(ceil(width/size.width * size.height)))))
        imageView.contentMode = UIViewContentMode.ScaleAspectFit
        imageView.image = self
        UIGraphicsBeginImageContext(imageView.bounds.size)
        imageView.layer.renderInContext(UIGraphicsGetCurrentContext()!)
        let result = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return result
    }
}

7. Final Features, Presentation and Evaluation

For the final evaluation these videos were created to give an overview of all the main features and functionality of the client side application. (Please make sure to watch both as it showcases the entire interface, that cannot be seen elsewhere!)

The iPhone demo below is the most important to watch as it showcases all overall functionality. The iPad Landscape video showcases the split view functionality, and last the iPad Portrait video shows some of the issues encountered in the think aloud test, and shows the pull in function added by the split view.

Some of the changes since the last evaluation includes:

  • New icons for Print, Email, and Refresh from Server
  • Icons and interface disables when refreshing to inform the user the content is not ready for use yet. Same is true when recipes are refreshing
  • Added Create and Delete Recipe functionality as Modal View
  • Added Picture snapping / image add functionality
  • Added warning window before deleting Categories or Recipes, as deletions are recursive
  • Added header color to match logo. Teal for normal pages, and Orange for add recipes page, so color difference implies functionality
  • Move to Core Services, and Alamofire
  • And more. See the videos above

The observations from the think aloud test resulted in the following findings. On the iPad versions users struggled to figure out how to get to the Categories View, as no button was apparent as on the iPhone version. One user suggested the first page (the details view), should include a small guide on how to use the application. The target group users liked the added icons for print and email, but did not like the new refresh icon, as they found the cloud icon confusing. New functionality was liked, like disabling the interface during refreshes, and the ability to add recipes, even though one commented there would be too much information to write on the iPhone or ipad in one go. When creating recipes, other comments came up too. The pre filled text should auto disappear once the field is clicked, or just be empty. One expressed it was annoying to delete the placeholder text. The same user noticed only one Ingredient could be added, and not several. While filling out the Instructions field, problems occurred with figuring out how to change lines. The return button had the wording OK, and not return. This confused the user. On the iPad version, all participants agreed the picture was too narrow, and it would be a good feature to be able to press it and enlarge it. The same comment did not exist with the iPhone version. Another comment was deletion of content introduced waiting time in the interface that does not conform well with the nonfunctional requirements. This is due to a problem encountered with Core Data, and was described as a bug in Core Data when searching online. For that reason the slow deletion times were accepted for now as this is an alpha version. A workaround would need to be implemented for final release. In general once the initial hurdle of finding the categories view on the iPad was over (if the ipad was in portrait), the users had an easy time navigating the application and find the functions.

The presentation in the iOS Course revealed only known concerns, like the phone view freezing for a few seconds when the recipe, or category deletion were performed. This is a known issue due to a bug that was encountered in Core Data. One also asked for the status on sharing recipes with other users inside the app. This was of cause not possible to do, as the login functionality still was not implemented. The rest of the response was positive.


Discussion and Conclusion

Initial evaluations had feedback, but in smaller portions. Final prototype had a large amount of feedback in comparison; constructive feedback. The reason could partly be the more correct execution of the think aloud test, as it was conducted more systematically than the first two evaluation sessions. The feedback highlights obvious and large problems with the applications usability that needs attention before release. As the users suggested, portrait mode exhibited problems, as well as the launch screen being uninformative especially on the iPad in portrait mode. A user would not know where to go from the start page intuitively because buttons are missing from this mode. The user evaluation are useful as they highlight problems that might not have been seen as a problem otherwise.

Let’s take a look at the currently missing features and known issues:

  • Feedback from final evaluation
  • Add more ingredients to recipes
  • Feedback on sync activity missing – to let the user know what is happening, and for how long they will have to wait for the sync to complete.
  • Redesign of sync functionality to be more effective, and better handle errors
  • Search Functionality missing
  • Login Functionality missing
  • Rate recipes by difficulty and how much the user likes it
  • Share within the App with another person using the App
  • Design of the Web Implementation
  • Attach Gallery to recipe

The list above highlights that all goals put forth has been reached. So in conclusion i am happy with the results. The set goals were reached, and exceeded. The biggest problem i faced was lacking explanations on how parts of the system works. Current tutorials on the internet in swift mostly cover the basics, but not much advanced content, as the language is young and is changing rapidly. Even code a year old needs rewriting due to Swift 2, and Apple changing function calls in the frameworks. The changing frameworks, and language made it a good learning experience with healthy bumps along the way.

If i should say anything negative, it would be that i should have started developing sooner, rather than so late in the process as i did (Take a look at the GIT commits). Unfortunately, this was forced by external factors. I must admit if i could have chosen another project with more interface to show, i would properly have chosen that in hindsight, as most of the functionality and code does not have many visual components. This makes it harder to show just how much work i put into the project.

The design of the application and the visual identity went well, and the response has been overwhelmingly positive, even from designers at work, and previous learning institutions. I am also happy with the choices made in the interface design. By sticking to standard interface elements, the users had an easy time maneuvering around the interface in all situations apart from the portrait iPad mode. It really helps to use these known elements, that force recognizability with the user base.

For the future i have Facebook, and maybe Google authentication in the works. A new website following the new visual direction will also be developed. When the above is solved, it will be released on the app store. Depending on the feedback i might develop an android version too. The price will properly be free as the price for keeping the site running is about 100kr a year, and it is mostly a showcase project for future other projects i might get involved in.

Another big change i would like to introduce is the separation of the Category, Recipe, and Ingredients Handlers from the DataManager. The Separation should function in a way where the handlers only managed core data, and had no responsibility for communicating to the server at all. DataManager should be renamed to Sync Handler, and only be responsible for handling the synchronization to the server. This would result in a more responsive interface, and enable caching of the data on the device, instead as a Slave Mirror as it currently functions as.

Updated Data Layer

As the illustration above exemplifies the controller should only interact with the handlers, apart from forcing a sync to happen. In hindsight this should have been the design from the beginning, so new objects could be created in core data without existing on the server. The problems encountered which kept the project from reaching its nonfunctional performance requirements, would have been easily countered by this design. Unfortunately i was colored by my web development experience. On the bright side, it was a valuable lesson to learn.

Great course! Thank you for the trip, i will definitely recommend it to others as i learned a lot.

This report has 15 normal pages


References

  • Alamofire, (2015). Alamofire/Alamofire. [online] Available at: https://github.com/Alamofire/Alamofire [Accessed 28 Nov. 2015].
  • Bass, L., Clements, P. and Kazman, R. (2013). Software architecture in practice. Upper Saddle River, NJ: Addison-Wesley.
  • Buxton, W. (2007). Sketching user experiences. Amsterdam: Elsevier/Morgan Kaufmann.
  • Dabus, L. (2015). How do I resize the UIImage to reduce upload image size. [online] Stackoverflow.com. Available at: http://stackoverflow.com/questions/29137488/how-do-i-resize-the-uiimage-to-reduce-upload-image-size [Accessed 28 Nov. 2015].
  • Hammad, S. (2015). The Safety Net » json. [online] Safehammad.com. Available at: http://safehammad.com/tag/json/ [Accessed 28 Nov. 2015].
  • Hanan, N. (2015). Choosing Images with UIImagePickerController in Swift. [online] Coding Explorer Blog. Available at: http://www.codingexplorer.com/choosing-images-with-uiimagepickercontroller-in-swift/ [Accessed 28 Nov. 2015].
  • Jacobs, B. (2015). Core Data and Swift: Relationships and More Fetching – Envato Tuts+ Code Tutorial. [online] Code Envato Tuts+. Available at: http://code.tutsplus.com/tutorials/core-data-and-swift-relationships-and-more-fetching–cms-25070 [Accessed 28 Nov. 2015].
  • Knopper, A. (2014). Send Email Tutorial in iOS8 with Swift. [online] iOScreator. Available at: http://www.ioscreator.com/tutorials/send-email-tutorial-ios8-swift [Accessed 28 Nov. 2015].
  • Knopper, A. (2015). AirPrint Tutorial in iOS8 with Swift. [online] iOScreator. Available at: http://www.ioscreator.com/tutorials/airprint-tutorial-ios8-swift [Accessed 28 Nov. 2015].
  • Krug, S. (2009). Rocket surgery made easy. Pearson.
  • Larman, C. (2005). Applying UML and patterns. Upper Saddle River, N.J.: Prentice Hall PTR.
  • Nahavandipoor, V. (2015). IOS 8 swift programming cookbook. Sebastopol, CA: O’Reilly Media.
  • Natasha The Robot, (2014). iOS: How To Make AutoLayout Work On A ScrollView. [online] Available at: http://natashatherobot.com/ios-autolayout-scrollview/ [Accessed 28 Nov. 2015].
  • Percival, R. (2015). The Complete iOS 9 Developer Course – Build 18 iOS9 Apps. [online] Udemy. Available at: https://www.udemy.com/the-complete-ios-9-developer-course/ [Accessed 28 Nov. 2015].
  • Skywinder, (2015). UIScrollView Scrollable Content Size Ambiguity. [online] Stackoverflow.com. Available at: http://stackoverflow.com/questions/19036228/uiscrollview-scrollable-content-size-ambiguity [Accessed 28 Nov. 2015].
  • Smyth, N. (2015). iOS9 App Development Essentials. eBookFrenzy.
  • SwiftyJSON, (2015). SwiftyJSON/SwiftyJSON. [online] Available at: https://github.com/SwiftyJSON/SwiftyJSON [Accessed 28 Nov. 2015].
  • Williams, R. (2014). The non-designer’s design book. 4th ed. Pearson.

Leave a Reply