Making Meetup 6.0

Making Meetup 6.0

This month, Paul Bruneau and Matthew Bischoff will talk about "Making Meetup 6.0". They'll walk us through the architecture underlying the new Meetup iOS app, rewritten entirely in Swift, as well as how they employed view models, protocols, generics, unit tests, and other patterns to build an app that's fun to work on and easy to change.

23ad4e437c3ece1b7a04696321036c30?s=128

Matthew Bischoff

November 15, 2016
Tweet

Transcript

  1. 1.
  2. 5.
  3. 7.
  4. 10.
  5. 11.
  6. 15.
  7. 16.

    MacBookRetina:Meetup mb$ cloc . 1250 text files. 1202 unique files.

    339 files ignored. http://cloc.sourceforge.net v 1.64 T=4.36 s (208.9 files/s, 21111.0 lines/s) ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- Swift 771 20479 20124 48891 JSON 134 0 0 2194 Objective C 2 46 15 137 C/C++ Header 4 61 74 59 ------------------------------------------------------------------------------- SUM: 911 20586 20213 51281 -------------------------------------------------------------------------------
  8. 18.

    platform :ios, '9.0' source 'https://github.com/CocoaPods/Specs.git' use_frameworks! target 'Meetup' do pod

    'RealmSwift', '~> 1.1.0' pod 'Alamofire', '~> 3.5.0' pod 'SocketRocket', '~> 0.5.0' pod 'SwiftyJSON', :git => 'https://github.com/Navdy/SwiftyJSON.git', :branch => 'xcode8b6' pod 'BuddyBuildSDK', '~> 1.0.12' pod 'RMStore', '~> 0.7' pod 'Static', git: 'https://github.com/venmo/Static.git' pod 'CLTokenInputView', '~> 2.3.0' pod 'KeychainAccess', '~> 2.4.0' pod 'XLForm', '~> 3.1.1' pod '1PasswordExtension', '~> 1.8' pod 'FormatterKit/TimeIntervalFormatter', '~> 1.8' pod 'Nuke', '~> 3.2.0' pod 'KVOController', '~> 1.1.0' pod 'BRYHTMLParser', '~> 2.1.0' pod 'AcknowList', :git => 'https://github.com/vtourraine/AcknowList.git', :branch => 'swift-2-3' pod 'ReachabilitySwift', '~> 2.4.0' pod 'NSAttributedString+CCLFormat', '~> 1.2.0' pod 'NYTPhotoViewer/Core', '~> 1.1.0' pod 'RSKGrowingTextView', '~> 1.2.0' pod 'Google/SignIn', '~> 2.0.3' pod 'FBSDKLoginKit', '~> 4.16' pod 'UIColor_Hex_Swift', :git => 'https://github.com/yeahdongcn/UIColor-Hex-Swift.git', :branch => 'Swift-2.3' pod 'DeepLinkKit', '~> 1.2.1' target 'MeetupTests' do inherit! :search_paths pod 'OHHTTPStubs', '~> 5.0.0' end end
  9. 20.

    PATTERNS > Storyboards, size classes, stack views, auto layout >

    private and final by default > Document anything internal or public > Protocols define interfaces > Dependencies are injected > Logic is unit tested (1403 tests)
  10. 23.

    /// Model representing a location where Meetups may take place.

    final class Location: Object { // MARK: - Public API /// The name of the city. dynamic var name = "" /// The full name of the city, as returned by query search, if applicable. dynamic var fullName: String? // MARK: - LocationGenerating var city: String { return name } /// The state which contains the city, if applicable. dynamic var state = "" /// The ISO_3166-1 country code for the country which contains the city. dynamic var country = "" /// Localized name of country based on request's language information. dynamic var localizedCountryName: String? /// The latitude of the city. dynamic var latitude: Double = kCLLocationCoordinate2DInvalid.latitude /// The longitude of the city. dynamic var longitude: Double = kCLLocationCoordinate2DInvalid.longitude /// The zip code of the city. For cities in countries without ZIP codes, a placeholder will be returned. dynamic var zipCode = "" } extension Location: NSCoding { … }
  11. 25.

    /// An enum encapsulating types of location requests. enum LocationRequest:

    MeetupRequest { /// Request locations nearby a specified set of coordinates. case ByCoordinate(coordinate: CLLocationCoordinate2D, bearerToken: String?, pageSize: Int) /// Request locations corresponding to a queried city name or zip code. case ByQuery(query: String, bearerToken: String?) /// Request the authenticated or pre-registation user's current country. case Country(bearerToken: String?) // MARK: - MeetupRequest var path: String? { switch self { case .ByCoordinate, .ByQuery: return "find/locations" case .Country: return "members/self" } } var parameters: [String: AnyObject]? { switch self { case let ByCoordinate(coordinate, _, pageSize): return ["lat": coordinate.latitude, "lon": coordinate.longitude, "page": pageSize] case let ByQuery(query, _): return ["query": query] case .Country: return ["only": "country"] } } var URLRequest: NSURLRequest { if let bearerToken = bearerToken { return defaultRequestWithPreRegistrationAuthorizationHeader(bearerToken: bearerToken) } else { return defaultURLRequest() } } }
  12. 27.

    /// Downloads objects related to nearby location requests. final class

    LocationUpdater { typealias Completion = (result: Result<[Location]>) -> Void /// The bearer token used to create authenticated requests. private let bearerToken: String? init(bearerToken: String? = nil) { self.bearerToken = bearerToken } func fetchLocationsNearby(coordinate: CLLocationCoordinate2D, pageSize: Int = 10, completion: Completion) { let request = LocationRequest.ByCoordinate(coordinate: coordinate, bearerToken: bearerToken, pageSize: pageSize) performLocationRequest(request, completion: completion) } func fetchLocationsMatchingQuery(query: String, completion: Completion) { let request = LocationRequest.ByQuery(query: query, bearerToken: bearerToken) performLocationRequest(request, completion: completion) } func fetchCountry(completion: (result: Result<Location>) -> Void) { let request = LocationRequest.Country(bearerToken: bearerToken) NetworkController.request(request).responseObject { (request, response, result: Result<Location>) in completion(result: result) } } private func performLocationRequest(locationRequest: LocationRequest, completion: Completion) { NetworkController.request(locationRequest).responseObject { (_, _, result: Result<[Location]>) in completion(result: result) } } }
  13. 29.

    /// Parses a `Location` object from a JSON dictionary, or

    `nil` if no identifier is found. struct LocationParser: Parser { static func parse(dictionaryRepresentation: [String : AnyObject]) -> Location? { let JSONObject = JSON(dictionaryRepresentation) let location = Location() location.name = JSONObject["city"].stringValue location.fullName = JSONObject["name_string"].string location.localizedCountryName = JSONObject["localized_country_name"].string location.state = JSONObject["state"].stringValue location.country = JSONObject["country"].stringValue location.zipCode = JSONObject["zip"].stringValue if let latitude = JSONObject["lat"].double, longitude = JSONObject["lon"].double { location.latitude = latitude location.longitude = longitude } return location } } extension Location: Parseable { typealias ParserType = LocationParser }
  14. 31.

    /// The view model containing all view-related data necessary to

    lay out an `LocationCell`. struct ViewModel { /// The configuration style for the cell based on a specified location status. enum LocationStatus { /// Cell configuration for an unknown location. case Unknown case Known(location: Location?, style: DisplayStyle) private var title: String { switch self { case .Unknown: return NSLocalizedString("Current location", comment: "…") case let .Known(_, style): return style.title } } private var image: UIImage { switch self { case .Unknown: return UIImage(named: "LocationServices")! case let .Known(_, style): return style.image } } } /// The location status for which to configure the cell. let locationStatus: LocationStatus }
  15. 33.

    /// A data source object that acts as the table

    view data source for nearby locations, or optionally a loading interface. final class LocationDataSource : NSObject, UITableViewDataSource { /// An enum representing the table section. Do not initialize directly. Instead, call `sectionForSectionIndex`. enum Section { /// The section when a loading indicator is displayed. case Loading /// The section containing a cell displaying the user's current device location. case CurrentLocation(rows: [Meetup.LocationDataSource.CurrentLocationRow]) /// The section containing locations nearby the user's current location. case NearbyLocations(rows: [Meetup.LocationDataSource.LocationRow]) /// The number of rows in the section. var numberOfRows: Int { get } } typealias CellConfiguration = (tableView: UITableView, indexPath: NSIndexPath, section: Section, authorizationStatus: CLAuthorizationStatus?) -> UITableViewCell init(loading: Bool, cellConfiguration: CellConfiguration) func sectionForSectionIndex(index: Int) -> Meetup.LocationDataSource.Section? }
  16. 35.

    /// Sets up the table view and creates configured cells

    for location editing. final class LocationCellCreator { init(tableView: UITableView) { tableView.registerNibForClass(LoadingCell.self, reuseIdentifier: self.dynamicType.LoadingCellIdentifier) … } func cell(tableView: UITableView, indexPath: NSIndexPath, section: LocationDataSource.Section, authorizationStatus: CLAuthorizationStatus?) -> UITableViewCell { let cell: UITableViewCell? switch section { case .Loading: cell = loadingCell(tableView: tableView, indexPath: indexPath) } return cell } } // MARK: - Private methods private func currentLocationCell(tableView tableView: UITableView, indexPath: NSIndexPath, authorizationStatus: CLAuthorizationStatus?, location: Location?, isNearOtherLocations: Bool, isOnboarding: Bool) -> UITableViewCell { … } private func onboardingLocationCell(tableView tableView: UITableView, indexPath: NSIndexPath) -> UITableViewCell { … } }
  17. 36.

    WHAT WE LEARNED > The best way to learn Swift

    is to write a lot of it > Lack of refactoring makes architecture more important > Doing all-swift removed inter-op challenges > Swift 2.2 -> 2.3 migration went smoothly > Compile times are still terrible