== SWIFT AND IOS AND IPHONES

   A booklet about developing ios applications with swift. This is really 
   good. Apple knows what they are doing. It all works. Xcode works. What 
   a surprise.

TABLEVIEWS

GOTCHAS FOR TABLEVIEWS ....

  If you set the style for the UITableViewCell in the Interface Builder, then
  dont register the cell.

CONSTANTS

  Constant are indicated by the "let" keyword (as opposed to "var"). Constants
  can only be assigned once. Which is really just a trick to stop the 
  programmer tricking herself. 

  * define and assign a constant
  --------
    let tree: Tree
    tree = filteredTrees[indexPath.row]
  ,,,

  * another way, with inferred constant type
  >> let tree = Tree("Eucalyptus", "globulus")

OPTIONALS

  Optionals are just a data type that can be any data type or "nil".
  They can be "unwrapped" (get to the datatype itselff) either explicitly
  or implicitly.

  * a trick to unwrap a optional conditionally??
  >> if let detailCandy = detailCandy { ... }

DATES

  * create and compare dates
  ----
    let now = Date()
    let oneminute = Date(timeIntervalSinceNow: 60)
    let onehour = Date(timeIntervalSinceNow: 3600)
    if now < oneminute { etc }
  ,,,

STRINGS

  * check if a textfile string has non-whitespace characters
  --------
    if !textField1.text.trimmingCharacters(in: .whitespaces).isEmpty {
      print("Non-whitespace!") 
    }
  ,,,

  * convert integer to string
  >> let s = String(i)
  >> let s = String(i!)  // if i is an optional. A Gotcha!!!

TUPLES

   Tuples are cool. They can contain a mixed bag of data types, unlike
   an array or dictionary. You can return them from a function or method,
   which is a good way to return more than one value from a method.

   * subscript a tuple with a number (cant be a variable)
   -----
     var tuple = ("size", 4, 4.0) 
     var name: String = tuple.0
   ,,,

ARRAYS 

  * sort a string array
  -----
     let students: Set = ["Kofi", "Abena", "Peter", "Kweku", "Akosua"]
     let sortedStudents = students.sorted()
     print(sortedStudents)
     // Prints "["Abena", "Akosua", "Kofi", "Kweku", "Peter"]"
  ,,,

  * create the array ["zz", "zz", "zz"]
  >> let aa = Array(repeating: "zz", count: 3) 

FILTERING ARRAYS ....

  Filtering is a way of returning a subset of an array based on
  a boolean function.

  * filter an array of trees
  -----
   filteredTrees = trees.filter({(tree: Tree) -> Bool in
      return tree.genus.lowercased().contains(searchText.lowercased())
    })
  ,,,

MAPPING ARRAYS ....

  Mapping an array is just like looping through an array, performing some
  transformation (mapping) on each element and creating a new array with
  the new elements.

  * using map and flatMap with arrays
  -------
   let numbers = [1, 2, 3, 4]

   let mapped = numbers.map { Array(repeating: $0, count: $0) }
   // [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]

   let flatMapped = numbers.flatMap { Array(repeating: $0, count: $0) }
   // [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]

 ,,,

DICTIONARIES

  A dictionary is a set of key value pairs.

  * increment integer value in dictionary, need to unwrap optional
  >> dict[tree.genus] = dict[tree.genus]! + 1

  * check if a key exists in a dictionary
  ---------
    //or: if let message = responseMessages["eucalypt"] {
    if (dict[tree.genus] != nil) {
        print("key exists")
    } else {
        print("key does not exist")
    }
  ,,,

  * a dictionary with array values
  -------
  var numbers = [
     "primes": [2, 3, 5, 7, 11, 13, 17],
     "triangular": [1, 3, 6, 10, 15, 21, 28],
     "hexagonal": [1, 6, 15, 28, 45, 66, 91]]
  ,,,

  
FLATMAPPING AND MAPPING A DICTIONARY ....

  * a dictionary flatmapping example (dictionary-->array)
  -------
    let dict = ["a":1, "b":2, "c":3]
    let new = dict.flatMap { "\($0.key):\($0.value)" }
     // new is array ["a:1", "b:2", "c:3"]
  ,,,,

ITERATE A DICTIONARY ....

  * print dictionary key/values with the keys sorted
  --------
    var count: Int
    for genus in results.keys.sorted() {
      count = results[genus]!
      print("\(genus): \(count)")
    }
  ,,,

  * iterate with foreach
  -----
    let numberWords = ["one", "two", "three"]
    numberWords.forEach { word in print(word) }
  ,,,,

  The array value below is sorted "in-place" (the dictionary value 
  is changed).
  
  * loop through the keys in in a dictionary with sorting
  ------
    for key in dict.keys {
      dict[key]?.sorted(by: >)
    }
  ,,,

  * print a value
  >> print(interestingNumbers["primes"]!)
  // Prints "[17, 13, 11, 7, 5, 3, 2]"

  The tuple variable names dont have to be initialized in the code below

  * iterate over a dictionary using tuples 
  -------
    for (name, age) in dict {
      print("Person '\(name)' is '\(age)' years old.")
    }
  ,,,

SEARCHING A DICTIONARY ....

  * search with "contains" or "firstIndex"
  -------
    let glyphIndex = 
      imagePaths.firstIndex(where: { $0.value.hasPrefix("/glyphs") })
    if let index = glyphIndex {
        print("The '\(imagesPaths[index].key)' image is a glyph.")
    } else { print("No glyphs found!") }
  ,,,
  
COLOURS OR COLORS

  * create a custom named colour
  ------
   import UIKit

   extension UIColor {
     static let candyGreen = 
       UIColor(red: 67.0/255.0, green: 205.0/255.0, blue: 135.0/255.0, alpha: 1.0)
    }
  ,,,

CORE GRAPHICS

  There appears no way to create a path from an array of points directly
  (have to loop through).

  * find the bounding box for a set of points (can use for maps, maybe, too)
  --------------
    var points:[CGPoint] = []
    points.append(CGPoint(x:1, y:0)) //TL
    points.append(CGPoint(x:3, y:0)) //TR
    points.append(CGPoint(x:1, y:2)) //BL
    points.append(CGPoint(x:3, y:2)) //BR

    let path = CGMutablePath()
    path.move(to: points[0])
    for ix in 1...3 {path.addLine(to: points[ix])}
    let rect = path.boundingBox 
  ,,,,
  
IMAGES

  
RESIZING ....

  
TRANSPARENCY ....

  Many ios glyphs need a transparent background
  Also, use "preview" with the alpha tool to create transparent background.
  (drag the magic wand over the colour to remove then hit delete)

  convert is part of "imagemagick"

  * convert all perfectly white pixels to transparent
  >> convert image.gif -transparent white result.png    (or result.gif)

  The smaller the percentage below, the closer to white the pixels have to be.

  * convert almost white pixels to transparent. (choose a percentage)
  >> convert image.gif -fuzz XX% -transparent white result.gif

TAB BAR IMAGES ....

 These are the images that appear (usually at the bottom) which allows the 
 user to switch between "screens" in a "tab bar" interface (controlled by 
 a UITabBarController. Normally the images are used as a "transparency mask"

 ( It may be better to use pdf images which are scalable vectors but there 
   seem to be problems with this)

  The line below assumes that the glyph below has a (perfectly) white background
  that needs to be converted to transparent.

  * a one liner to convert an icon to 3 files of correct sizes+names+transparency
  >> f=name.png; g=${f%.?*}; echo $g; h=$g.transparent.png; convert $f -transparent white $h; convert -resize 75 -monitor $h $g"@3x.png"; convert -resize 50 -monitor $h $g"@2x.png"; convert -resize 25 -monitor $h $g"@1x.png"

  * convert an image into an xcode image set 
  --------
  iconset() {
    if [ -z "$1" ]; then
    echo "
     usage: $FUNCNAME fileName [baseSize]
       An xcode iOS image preparation function for apps.

       prepares 3 images @3x @2x @1x with base size 'baseSize' default 30
       (@3x will have dimensions baseSize x 3)" 
    return 1 ;
    fi
    size="$2"
    if [ -z "$2" ]; then
      size=30
    fi
    f="$1"; g=${f%.?*}; echo $g; 
    convert -resize $((size*3)) -monitor $f $g"@3x.png";
    convert -resize $((size*2)) -monitor $f $g"@2x.png";
    convert -resize $size -monitor $f $g"@1x.png"
  }
  ,,,

  * a bash function to format an image icon
  --------
  tabicon() {
    if [ -z "$1" ]; then
    echo "
     usage: $FUNCNAME fileName
       An xcode iOS image preparation function for apps.

       This tries to prepare a (monochrome?) glyph into a 
       set of 3 files of suitable sizes, names and transparency to be 
       used in a UITabBarController in an iOS app. We assume that 
       the glyph has a white background (which will be made transparent),
       is square (width=height) and that the size is > 75 pixels." 
     return 1 ;
     fi
     f="$1"; g=${f%.?*}; echo $g; h=$g.transparent.png;
     # convert $f -transparent white $h;
     # try this below if the background is not quite white
     convert $f -fuzz 15% -transparent white $h
     convert -resize 75 -monitor $h $g"@3x.png";
     convert -resize 50 -monitor $h $g"@2x.png";
     convert -resize 25 -monitor $h $g"@1x.png"
  }
  ,,,

PREPARATION OF IMAGES FOR TABBARCONTROLLER ....

   * use imagemagick "identify" to learn info about an image
   >> identify name.png

 use black and white image, background transparent, make white transparent
 see www.flaticon.com for icons to use.

    75x75 name@3x.png
    50x50 name@2x.png
    25x25 name.png  (or name@1x.png)

  The following may be useful for converting a coloured glyph into 
  something suitable as a tabbar icon

  * convert an image to monochrome (black and white, not greyscale
  >> convert -monochrome "treemap@3x.png" "treemapBW@3x.png".

IMAGE SIZES ....

  Image are usual given in 3 sizes 1x 2x 3x to cope with different device screen
  sizes

  Below is a table to give an idea of approximate image sizes for different
  contexts in an iphone app. 1x 2x and 3x sizes are given (but the app will only
  use one of these on a given phone.

  == image sizes
  image in detail view, 300x300 600x600 900x900


WEBVIEWS AND WEBKIT

  Embed webviews in a navigation controller

  * a minimal but working example
  >> https://www.ioscreator.com/tutorials/webview-ios-tutorial

LOCATION 

  The relevant api is call Core Location and the classes start with 
  "CL" such as CLLocation.

  A "device", lets call it a phone, can get information about its (and 
  presumably) the users location with various methods. Which of the 
  following is actually used by the CLLocationManager class is determined
  by how that object is configured, and the accuracy required.
  
  The gps locator:
    The most powerful and accurate but this also consumes the
    most battery power. 
  Wifi connection:
    accurate if available. wifi transmitter uses extra battery power.
  Phone "Cell":
    accurate to about 1 kilometer, always available, and does not use
    any extra power.

  * the following properties affect power consumption (gps) and accuracy
  -----
    locman.desiredAccuracy = kCLLocationAccuracyHundredMeters
    locman.distanceFilter = 100.0  // In meters.
  ,,,

 STEPS ....

  * include a key in the "info.plist" file to give a reason for wanting location access
  >> NSLocationWhenInUseUsageDescription - keyname in info.plist
  >> "Privacy - Location when in use Usage Description"
  
  There are other keys for permanent access to the users location - all under
  "Privacy".
   
  1. Add the NSLocationWhenInUseUsageDescription key to your app's Information
  Property List file. (Xcode displays this key as "Privacy - Location When In
  Use Usage Description" in the property list editor.)

  2. Create and configure your CLLocationManager object.

  3. Call the requestWhenInUseAuthorization() method of your CLLocationManager
  object prior to starting any location services.

SIGNIFICANT CHANGE LOCATION SERVICE ....

  uses wifi or phone cells - accuracy about 500 metres

  * needs "always" auth
  ----
    if authorizationStatus != .authorizedAlways {
        // User has not authorized access to location information.
        return
    }
  ,,,

  * only difference is the way the manager is started
  >> locationManager.startMonitoringSignificantLocationChanges()

  
STANDARD LOCATION SERVICE ....
 
  This is the most power hungry service (using gps). See "significant-change" 
  and "visits" service for alternatives.

  * check if location is enabled
  -------------
  func checkForLocationServices() {
    if CLLocationManager.locationServicesEnabled() {
        // Location services are available
    } else {
        // not available 
    }
  }
  ,,,

DELEGATE METHODS ....

  * get the last location (in this delegate method) and check the timestamp
  ---------
    func locationManager(_ manager: CLLocationManager,  
      didUpdateLocations locations: [CLLocation]) {
      let last = locations.last!
      let 1hour = 3600
      if last.timestamp.timeIntervalSinceNow > 1hour {
        // old data, ignore
      }
    }
  ,,,

  * respond to any error that may occur during location services
  ---------
   func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    if let error = error as? CLError, error.code == .denied {
       // Location updates are not authorized.
       manager.stopUpdatingLocation()
       return
    }       
    // Notify the user of any errors.
   }
  ,,,

  * create and configure a location manager object
  ---------
  import UIKit
  import MapKit

  class MapViewController: UIViewController, CLLocationManagerDelegate {

    let locman = CLLocationManager()

    // viewDidLoad, call startLocationServices.

    func locationManager(_ manager: CLLocationManager,  
      didUpdateLocations locations: [CLLocation]) {
      let last = locations.last!
      print("Timestamp: \(last.timestamp)")
      print("location: \(last.coordinate)")
      print("speed: \(last.speed)")
      // altitude, course, horizontalAccuracy ... etc 
    }

    // respond to errors delegate method
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
     if let error = error as? CLError, error.code == .denied {
        // Location updates are not authorized.
        manager.stopUpdatingLocation()
        return
     }       
     // Notify the user of any errors.
    }

    func startLocationService() {
       let authorizationStatus = CLLocationManager.authorizationStatus()
       if authorizationStatus != .authorizedWhenInUse && 
          authorizationStatus != .authorizedAlways {
          // User has not authorized access to location information.
          return
       }
       // Do not start services that aren't available.
       if !CLLocationManager.locationServicesEnabled() {
          // Location services is not available.
          return
       }
       // Configure and start the service.
       locman.desiredAccuracy = kCLLocationAccuracyHundredMeters
       locman.distanceFilter = 100.0  // In meters.
       locman.delegate = self
       locman.startUpdatingLocation()
    }
  }
  ,,,


GEOCODING ....

  Geocoding just means converting some kind of human readable address (street, city etc)
  into latitude and longitude and vice versa. The human-readable adddress is stored
  in a CLPlacemark object. A limited number of geocoding requests can be made per
  time-period (to Apple servers). 

  * convert lat/long to address
  ------------
  func lookUpCurrentLocation(completionHandler: @escaping (CLPlacemark?) -> Void ) {
    // Use the last reported location.
    if let last = self.locationManager.location {
        let geocoder = CLGeocoder()
            
        // Look up the location and pass it to the completion handler
        geocoder.reverseGeocodeLocation(last, 
          completionHandler: { (placemarks, error) in
          if error == nil {
            let firstLocation = placemarks?[0]
            completionHandler(firstLocation)
          } // error during geoloc
          else { completionHandler(nil) }
        })
    } // no loc available
    else { completionHandler(nil) }
  }
    // call lookup
     self.lookUpCurrentLocation { placemark in
      if placemark != nil {
        print(placemark!)
      }
      else {
        print("geocoding not available")
      }
    }
   
 ,,,

MAP REGIONS

   The code below is not necessary with recent ios, but it contains
   algorithm for finding a bounding box of a set of coordinates.

   * get all visible annotations as an object
   ---------
     func getVisibleTrees() -> [Tree] {
       return self.mapView.annotations(in: self.mapView.visibleMapRect).map{ $0 as! Tree}
     }
   ,,,

   * zoom a map to fit a set of points, objective c
   --------
     if([mapView.annotations count] == 0)
        return;

    CLLocationCoordinate2D topLeftCoord;
    topLeftCoord.latitude = -90;
    topLeftCoord.longitude = 180;

    CLLocationCoordinate2D bottomRightCoord;
    bottomRightCoord.latitude = 90;
    bottomRightCoord.longitude = -180;

    for(MapAnnotation* annotation in mapView.annotations)
    {
        topLeftCoord.longitude = fmin(topLeftCoord.longitude, annotation.coordinate.longitude);
        topLeftCoord.latitude = fmax(topLeftCoord.latitude, annotation.coordinate.latitude);

        bottomRightCoord.longitude = fmax(bottomRightCoord.longitude, annotation.coordinate.longitude);
        bottomRightCoord.latitude = fmin(bottomRightCoord.latitude, annotation.coordinate.latitude);
    }

    MKCoordinateRegion region;
    region.center.latitude = topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5;
    region.center.longitude = topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5;
    region.span.latitudeDelta = fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 1.1; // Add a little extra space on the sides
    region.span.longitudeDelta = fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 1.1; // Add a little extra space on the sides

    region = [mapView regionThatFits:region];
    [mapView setRegion:region animated:YES];

    ,,,,

MAPS WITH MAPKIT

  These are actual maps of the world, rather than the "map" function.

  * change the position of the callout bubble for an annotation
  >> view.calloutOffset = CGPoint(x: -5, y: 5)

  * will set map region so that all annotations are visible
  >> func showAnnotations([MKAnnotation], animated: Bool)
    
  * make a MKMapRect (rectangular map region) using 2 points (opposite corners)
  ----------------
      // these are your two lat/long coordinates
    CLLocationCoordinate2D coordinate1 = CLLocationCoordinate2DMake(lat1,long1);
    CLLocationCoordinate2D coordinate2 = CLLocationCoordinate2DMake(lat2,long2);

    // convert them to MKMapPoint
    MKMapPoint p1 = MKMapPointForCoordinate (coordinate1);
    MKMapPoint p2 = MKMapPointForCoordinate (coordinate2);

    // and make a MKMapRect using mins and spans
    MKMapRect mapRect = MKMapRectMake(fmin(p1.x,p2.x), fmin(p1.y,p2.y), fabs(p1.x-p2.x), fabs(p1.y-p2.y));
  ,,,

IMAGES ON MAPS ....

  The AnnotationView "pin" may be changed for a colour image @1x = 48x48 pixels
  with a transparent background. (Define a custome AnnotationView class in its
  own file, and configure it there)

  * get rid of a coloured trees shadow (background is white)
  >> convert tree.fluffy.png -fuzz 25% -transparent white result.png

SEGUES FROM MAPS ....

  * check list:
  First drag from the dock bar on the map scene to the detail scene
  in xcode storyboard and give it an identifier eg "mapToDetail

  * go to detail view when annotation callout button is pressed.
  ----------
    func mapView(_ mapView: MKMapView, 
       annotationView view: MKAnnotationView, 
       calloutAccessoryControlTapped control: UIControl) {
        performSegue(withIdentifier: "mapToDetail", sender: nil)
    }
    // Configure your destination view controller from prepare(for:sender:).

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let viewController = segue.destination as? SetNotificationViewController {
            // configure your view controller
        }
    }
  ,,,
  
ANNOTATIONVIEWS ....

  This is what is actually seen on the map

  * change the marker colour using rgb
  >> view.markerTintColor = UIColor(red: 44/256, green: 205/256, blue: 64/256, alpha: 1)

  I dont know how this works but it does.

  * open a callout for an annotation view in code (tree object is an annotation)
  >> _ = [mapView .selectAnnotation(tree, animated: true)]

  * set text on annotation view pin (one letter) put in custom AnnotationView
  >> glyphText = String(artwork.discipline.first!)

  * custom annotation view with image instead of pin
  ---------

    // register the custom annotation view in the map view controller (viewdidload)
    mapView.register(ArtworkMarkerView.self,
    forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)

    // this goes in a separate file, 
    class ArtworkView: MKAnnotationView {
     override var annotation: MKAnnotation? {
       willSet {
         guard let artwork = newValue as? Artwork else {return}
         canShowCallout = true
         calloutOffset = CGPoint(x: -5, y: 5)
         rightCalloutAccessoryView = UIButton(type: .detailDisclosure)

         if let imageName = artwork.imageName {
           image = UIImage(named: imageName)
         } else {
           image = nil
         }
       }
     }
    }
  ,,,

  * custom disclosure buttom in annotation view class
  -----
  let mapsButton = UIButton(frame: CGRect(origin: CGPoint.zero,
   size: CGSize(width: 30, height: 30)))
mapsButton.setBackgroundImage(UIImage(named: "Maps-icon"), for: UIControlState())
rightCalloutAccessoryView = mapsButton
 ,,,

 * custom multiline callout label in annotation view class
 ------
  let detailLabel = UILabel()
  detailLabel.numberOfLines = 0
  detailLabel.font = detailLabel.font.withSize(12)
  detailLabel.text = artwork.subtitle
  detailCalloutAccessoryView = detailLabel
 ,,,

ANNOTATIONS ....

   Annotations represent displayable points on the map, but they are just 
   data. This is a protocol, so any object can be made to conform.

   * get all currently visible annotations from a mapview as objects
   -------
     // "mapView" is an IBOutlet connection
     let trees = 
       self.mapView.annotations(in: self.mapView.visibleMapRect).map{ $0 as! Tree}
   ,,,

   * get all visible annotations in a different tab of table controller
   -------
   override func viewDidLoad() {
     super.viewDidLoad()
     let map = self.tabBarController?.viewControllers![0] as! MapViewController
     let trees = 
       map.mapView.annotations(in: map.mapView.visibleMapRect).map{ $0 as! Tree}
     
   }
   ,,,

GOTCHAS

  If you get the "Cell Identifier" wrong in the nib editor in Xcode, your 
  app will crash and it wont be obvious why, unless you read this note.


HTTP

  * simplest way to get contents of url
  -----------
      if let url = URL(string: "https://example.com/file.txt") {
        do {
          let contents = try String(contentsOf: url)
          print(contents)
          words = contents.components(separatedBy: ",")
          } catch {
              // contents could not be loaded
          }
        } else {
            // the URL was bad!
        }
     }
   ,,,,

UPLOADING ....


  * How to prepare and upload json data to an http server using post.
  >> https://developer.apple.com/documentation/foundation/url_loading_system/uploading_data_to_a_website

JSON

  "json" which apparently stands for javascript object notation is used
  just about every where to get data into apps. Its not as bloated as
  xml and probably a bit easier to parse, especially considering that 
  everyone uses it, and there are lots of libraries out there. 

  Usually your app will get your ration of json from an api which
  is just a way of requesting not the entire dataset. 

  * read json file swift 5
  -------
    let path = Bundle.main.path(forResource: "filename", ofType: "json")
    let jsonData = 
       try? NSData(contentsOfFile: path!, options: NSData.ReadingOptions.mappedIfSafe)
  ,,,

  * read json data from a file in main folder
  ----------------
    if let path = Bundle.main.path(forResource: "file", ofType: "json") {
        do {
            let fileUrl = URL(fileURLWithPath: path)
            let data = try Data(contentsOf: fileUrl, options: .mappedIfSafe)
        } catch {
            // Handle error here
        }
    }
  ,,,,

  Use JSONSerialization may be superceded by Codable etc
  or SwiftyJson

  * read json data and serialize from a file in main folder
  ----------------
  static func readJSONFromFile(fileName: String) -> Any?
  {
      var json: Any?
      if let path = Bundle.main.path(forResource: fileName, ofType: "json") {
          do {
              let fileUrl = URL(fileURLWithPath: path)
              // Getting data from JSON file using the file URL
              let data = try Data(contentsOf: fileUrl, options: .mappedIfSafe)
              json = try? JSONSerialization.jsonObject(with: data)
          } catch {
              // Handle error here
          }
      }
      return json
  }
  ,,,,

JQ AND JSON ....

  The advantage of "jq" (Json Query) is that it allows manipulation of json
  data to be scripted from the bash shell or elsewhere.

  * a command line tool for json
  >> jq

  * download and format the summary of the Adansonia/Boabab tree.
  >> curl 'https://en.wikipedia.org/api/rest_v1/page/summary/Adansonia' | jq '.'

  * just get the first object from the json data (where toplevel is an array [...] )
  >> | jq '.[0]'

  * get all toplevel object and build new object with some sub-objects
  >> jq '.[] | {message: .commit.message, name: .commit.committer.name}'
    
  * just get the thumbnail image url from the Baobab page summary
  >> curl 'https://en.wikipedia.org/api/rest_v1/page/summary/Adansonia' | jq '. | {thumb: .thumbnail.source}'

WIKIPEDIA AND JSON ....

  Wikipedia provides a rest api giving json summaries about a page. 

  * example wikipedia rest api (about "Stack Overflow" site)
  >> https://en.wikipedia.org/api/rest_v1/page/summary/Stack_Overflow

  * api urls for json data
  ------
    "summary": "https://en.wikipedia.org/api/rest_v1/page/summary/Stack_Overflow",
    "metadata": "https://en.wikipedia.org/api/rest_v1/page/metadata/Stack_Overflow",
    "references": "https://en.wikipedia.org/api/rest_v1/page/references/Stack_Overflow",
    "media": "https://en.wikipedia.org/api/rest_v1/page/media/Stack_Overflow",
    "edit_html": "https://en.wikipedia.org/api/rest_v1/page/html/Stack_Overflow",
    "talk_page_html": "https://en.wikipedia.org/api/rest_v1/page/html/Talk:Stack_Overflow"
  ,,,,

CODABLE WITH JSON ....

   The Codable protocol (Encodable and Decodable) allows for the automatic
   "persistance" of swift object by encoding and decoding to storage. This 
   is an improvement on NSCodable although not a complete replacement.

   The tricky thing about codable is when the struct or class heirarchy does
   not match the json object heirarchy, but it is possible anyway.

   * a good article about codable and date formats and heirarchical data.
   >> https://www.hackingwithswift.com/articles/119/codable-cheat-sheet



  * A minimal Decode json or geojson string with a codable object
  -------------------
    struct Tree:Codable {
        var genus:String
        var species:String
        var latitude: Float
        var longitude: Float
    }
    ....

      let jsonString = """
   [
   { "genus": "Quercus", "species": "Robur", "latitude": 32.1, "longitude": 70.44 },
   { "genus": "Eucalyptus", "species": "Globulus", "variety": "", "latitude": 33.1, "longitude": 70.44
   }]
   """

  if let jsonData = jsonString.data(using: .utf8)
  {
      let decoder = JSONDecoder()
          do {
              self.trees = try decoder.decode([Tree].self, from: jsonData)
              print(self.trees[1].genus)
          } catch {
              print(error.localizedDescription)
          }
      }
  }

  ,,,,


HEIRARCHICAL WITH CODABLE AND JSON ....

  This is the trickiest case, here is a sketch of how to do it
  -------------
   let json = """
   [{
      "name": {
       "first_name": "Taylor",
       "last_name": "Swift"
      },
      "age": 26
    },
    ...more json object
    ]
   """

   struct User: Codable {
       var firstName: String
       var lastName: String
       var age: Int
       // these coding keys are "top-level" properties in json object!!
       enum CodingKeys: String, CodingKey {
         case name, age
       }
       // these are the "nested" properties
       enum NameCodingKeys: String, CodingKey {
         case firstName, lastName
       }
       init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         self.age = try container.decode(Int.self, forKey: .age)
         let name = try container.nestedContainer(
            keyedBy: NameCodingKeys.self, forKey: .name)
         self.firstName = try name.decode(String.self, forKey: .firstName)
i        self.lastName = try name.decode(String.self, forKey: .lastName)
       }
       func encode(to encoder: Encoder) throws {

         var container = encoder.container(keyedBy: CodingKeys.self)
         try container.encode(age, forKey: .age)
         var name = container.nestedContainer(
            keyedBy: NameCodingKeys.self, forKey: .name)
         try name.encode(firstName, forKey: .firstName)
         try name.encode(lastName, forKey: .lastName)
       }
   }
   ,,,,

ROOTVIEW

  The rootview and RootViewController are the top level 
  View and Controller for an application. All other views are 
  subviews or else are instantiated by them

  * Make ListViewController the rootviewcontroller for the app  
  -------------
  // In AppDelegate.swift --> func application(::didFinishLaunchingWithOptions)
    window = UIWindow()
    window?.makeKeyAndVisible()
    window?.rootViewController = ListViewController()
  ,,,

TABLES TABLEVIEWS AND TABLEVIEWCONTROLLERS

  * checklist for creating a table in xcode
  ------
    set the "cell identifier" in the interface builder == code CellId
    if you use detailLabel, then make sure that the type is set in interface builder
      (otherwise the program crashes)
 ,,,
 
* simplest possible table, 5 rows, same text on each row
  ----------------

  import UIKit
  class TreeTableController: UITableViewController {
      let cellId = "cellId"
      override func viewDidLoad() {
          super.viewDidLoad()
          setupTableView()
      }
      func setupTableView(){
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
      }

      //tableview delegate methods
      override func numberOfSections(in tableView: UITableView) -> Int {
          return 1
      }
      override func tableView(_ tableView: UITableView, 
          numberOfRowsInSection section: Int) -> Int {
          return 5
      }

    override func tableView(_ tableView: UITableView, 
        cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(
            withIdentifier: cellId, for: indexPath)
        cell.textLabel?.text = "Hello, World"
        return cell
    }
}

TABLES AND SEGUES ....

   * storyboard checklist...
   >> create the segue in the storyboard and give it identifier "Select Tree" eg.

   This will be a "show" segue either for the selection or for the 
   accessory (right button on the cell)

   * pass data from selected row to next view controller
   ---------
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
       if segue.identifier == "Select Tree" {
         if let indexPath = tableView.indexPathForSelectedRow {
           let selectedRow = indexPath.row
           let mv = segue.destination as! MapViewController
           mv.currentTree = selectedRow
         }
       }
     }
   ,,,,

SEARCH BARS AND SEARCH BAR CONTROLLERS

IMPLEMENTING IN CODE....

  * add a search controller to the containing controller as a property
  >> let searchController = UISearchController(searchResultsController: nil)
  
  * implement a search bar in table view using same table to show results
  --------
  class MyTableViewController: UTableViewController, UISearchResultsUpdating {

    var trees = [Tree]()
    var filteredTrees = [Tree]()

    let searchController = UISearchController(searchResultsController: nil)
    //...
    func isFiltering() -> Bool {
      return searchController.isActive && !searchBarIsEmpty()
    }

    override func viewDidLoad() {
      super.viewDidLoad()
      //...
      searchController.searchResultsUpdater = self
      searchController.hidesNavigationBarDuringPresentation = false
      searchController.obscuresBackgroundDuringPresentation = false
      searchController.searchBar.sizeToFit()
      self.tableView.tableHeaderView = searchController.searchBar
    }

    // protocol method for search bar
    func updateSearchResults(for searchController: UISearchController) {
      filteredTrees = self.trees.filter({(tree: Tree) -> Bool in
        return tree.genus.lowercased().contains(searchText.lowercased())
      })
      tableView.reloadData()
    }

    func tableView(_ tableView:UITableView, numberOfRowsInSection section: Int) -> Int 
    {
      if isFiltering() { return self.filteredTrees.count }
      return self.trees.count
    }

    func tableView(
      _ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
      let tree: Tree
      if isFiltering() { tree = filteredTrees[indexPath.row] } 
      else { tree = trees[indexPath.row] }
      cell.textLabel!.text = tree.genus 
      cell.detailTextLabel!.text = tree.commonName 
      return cell
    }

  }
  ,,,

  * another config
  -------------
    // Setup the Search Controller
    searchController.searchResultsUpdater = self
    searchController.obscuresBackgroundDuringPresentation = false
    searchController.searchBar.placeholder = "Search Candies"
    navigationItem.searchController = searchController
    definesPresentationContext = true
  ,,,

TAB BAR CONTROLLERS

  First create the new view controller, then drag a segue from the the 
  tab bar scene to the new view controller

  The seque for a tabbar to its view is "relationship"->"view controllers"
 
  * pass array of annotations (tree object) from one tab to another
  ------------
  // in "viewDidLoad"
     super.viewDidLoad()
     let map = self.tabBarController?.viewControllers![0] as! MapViewController
     self.trees = map.mapView.annotations as! [Tree]
  ,,,
  ,,,

  The code below uses "didSet" to make sure the detail view is updated when
  the item is changed.

  * a pattern to configure a detail view from a masterview (need "prepare" in master)
  -------------
  var detailCandy: Candy? {
    didSet { configureView() }
  }

  func configureView() {
    if let detailCandy = detailCandy {
      if let detailDescriptionLabel = detailDescriptionLabel, let candyImageView = candyImageView {
        detailDescriptionLabel.text = detailCandy.name
        candyImageView.image = UIImage(named: detailCandy.name)
        title = detailCandy.category
      }
    }
  }

  override func viewDidLoad() {
    super.viewDidLoad(); configureView()
  }

 ,,,

REFERENCING SIBLING TABS ....

  * reference a sibling when current tab is in a navigation controller 
  ---------
    let detailView = self.navigationController?.tabBarController?.viewControllers?[0] as! TreeDetailViewController
    self.tree = detailView.tree
  ,,,

NAVIGATION CONTROLLERS
NAVIGATION BAR 
 
  A Navigation bar appears at the top of the screen and does all sorts
  of useful thing automatically. It gets put there when you embed
  a ViewController in a UINavigationController. 

  * Set the title of the navigation bar
  -----
     // in viewWillAppear
     self.navigationController!.navigationBar.topItem?.title = "Statistics";
  ,,,

  * unset the title when the view disappears
  --------
  override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    self.navigationController!.navigationBar.topItem?.title = "";
  }
  ,,,

BREW


  Brew is a way of installing things on the command line in osx. Its not part
  of swift.

  Graphical programs use the "cask" prefix before install.

  * install the gimp image editor with brew 
  >> brew cask install gimp

  * install the wget net file getter with brew
  >> brew install wget

  * search for all programs (graphical and cli) with "image" in the name
  >> brew search image

  For some reason "desc" doesnt seem to work with "casks", gui programs

  * show one line description of a cli program
  >> brew desc imagemagick


DOCUMENTATION FORMAT

  By writing comments above classes, properties and methods, Xcode will automatically
  document those classes and display the help in "quick help". Swift uses the 
  "markdown" format for documentation.

  * make a multiline comment
  >> /**   ... */

  * single line
  >> ///