== 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 >> ///