Lab 6 : Social Networks App

Objective
  • Leverage DrawableGraph and GraphView to build a new app for showing and examining social networks.

Table Of Contents

Now that you’ve created a way to search through a social network graph, you will build an iOS app for exploring social networks. (Note: Visualizing large graphs coherently and efficiently is a challenging in problem, so we will stick to data sets smaller than Marvel Comics for this part.)

Your app’s interface will be very much like last week. You will continue to use your GraphView to display and manipulate graphs, but unlike last week the data files will come with locations for all of the nodes so you do not have to lay out the graph yourself. Once displayed, the user can choose two nodes from the graph by touching one and tracing a line to another. Your program should then highlight the edges (and possibly the nodes) on the shortest path connecting the selected nodes. As the user performs this gesture, you should draw an arrow from where the gesture started to where the finger currently is. The user should be able to repeat the gesture multiple times, with the resulting path being updated in response to each gesture, and the two-finger Pan, Zoom, and Zoom-to-Max gestures from Connect The Dots should be implemented as before.

One possible way to proceed is to build the model, then the view, and then the controller, since both the model and view are relatively self-contained.

Project Setup

In XCode, you will use the same GraphProjects workspace, and work in the SocialNetworksApp target inside the UIClients Project. The project should be set up properly, but here are some steps that were required to make it so.

  1. The SocialNetworks target will share a number of source files with the GraphViz target: GraphView, GraphItem, Files, ModelToViewCoordinates, CGPointArrow. To make those files visible to your new target, select the “UIClients” project in the Project Navigator. Then select the “SocialNetworksApp” target and view the “Build Phases” tab, as shown below. You can then drag the source files you wish to include into the “Compile Sources” list:

You can determine the targets to which a file belongs by selecting it in the Project Navigator and then looking at its “Target Membership” in the File Inspector, as shown below.

If you encounter errors that XCode cannot find a class you declared, a good first check is that the file is included in the target you are building.

  1. You also must include GraphADT.framework in the new target:

Model

For the model, we could extend last week’s DrawableGraph class with the new features we need, but that class wasn’t really designed to be extensible and has a lot of GraphViz-specific code in it. We could copy-paste-and-modify the old class to suit our current needs, but only at the cost of introducing a lot of duplicate code. Instead, we can use this as a good opportunity to practice an important part of software development:

Fix broken windows before they become raging dumpster fires.

Broken windows include designs that no longer fit our needs. To that end, we’re going to decouple DrawableGraph from the Conroller and View and then refactor DrawableGraph to support better code reuse and extensibility.

Weakening the Coupling Between the Model and Controller/View

Commit all changes before proceeding. Refactoring involves potentially many small changes in the code, and you’ll want to be able to undo them should something go wrong in the middle of the process.

I’d like you to revisit your DrawableGraph design in GraphViz for a moment. How tightly coupled is that class to the rest of your system? It is part of the model and clearly depends upon your Graph ADT, but does it depend on anything from the controller or view? That is, could you take DrawableGraph (and the Graph framework) and use them in an entirely different program? If not, then your model is too tightly-coupled to the rest of your system and I’d like you to refactor the class to eliminate those dependencies. For example, your DrawableGraph may currently create the GraphItems for your view to display, meaning that it is coupled to your view. If that is the case, you should remove that functionality from DrawableGraph. A more appropriate place would be, for example, the controller’s updateUI() method. To support this you may need add a few small helper methods to DrawableGraph to give the controller access to the underlying graph’s nodes and edges, but that doesn’t introduce any new coupling since the controller already “owns” the model and interacts with it. (Part of the reason to address this now is that we will be reusing DrawableGraph this week and we’ll need to change how the graph items are created…)

Refactoring DrawableGraph

Commit all changes to GitHub before proceeding. Refactoring involves potentially many small changes in the code, and you’ll want to be able to undo them should something go wrong in the middle of the process.

First, note what is common between GraphViz and SocialNetworks. We need to draw a graph on the screen. But in the case of GraphViz, the DrawableGraph picks initial node locations and provides an algorithm for adjusting them. In the case of SocialNetworks, the client provides the locations and never moves nodes, but it also needs a way to find paths in the underlying graph. To support these needs (and likely others), we’ll go back and revise DrawableGraph to capture only the common parts and then create subclasses to create the specialized forms.

For the moment, switch back to the GraphViz scheme. To begin refactoring, create a new class GraphVizDrawableGraph that is a subclass of DrawableGraph. Then move all code related to GraphViz out of DrawableGraph and into the subclass. That should leave you with two class like the following:

/* ...all your docs and specification as before... */
public class DrawableGraph {
  
  /* protected */ let graph : Graph = Graph()
  /* protected */ var locations : [String:CGPoint] = [:]
  
  /* your abs function and rep invariant */

  public init() { }
  
  /* protected */ func checkRep() { ... }

  ... Code common to *all* DrawableGraphs ... 

}
class GraphVizDrawableGraph : DrawableGraph {
  
  public override init() {
    super.init()
  }

  public convenience init(jsonForGraph json : [String:Any]) {
    self.init()
    ... as before ...
  }
  
  // ... All code related to the layout algorithm ...
}


Virtually all OO languages other than Swift support protected properties or fields, and for good reason: they are useful for designing extensible classes. Since we don’t have protected I’ve added the “protected” keyword as a comment where appropriate above, and we’ll just never access those properties/methods outside of DrawableGraph and subclasses. (Yes, this is lame, but the best we can do in Swift… I don’t think we’ll do this again this semester…) Also note that the two intializers in GraphVizDrawableGraph both call a different init method from either the super class or this class itself.

The last step is to modify your controller, and possibly some of your unit tests, to use a GraphVizDrawableGraph instead of a DrawableGraph. Once you make that change, verify GraphViz works as before.

SocialNetworksDrawableGraph

Now we can switch back to the SocialNetworks scheme and write our SocialNetworksDrawableGraph model. With the above refactoring, it should be relatively short, essentially an initializer that creates a graph and node locations from the JSON object, and a method to compute shortest paths:

public convenience init(jsonForGraphWithPropertiesAndLocations json : [String:Any])

public func shortestPath(from src: String, to dst: String) -> ...

Before proceeding, be sure to unit test your new drawable graph class:

Testing

Add a new unit test file SocialNetworksDrawableGraphTests and test your graph loading code and your graph searching code before working on the controller. Focus only on tests for new functionality in SocialNetworksDrawableGraph. That is, don’t test everything you have already tested. We’ll consider it sufficient just to write a test ensuring that a SocialNetworksDrawableGraph can load a small JSON file and return a shortest path properly. Here’s one test that fits that mold:

import XCTest
@testable import SocialNetworks

class SocialNetworksModelTests: XCTestCase {
  
  /// Test that your model can load and find paths properly.
  func testSocialNetworksDrawableGraphModel() {
    let jsonString = """
      {
        "nodes" : ["A", "B", "C" ],
        "edges" : [
          {"src" : "A", "label" : "s1", "dst" : "B" },
          {"src" : "A", "label" : "s2", "dst" : "C" },
          {"src" : "B", "label" : "s3", "dst" : "C" },
        ],
        "locations": {
          "A" : { "x": 1, "y": 1 },
          "B" : { "x": -1, "y": 1 },
          "C" : { "x": -1, "y": -1 }
        }
      }
    """
    let json = (try? JSONSerialization.jsonObject(with: jsonString.data(using: .ascii)!)) as! [String:Any]
    let model = SocialNetworksDrawableGraph(jsonForGraph: json)
    
    // Verify a few properties of our model:
    let nodes = model.nodes()
    XCTAssertEqual(Set(nodes), Set(["A", "B", "C"]))

    let location = model.location(of: "B")
    XCTAssertEqual(location.x, -1, accuracy: 0.001)
    XCTAssertEqual(location.y, 1, accuracy: 0.001)
    
    // Verify short path works:
    // TODO: assert model.shortestPath(from: "A", to: "C") finds a path
    // TODO: assert model.shortestPath(from: "C", to: "A") does not find a path
  }
}

Controller and View

The UI for you app should follow the same basic structure as Connect The Dots and GraphViz. (Don’t worry - we’ll start writing other sorts of UIs next week!)

  1. Set up the storyboard, defining a new custom controller called SocialNetworksController. It can borrow parts from your earlier labs. (Yet another place where we could reconsider our design and better plan for code reuse…)

  2. Since our view is essentially a GraphView with the additional ability of drawing an extra arrow on top of the graph, we can simply creat a small subclass GraphViewWithOverlayPath of GraphView with one additional public property, overlayPath. To add a bit of flexibility, we can declare overlayPath with type UIBezierPath? in case we ever wish to draw something other than a straight arrow. Other than that property, the only other necessary part of your subclass is a draw method that draws the overlayPath (provided it is not nil) after having the super class draw the graph.

  3. At this point you can test loading and displaying a graph, and adding a overlay to it. Once that is working, add a button to let the user load other graphs and create gesture recognizers for panning and zooming your view.

  4. Implement a pan gesture for selecting nodes and finding the shortest path between them. The behavior is as follows:
    • As the user pans around, an arrow should be draw from to initial touch to where the finger currently is located.
    • When the pan gesture ends, the shortest path between the nodes at the initial and final locations for the gesture should be found. All nodes and edges along that path should be hilighted. If there is no path or the initial/final touch was not in a node, nothing should be highlighted.

    While the standard UIPanGestureRecognizer does provide a location(in:) method to get the current finder position, it provides no way to get the initial position. Even the location it reports when it sends the .begin event is not the initial position (but rather where the finger was when the pan was first recognized as a pan and not a touch). We provide a PanGestureRecognizerWithInitialTouch custom gesture recognizer that does store the initial touch location in its initialTouchLocation. To use a custom recognizer, drag a “Custom Gesture Recognizer” from the Object Library to the view you wish to associate with the gesture, and then use the Identity Inspector to change its class to PanGestureRecognizerWithInitialTouch. (Don’t forget to add the source file for that class to your project too!)

    Think carefully about how this gesture action interacts with the rest of your class, particularly updateUI(). You will likely need to record some additional information in the controller’s properties – eg, the path, but try to avoid adding any unecessary complexity.

  5. Spend a few minutes on your UI and usability aspects:

  • You may wish to skip drawing edge labels except on edges that are part of a selected path. This should not require changing your GraphView.

  • The edge labels can be pesky to read because an edge’s arrow may run right through its text. The NSAttributedStringKey.backgroundColor key enables you to specify the background color of text. For edge labels, you can set that attribute to UIColor.white to make them more readable. This does require modifying your GraphView in small ways.

  • Depending on your updateUI code, you may notice that unhighlighted edges are drawn over highlighted edges. You can improve the aesthetics if you ensure that the highlighted edges are always on top. Again, in a well-designed program, this should be a self-contained local change.

Extensions

Here are a few ideas on how you could extend your app, but there are many other possibilities as well. Be creative!

  • Continuously show the shortest path from the source node to whatever node is under the user’s finger as it pans around the screen.

  • Make the pan gesture “snappy” in that the arrow overlay starts at the center of the node closest to the initial touch location and ends in the center of the node closest to the current touch location. It may make sense to only snap to a node if the touch is within a certain distance of it.

  • At a gesture to identify all nodes within \(k\) steps of a note touched on. Make \(k\) configurable by the user.

  • Add functionality to find the shortest path from a starting node to every other node in the graph. This is a minimal spanning tree rooted at the source. Include a new gesture to trigger this feature.

  • Enable the user to add new nodes and edges to the graph after it has been loaded.

  • There were three places where we could not leverage code from earlier labs without introducing some degree of code duplication: the test driver, the DrawableGraph, and the controller. We’ve already addressed the DrawableGraph. Revisit the other parts and try to provide more extensible designs to avoid some of the code duplication problems we encountered.