Randy Scovil
Cuesta College/Cal Poly
Yes We Do Apps, Inc.
Right Up Front
ž I am not here to advocate for Swift.
ž I am not here to advocate against Swift.
ž I use both Objective-C and Swift.
ž I teach both Objective-C and Swift.
@randyscovil / SV-iOS 22/24/18
Understood Up Front
ž This is a brief introduction to Swift, with
an eye to the key differences from
Objective-C.
ž You are pretty familiar with Objective-C,
and haven’t had the time/inclination to
investigate Swift.
ž We may not agree on everything.
@randyscovil / SV-iOS 32/24/18
Brief History of Teaching iOS
ž Jan. 2010: First iOS Course
ž Jan. 2013: First iOS II Course
— (1st such course in CA)
ž Jan. 2015: Swift taught in iOS II
— Objective-C in iOS I
ž Aug. 2015: Swift taught in iOS I
ž Sep. 2016: 400-level iOS @ Cal Poly
— Currently: Start with Swift, then ObjC
@randyscovil / SV-iOS 42/24/18
Observations of iOS Courses
ž Community College:
— Wide range of backgrounds, sizeable contingent
has 0 coding background
— Got the core of Objective-C within a semester
ž Cal Poly:
— Course has System Prog (C) prereq
— After Swift & C, pick up Objective-C quickly
ž The easy parts of Swift appear to be easier
to learn than Objective-C
ž The harder parts of Swift seem harder
@randyscovil / SV-iOS 52/24/18
Data Types
ž Most things are value types
— Classes and closures are reference types
ž Value types include:
— Strings
— Collections (Array, Set, Dictionary)
○ Use copy-on-write
— Enums
— Tuples
@randyscovil / SV-iOS 62/24/18
Implicit All-Seeing Superclass
ž In Swift, there isn’t one
— Must explicitly subclass NSObject if needed
ž Need to opt in to key protocols
— CustomStringConvertible
○ description
— Equatable (required for next two)
○ ==
— Hashable
○ hashValue
— Comparable
○ <
@randyscovil / SV-iOS 72/24/18
Declarations
ž No explicitly (im)mutable types
ž Declare with let for constant, var for
variable (must use var, not implicit)
ž Can explicitly set type with a type
annotation, or set via type inference
based on initial value
// Type annotation
let theString : String
// Inferred as String
var someString = “Hey now”
@randyscovil / SV-iOS 82/24/18
Strong Typing w/Inference
ž The inferred type becomes the compile-
time type of the variable – not dynamic a
la Python:
// Inferred as String
var someString = “Hey now”
// Compile error
someString = 42
2/24/18 @randyscovil / SV-iOS 9
Optionals
ž One of the bigger adjustments to make
ž Any type (value or reference) can be
declared as an optional type
— Means it may have nil, or a value
— Declare with a ? after the type (e.g. Int?)
ž Frees you from initialization obligations
ž Actually syntactic sugar for the
Optional generic type
@randyscovil / SV-iOS 102/24/18
Optional Binding
ž Execute only if optional is not nil
// instead of this
if possibleNil != nil {
doSomethingWith(possibleNil!)
}
// you tend to do this
// localValue is assigned value
if let localValue = possibleNil {
doSomethingWith(localValue)
}
@randyscovil / SV-iOS 112/24/18
Nil-coalescing Operator
ž Provides default value if optional is nil
let defaultColorName = "red"
var userDefinedColorName: String?
var colorNameToUse =
userDefinedColorName ??
defaultColorName
2/24/18 @randyscovil / SV-iOS 12
Optional Chaining
ž When you need to access a property of
an optional
— In lieu of forced unwrapping
— ? After optional to access
// address is optional property
// true if address non-nil
if let zip = joe.address?.zip {
print(“Joe has address, zip”)
}
@randyscovil / SV-iOS 132/24/18
Unwrapped Optionals
ž If an optional is assumed to always have
a value, can force unwrap it using the !
operator
— Unwrapping a nil is a runtime error
cell.textLabel!.text = someString
ž If declared as implicitly unwrapped will
get value directly – don’t need !
func guh(unwrapMe : String!) {
let implicit : String = unwrapMe
@randyscovil / SV-iOS 142/24/18
Range Operators
ž Half-open range ..< // up to end
for i in 0..<array.count {
doFunStuff(array[i])
}
ž Closed range . . . // includes end
for i in 1 . . . howManyTimes {
doStuff()
}
@randyscovil / SV-iOS 152/24/18
Strings
ž Pretty straightforward
ž Immutable/mutable based on let/var
ž Get contents of value as String with ()
around variable
— Type must conform to
CustomStringConvertible protocol
let bob = 23
someLabel.text = “Value: (bob)”
@randyscovil / SV-iOS 162/24/18
Control Flow
ž No () around conditions, {} required
ž Usual conditionals/loops, no for(;;)
ž repeat/while in lieu of do/while
ž switch is a different animal
— Checks to see if exhaustive
— Implicit break, explicit fallthrough
— Range and Tuple matching
— where clause – can match with a condition:
case let (x, y) where x == y :
@randyscovil / SV-iOS 172/24/18
guard Statements
ž Concise way to send code packing if a
condition(s) is not met
— Can do multiple clauses separated by ,
ž Assigned value not local to guard clause
/* assign value of key “name” from
person return if key not present
*/
guard let name = person[“name”] else {
return
}
@randyscovil / SV-iOS 182/24/18
Functions
ž Do not have to belong to anything – can
be global
ž Functions (methods) can be members of
enums and structs
ž Functions can be variables, parameters
and return values
ž Can be nested inside other functions
ž Overridden versions require an explicit
override keyword
@randyscovil / SV-iOS 192/24/18
Function Headers
ž All functions start with the func keyword
ž Return value is last after -> operator
— If no ->, implicitly void
func timesTwo(a : Int) - > Int {
return a * 2
}
func noReturn(a : Int) {
print(a)
}
@randyscovil / SV-iOS 202/24/18
Parameter Passing
ž Can have separate external/internal
parameter names, or _ to waive external
func someFunc(_ firstParam: Int) {
ž Parameters must be explicitly annotated
to mutate them locally
— Declare them as var parameters
ž Allow mutating beyond scope of function
by prepending & on the argument and
listing parameter as inout
@randyscovil / SV-iOS 212/24/18
Closures
ž Unnamed functions - think blocks
ž Use {} and in , ^ not used
ž sort(by:) and, sorted(by:) take a
closure argument with two parameters
of array type, returning Bool
reversed = names.sort( {
(s1: String, s2: String) -> Bool in
return s1 > s2
})
@randyscovil / SV-iOS 222/24/18
Trailing Closures
ž If closure is the last argument, you can
pass it outside the closing paren of the
method call
— if only argument can lose the ()
reversed = names.sort {
(s1: String, s2: String) -> Bool in
return s1 > s2
}
ž This can be reduced down to:
{ $0 > $1 }
@randyscovil / SV-iOS 232/24/18
Classes, Structs, Enums
ž Classes are reference types
ž Structs are value types and can have
defined initializers (in addition to a
compiler-generated memberwise initializer)
ž Structs very similar to classes, use when:
— Type is a few relatively simple values
— Want to pass a copy rather than a reference
— Properties are value types
— Doesn’t need to inherit from another type
@randyscovil / SV-iOS 242/24/18
How are Classes Distinct?
ž Allow inheritance
— Define superclass and protocols conformed
to with commas after : following name:
class Student : Person, Hashable
ž Can check runtime type via type casting
— is operator
ž Have deinitializers
— deinit()
ž Use reference counting
@randyscovil / SV-iOS 252/24/18
Enumerations
ž Can have methods
ž Once type is established (through local
declaration, parameter, or expected
argument type) can be referenced
without the actual type but just with .
and the enum value
// Infers .red from UIColor
let goofyLabel.textColor = .red
@randyscovil / SV-iOS 262/24/18
Properties
ž Declared as variables outside methods
ž lazy properties
— Not initialized until used
ž Computed properties
— Declared as properties that can define get/set
operations to compute on demand
ž Property Observers
— Can define willSet and didSet code to be
triggered pre/post-assignment
— New and old values are available
@randyscovil / SV-iOS 272/24/18
Initialization
ž Every property must do one of:
— Have a default value
— Be initialized in all init()s
— Be declared as an optional
ž Specific init rules when subclassing
— Designated, convenience and required
— Two-phase initialization
ž Failable Initializers - init?()
— Can return nil
@randyscovil / SV-iOS 282/24/18
Error Handling
ž Can define throws in header
— Do not have to specify error type thrown
ž Method call preceded by try inside a do
do {
try iAmErrorProne(“Guh”)
}
catch {
}
@randyscovil / SV-iOS 292/24/18
Optional try, defer
ž Assigns nil if error thrown
// x is returned value or nil
let x =
try? someThrowingFunction()
ž defer is also supported
let file = open(filename)
defer {
close(file)
}
2/24/18 @randyscovil / SV-iOS 30
Type Casting – Runtime Types
ž Check for runtime type with is
// Movie is subclass of MediaItem
let library = [MediaItem]()
for item in library {
if item is Movie {
print(“Movie!”)
}
}
@randyscovil / SV-iOS 312/24/18
Downcasting – as?, as!
ž Conditional type cast – only do
conditional block if cast succeeds
— (also checks nil)
if let movie = item as? Movie {
print(“Movie!”)
}
ž Forced cast – when type is assured
let destVC =
segue.destinationViewController
as! EditItemViewController
@randyscovil / SV-iOS 322/24/18
Extensions
ž Think Categories
ž Define as extension, then type being
extended:
extension SomeType {
ž May include adding conformance to a
protocol:
extension SomeType: SomeProtocol {
@randyscovil / SV-iOS 332/24/18
Protocols
ž Structs can adopt
ž Can require properties
ž Can require specific initializers
ž Optional requirements must be explicitly
labeled optional
ž Can use conditional conformance that
only applies to certain types:
extension Array: TextRepresentable
where Element: TextRepresentable {
@randyscovil / SV-iOS 342/24/18
Generics
ž Very similar to what you may have
encountered in Java
ž Optionals and Collections are generic
ž Parameterized type in <>
func swapTwoValues<T>
(inout a: T, inout _ b: T) {
let tempA = a
a = b
b = tempA
}
@randyscovil / SV-iOS 352/24/18
The Codable Protocol
ž Inherently supports common built-in
types (String, Int, [], etc.)
ž Declare conformance in type declaration
ž If property name matches the JSON key,
it’s automagically assigned
ž If all of your property types are
Codable…that’s IT, including Arrays
ž Conversion enacted by
JSONDecoder() and JSONEncoder()
Simple Codable Example
// Properties Codable, match JSON keys
struct SCWAttendee : Codable {
var firstName: String
var lastName: String
var affiliation: String
}
// Using data from URLSessionDataTask
let decoder = JSONDecoder()
let attendeesArray = try decoder.decode
([SCWAttendee.self], from: data)
2/24/18 @randyscovil / SV-iOS 37
Codable With CodingKeys
// property names don’t match JSON
struct SCWAttendee : Codable {
var firstName: String
var lastName: String
var affiliation: String
private enum CodingKeys:
String, CodingKey {
case firstName
case lastName
case affiliation = ”company"
}
}
2/24/18 @randyscovil / SV-iOS 38
If Property Isn’t Codable?
ž Define a nested type for this property
— If all of its properties are Codable, done
— Else continue to break down into nested
types until you have a nested structure that
results in all data being Codable
ž The problem with dictionaries:
— Can’t guarantee value for key is Codable
— Solution: Create a property that is a nested
type that conforms to Codable, and select
only the elements of the dictionary you want
Nested Codable Types
// Map nested dict to nested Codable type
struct SCWAttendee : Codable {
var firstName : String
var lastName : String
var affiliation: Affilliation
struct Affilliation : Codable {
var company : String
var title : String
var years : Int
}
}
2/24/18 @randyscovil / SV-iOS 40
Bonus: Data Persistence
ž Supports persisting custom types as is
let data = try
Data(contentsOf: fileURL)
let decoder = JSONDecoder()
let tempArr = try decoder.decode
([SCWAttendee].self, from: data)
let jsonData = try
encoder.encode(attendees)
try jsonData.write(to: fileURL)
Swift Resources
ž The Swift Programming Language
— At Apple Developer site and via iBooks
— Pretty good read, not set up for TL;DR
ž Swift Playgrounds in Xcode
— Great for testing out ideas in code
— Interactively displays results
— Support graphics
— Can embed Markdown
— Not an app component
@randyscovil / SV-iOS 422/24/18
Thanks!
ž Contact me anytime:
— rscovil@calpoly.edu
— rscovil@cuesta.edu
— randy@yeswedoapps.com
— @randyscovil
@randyscovil / SV-iOS 432/24/18

Objective-C to Swift - Swift Cloud Workshop 3

  • 1.
    Randy Scovil Cuesta College/CalPoly Yes We Do Apps, Inc.
  • 2.
    Right Up Front žI am not here to advocate for Swift. ž I am not here to advocate against Swift. ž I use both Objective-C and Swift. ž I teach both Objective-C and Swift. @randyscovil / SV-iOS 22/24/18
  • 3.
    Understood Up Front žThis is a brief introduction to Swift, with an eye to the key differences from Objective-C. ž You are pretty familiar with Objective-C, and haven’t had the time/inclination to investigate Swift. ž We may not agree on everything. @randyscovil / SV-iOS 32/24/18
  • 4.
    Brief History ofTeaching iOS ž Jan. 2010: First iOS Course ž Jan. 2013: First iOS II Course — (1st such course in CA) ž Jan. 2015: Swift taught in iOS II — Objective-C in iOS I ž Aug. 2015: Swift taught in iOS I ž Sep. 2016: 400-level iOS @ Cal Poly — Currently: Start with Swift, then ObjC @randyscovil / SV-iOS 42/24/18
  • 5.
    Observations of iOSCourses ž Community College: — Wide range of backgrounds, sizeable contingent has 0 coding background — Got the core of Objective-C within a semester ž Cal Poly: — Course has System Prog (C) prereq — After Swift & C, pick up Objective-C quickly ž The easy parts of Swift appear to be easier to learn than Objective-C ž The harder parts of Swift seem harder @randyscovil / SV-iOS 52/24/18
  • 6.
    Data Types ž Mostthings are value types — Classes and closures are reference types ž Value types include: — Strings — Collections (Array, Set, Dictionary) ○ Use copy-on-write — Enums — Tuples @randyscovil / SV-iOS 62/24/18
  • 7.
    Implicit All-Seeing Superclass žIn Swift, there isn’t one — Must explicitly subclass NSObject if needed ž Need to opt in to key protocols — CustomStringConvertible ○ description — Equatable (required for next two) ○ == — Hashable ○ hashValue — Comparable ○ < @randyscovil / SV-iOS 72/24/18
  • 8.
    Declarations ž No explicitly(im)mutable types ž Declare with let for constant, var for variable (must use var, not implicit) ž Can explicitly set type with a type annotation, or set via type inference based on initial value // Type annotation let theString : String // Inferred as String var someString = “Hey now” @randyscovil / SV-iOS 82/24/18
  • 9.
    Strong Typing w/Inference žThe inferred type becomes the compile- time type of the variable – not dynamic a la Python: // Inferred as String var someString = “Hey now” // Compile error someString = 42 2/24/18 @randyscovil / SV-iOS 9
  • 10.
    Optionals ž One ofthe bigger adjustments to make ž Any type (value or reference) can be declared as an optional type — Means it may have nil, or a value — Declare with a ? after the type (e.g. Int?) ž Frees you from initialization obligations ž Actually syntactic sugar for the Optional generic type @randyscovil / SV-iOS 102/24/18
  • 11.
    Optional Binding ž Executeonly if optional is not nil // instead of this if possibleNil != nil { doSomethingWith(possibleNil!) } // you tend to do this // localValue is assigned value if let localValue = possibleNil { doSomethingWith(localValue) } @randyscovil / SV-iOS 112/24/18
  • 12.
    Nil-coalescing Operator ž Providesdefault value if optional is nil let defaultColorName = "red" var userDefinedColorName: String? var colorNameToUse = userDefinedColorName ?? defaultColorName 2/24/18 @randyscovil / SV-iOS 12
  • 13.
    Optional Chaining ž Whenyou need to access a property of an optional — In lieu of forced unwrapping — ? After optional to access // address is optional property // true if address non-nil if let zip = joe.address?.zip { print(“Joe has address, zip”) } @randyscovil / SV-iOS 132/24/18
  • 14.
    Unwrapped Optionals ž Ifan optional is assumed to always have a value, can force unwrap it using the ! operator — Unwrapping a nil is a runtime error cell.textLabel!.text = someString ž If declared as implicitly unwrapped will get value directly – don’t need ! func guh(unwrapMe : String!) { let implicit : String = unwrapMe @randyscovil / SV-iOS 142/24/18
  • 15.
    Range Operators ž Half-openrange ..< // up to end for i in 0..<array.count { doFunStuff(array[i]) } ž Closed range . . . // includes end for i in 1 . . . howManyTimes { doStuff() } @randyscovil / SV-iOS 152/24/18
  • 16.
    Strings ž Pretty straightforward žImmutable/mutable based on let/var ž Get contents of value as String with () around variable — Type must conform to CustomStringConvertible protocol let bob = 23 someLabel.text = “Value: (bob)” @randyscovil / SV-iOS 162/24/18
  • 17.
    Control Flow ž No() around conditions, {} required ž Usual conditionals/loops, no for(;;) ž repeat/while in lieu of do/while ž switch is a different animal — Checks to see if exhaustive — Implicit break, explicit fallthrough — Range and Tuple matching — where clause – can match with a condition: case let (x, y) where x == y : @randyscovil / SV-iOS 172/24/18
  • 18.
    guard Statements ž Conciseway to send code packing if a condition(s) is not met — Can do multiple clauses separated by , ž Assigned value not local to guard clause /* assign value of key “name” from person return if key not present */ guard let name = person[“name”] else { return } @randyscovil / SV-iOS 182/24/18
  • 19.
    Functions ž Do nothave to belong to anything – can be global ž Functions (methods) can be members of enums and structs ž Functions can be variables, parameters and return values ž Can be nested inside other functions ž Overridden versions require an explicit override keyword @randyscovil / SV-iOS 192/24/18
  • 20.
    Function Headers ž Allfunctions start with the func keyword ž Return value is last after -> operator — If no ->, implicitly void func timesTwo(a : Int) - > Int { return a * 2 } func noReturn(a : Int) { print(a) } @randyscovil / SV-iOS 202/24/18
  • 21.
    Parameter Passing ž Canhave separate external/internal parameter names, or _ to waive external func someFunc(_ firstParam: Int) { ž Parameters must be explicitly annotated to mutate them locally — Declare them as var parameters ž Allow mutating beyond scope of function by prepending & on the argument and listing parameter as inout @randyscovil / SV-iOS 212/24/18
  • 22.
    Closures ž Unnamed functions- think blocks ž Use {} and in , ^ not used ž sort(by:) and, sorted(by:) take a closure argument with two parameters of array type, returning Bool reversed = names.sort( { (s1: String, s2: String) -> Bool in return s1 > s2 }) @randyscovil / SV-iOS 222/24/18
  • 23.
    Trailing Closures ž Ifclosure is the last argument, you can pass it outside the closing paren of the method call — if only argument can lose the () reversed = names.sort { (s1: String, s2: String) -> Bool in return s1 > s2 } ž This can be reduced down to: { $0 > $1 } @randyscovil / SV-iOS 232/24/18
  • 24.
    Classes, Structs, Enums žClasses are reference types ž Structs are value types and can have defined initializers (in addition to a compiler-generated memberwise initializer) ž Structs very similar to classes, use when: — Type is a few relatively simple values — Want to pass a copy rather than a reference — Properties are value types — Doesn’t need to inherit from another type @randyscovil / SV-iOS 242/24/18
  • 25.
    How are ClassesDistinct? ž Allow inheritance — Define superclass and protocols conformed to with commas after : following name: class Student : Person, Hashable ž Can check runtime type via type casting — is operator ž Have deinitializers — deinit() ž Use reference counting @randyscovil / SV-iOS 252/24/18
  • 26.
    Enumerations ž Can havemethods ž Once type is established (through local declaration, parameter, or expected argument type) can be referenced without the actual type but just with . and the enum value // Infers .red from UIColor let goofyLabel.textColor = .red @randyscovil / SV-iOS 262/24/18
  • 27.
    Properties ž Declared asvariables outside methods ž lazy properties — Not initialized until used ž Computed properties — Declared as properties that can define get/set operations to compute on demand ž Property Observers — Can define willSet and didSet code to be triggered pre/post-assignment — New and old values are available @randyscovil / SV-iOS 272/24/18
  • 28.
    Initialization ž Every propertymust do one of: — Have a default value — Be initialized in all init()s — Be declared as an optional ž Specific init rules when subclassing — Designated, convenience and required — Two-phase initialization ž Failable Initializers - init?() — Can return nil @randyscovil / SV-iOS 282/24/18
  • 29.
    Error Handling ž Candefine throws in header — Do not have to specify error type thrown ž Method call preceded by try inside a do do { try iAmErrorProne(“Guh”) } catch { } @randyscovil / SV-iOS 292/24/18
  • 30.
    Optional try, defer žAssigns nil if error thrown // x is returned value or nil let x = try? someThrowingFunction() ž defer is also supported let file = open(filename) defer { close(file) } 2/24/18 @randyscovil / SV-iOS 30
  • 31.
    Type Casting –Runtime Types ž Check for runtime type with is // Movie is subclass of MediaItem let library = [MediaItem]() for item in library { if item is Movie { print(“Movie!”) } } @randyscovil / SV-iOS 312/24/18
  • 32.
    Downcasting – as?,as! ž Conditional type cast – only do conditional block if cast succeeds — (also checks nil) if let movie = item as? Movie { print(“Movie!”) } ž Forced cast – when type is assured let destVC = segue.destinationViewController as! EditItemViewController @randyscovil / SV-iOS 322/24/18
  • 33.
    Extensions ž Think Categories žDefine as extension, then type being extended: extension SomeType { ž May include adding conformance to a protocol: extension SomeType: SomeProtocol { @randyscovil / SV-iOS 332/24/18
  • 34.
    Protocols ž Structs canadopt ž Can require properties ž Can require specific initializers ž Optional requirements must be explicitly labeled optional ž Can use conditional conformance that only applies to certain types: extension Array: TextRepresentable where Element: TextRepresentable { @randyscovil / SV-iOS 342/24/18
  • 35.
    Generics ž Very similarto what you may have encountered in Java ž Optionals and Collections are generic ž Parameterized type in <> func swapTwoValues<T> (inout a: T, inout _ b: T) { let tempA = a a = b b = tempA } @randyscovil / SV-iOS 352/24/18
  • 36.
    The Codable Protocol žInherently supports common built-in types (String, Int, [], etc.) ž Declare conformance in type declaration ž If property name matches the JSON key, it’s automagically assigned ž If all of your property types are Codable…that’s IT, including Arrays ž Conversion enacted by JSONDecoder() and JSONEncoder()
  • 37.
    Simple Codable Example //Properties Codable, match JSON keys struct SCWAttendee : Codable { var firstName: String var lastName: String var affiliation: String } // Using data from URLSessionDataTask let decoder = JSONDecoder() let attendeesArray = try decoder.decode ([SCWAttendee.self], from: data) 2/24/18 @randyscovil / SV-iOS 37
  • 38.
    Codable With CodingKeys //property names don’t match JSON struct SCWAttendee : Codable { var firstName: String var lastName: String var affiliation: String private enum CodingKeys: String, CodingKey { case firstName case lastName case affiliation = ”company" } } 2/24/18 @randyscovil / SV-iOS 38
  • 39.
    If Property Isn’tCodable? ž Define a nested type for this property — If all of its properties are Codable, done — Else continue to break down into nested types until you have a nested structure that results in all data being Codable ž The problem with dictionaries: — Can’t guarantee value for key is Codable — Solution: Create a property that is a nested type that conforms to Codable, and select only the elements of the dictionary you want
  • 40.
    Nested Codable Types //Map nested dict to nested Codable type struct SCWAttendee : Codable { var firstName : String var lastName : String var affiliation: Affilliation struct Affilliation : Codable { var company : String var title : String var years : Int } } 2/24/18 @randyscovil / SV-iOS 40
  • 41.
    Bonus: Data Persistence žSupports persisting custom types as is let data = try Data(contentsOf: fileURL) let decoder = JSONDecoder() let tempArr = try decoder.decode ([SCWAttendee].self, from: data) let jsonData = try encoder.encode(attendees) try jsonData.write(to: fileURL)
  • 42.
    Swift Resources ž TheSwift Programming Language — At Apple Developer site and via iBooks — Pretty good read, not set up for TL;DR ž Swift Playgrounds in Xcode — Great for testing out ideas in code — Interactively displays results — Support graphics — Can embed Markdown — Not an app component @randyscovil / SV-iOS 422/24/18
  • 43.
    Thanks! ž Contact meanytime: — rscovil@calpoly.edu — rscovil@cuesta.edu — randy@yeswedoapps.com — @randyscovil @randyscovil / SV-iOS 432/24/18