├── .github └── workflows │ └── dynamic-security.yml ├── .gitignore ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── SECURITY.md ├── bin ├── deploy └── setup ├── book ├── book.md ├── iOS │ ├── a_new_xcode_project.md │ ├── add_an_event_view_controller.md │ ├── api_client.md │ ├── app_skeleton.md │ ├── cocoapods.md │ ├── displaying_events.md │ ├── event_object.md │ ├── finishing_touches.md │ ├── getting_an_event.md │ ├── introduction.md │ ├── making_the_post_event_request.md │ ├── making_the_post_user_request.md │ ├── map_view_controller.md │ ├── posting_a_user.md │ ├── posting_an_event.md │ ├── posting_with_the_event_view_controller.md │ ├── user_object.md │ └── viewing_with_the_event_view_controller.md ├── images │ ├── cover.pdf │ ├── humon-database-representation.png │ ├── ios_app_skeleton_1.png │ ├── ios_app_skeleton_2.png │ ├── ios_app_skeleton_3.png │ ├── ios_event_object_1.png │ ├── ios_new_xcode_project_1.png │ ├── ios_new_xcode_project_2.png │ └── ios_new_xcode_project_3.png ├── introduction │ └── introduction.md ├── rails │ ├── creating_a_geocoded_get_request.md │ ├── creating_a_get_request.md │ ├── creating_a_patch_request.md │ ├── creating_a_post_request.md │ └── introduction.md └── sample.md ├── example_apps ├── .gitkeep ├── iOS │ ├── Humon.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcuserdata │ │ │ └── apprentice.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ ├── Humon.xcworkspace │ │ └── contents.xcworkspacedata │ ├── Humon │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ └── HUMIcon@2x.png │ │ │ ├── Contents.json │ │ │ ├── HUMButtonQuestion.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── HUMButtonQuestion@2x.png │ │ │ ├── HUMChevronBack.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── HUMChevronBack@2x.png │ │ │ ├── HUMChevronForward.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── HUMChevronForward@2x.png │ │ │ ├── HUMPlacemarkLarge.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── HUMPlacemarkLarge@2x.png │ │ │ ├── HUMPlacemarkLargeTalking.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── HUMPlacemarkLargeTalking@2x.png │ │ │ ├── HUMPlacemarkSmall.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── HUMPlacemarkSmall@2x.png │ │ │ ├── HUMPlacemarkSmallGrey.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── HUMPlacemarkSmallGrey@2x.png │ │ │ ├── HUMPlacemarkSmallGreyTalking.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── HUMPlacemarkSmallGreyTalking@2x.png │ │ │ ├── HUMPlacemarkSmallTalking.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── HUMPlacemarkSmallTalking@2x.png │ │ │ └── HUMSuccessImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── HUMSuccessImage.png │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── Categories │ │ │ ├── NSDateFormatter+HUMDefaultDateFormatter.h │ │ │ ├── NSDateFormatter+HUMDefaultDateFormatter.m │ │ │ ├── NSObject+HUMNullCheck.h │ │ │ ├── NSObject+HUMNullCheck.m │ │ │ ├── UIImage+HUMColorImage.h │ │ │ └── UIImage+HUMColorImage.m │ │ ├── Cells │ │ │ ├── HUMNameCell.h │ │ │ ├── HUMNameCell.m │ │ │ ├── HUMTextFieldCell.h │ │ │ ├── HUMTextFieldCell.m │ │ │ ├── HUMTimeCell.h │ │ │ ├── HUMTimeCell.m │ │ │ ├── HUMTimePickerCell.h │ │ │ └── HUMTimePickerCell.m │ │ ├── Clients │ │ │ ├── HUMRailsAFNClient.h │ │ │ ├── HUMRailsAFNClient.m │ │ │ ├── HUMRailsClient.h │ │ │ └── HUMRailsClient.m │ │ ├── Controllers │ │ │ ├── HUMAddEventViewController.h │ │ │ ├── HUMAddEventViewController.m │ │ │ ├── HUMConfirmationViewController.h │ │ │ ├── HUMConfirmationViewController.m │ │ │ ├── HUMEditEventViewController.h │ │ │ ├── HUMEditEventViewController.m │ │ │ ├── HUMEventViewController.h │ │ │ ├── HUMEventViewController.m │ │ │ ├── HUMEventViewControllerFromBook.h │ │ │ ├── HUMEventViewControllerFromBook.m │ │ │ ├── HUMLocateEventViewController.h │ │ │ ├── HUMLocateEventViewController.m │ │ │ ├── HUMMapViewController.h │ │ │ ├── HUMMapViewController.m │ │ │ ├── HUMViewEventViewController.h │ │ │ └── HUMViewEventViewController.m │ │ ├── Info.plist │ │ ├── Models │ │ │ ├── HUMAppearanceManager.h │ │ │ ├── HUMAppearanceManager.m │ │ │ ├── HUMEvent.h │ │ │ ├── HUMEvent.m │ │ │ ├── HUMUser.h │ │ │ ├── HUMUser.m │ │ │ ├── HUMUserSession.h │ │ │ └── HUMUserSession.m │ │ ├── Views │ │ │ ├── HUMAnnotationView.h │ │ │ ├── HUMAnnotationView.m │ │ │ ├── HUMButton.h │ │ │ ├── HUMButton.m │ │ │ ├── HUMFooterView.h │ │ │ └── HUMFooterView.m │ │ └── main.m │ ├── HumonTests │ │ ├── HUMEventJson.json │ │ ├── HUMEventKiwiTests.m │ │ ├── HUMEventTests.m │ │ ├── HUMJSONFileManager.h │ │ ├── HUMJSONFileManager.m │ │ ├── HUMJSONFileManagerKiwiTests.m │ │ ├── HUMJSONFileManagerTests.m │ │ ├── HUMUserKiwiTests.m │ │ ├── HUMUserSessionKiwiTests.m │ │ ├── HUMUserSessionTests.m │ │ ├── HUMUserTests.m │ │ └── Info.plist │ ├── Podfile │ └── README.md └── rails │ ├── .gitignore │ ├── .ruby-version │ ├── Gemfile │ ├── Gemfile.lock │ ├── Procfile │ ├── README.md │ ├── Rakefile │ ├── app │ ├── assets │ │ ├── images │ │ │ └── .keep │ │ └── javascripts │ │ │ ├── application.js │ │ │ └── prefilled_input.js │ ├── controllers │ │ ├── api │ │ │ └── v1 │ │ │ │ ├── attendances_controller.rb │ │ │ │ ├── events │ │ │ │ └── nearests_controller.rb │ │ │ │ ├── events_controller.rb │ │ │ │ └── users_controller.rb │ │ ├── api_controller.rb │ │ ├── application_controller.rb │ │ └── concerns │ │ │ └── .keep │ ├── helpers │ │ └── application_helper.rb │ ├── mailers │ │ └── .keep │ ├── models │ │ ├── .keep │ │ ├── attendance.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── event.rb │ │ └── user.rb │ └── views │ │ ├── api │ │ └── v1 │ │ │ ├── attendances │ │ │ └── create.json.jbuilder │ │ │ ├── events │ │ │ ├── _event.json.jbuilder │ │ │ ├── create.json.jbuilder │ │ │ ├── nearests │ │ │ │ └── index.json.jbuilder │ │ │ ├── show.json.jbuilder │ │ │ └── update.json.jbuilder │ │ │ └── users │ │ │ └── create.json.jbuilder │ │ ├── application │ │ ├── _flashes.html.erb │ │ └── _javascript.html.erb │ │ ├── layouts │ │ └── application.html.erb │ │ └── pages │ │ └── .keep │ ├── bin │ ├── bundle │ ├── delayed_job │ ├── rails │ ├── rake │ └── setup │ ├── config.ru │ ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ ├── staging.rb │ │ └── test.rb │ ├── initializers │ │ ├── backtrace_silencers.rb │ │ ├── disable_xml_params.rb │ │ ├── errors.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── rack_timeout.rb │ │ ├── secret_token.rb │ │ ├── session_store.rb │ │ ├── smtp.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── routes.rb │ └── unicorn.rb │ ├── db │ ├── migrate │ │ ├── 20131003185021_create_delayed_jobs.rb │ │ ├── 20131003213856_create_users.rb │ │ ├── 20131028210819_create_events.rb │ │ ├── 20131030180424_add_location_fields_to_events.rb │ │ ├── 20131030184615_change_user_fields.rb │ │ └── 20131031224142_create_attendances.rb │ ├── schema.rb │ └── seeds.rb │ ├── lib │ ├── assets │ │ └── .keep │ └── tasks │ │ └── .keep │ ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── favicon.ico │ └── robots.txt │ ├── spec │ ├── controllers │ │ ├── .keep │ │ └── api │ │ │ └── v1 │ │ │ └── users_controller_spec.rb │ ├── factories.rb │ ├── helpers │ │ └── .keep │ ├── lib │ │ └── .keep │ ├── models │ │ ├── attendance_spec.rb │ │ ├── event_spec.rb │ │ ├── factories_spec.rb │ │ └── user_spec.rb │ ├── requests │ │ └── api │ │ │ └── v1 │ │ │ ├── attendances_spec.rb │ │ │ ├── events │ │ │ ├── events_spec.rb │ │ │ └── nearest_spec.rb │ │ │ └── users_spec.rb │ ├── spec_helper.rb │ └── support │ │ ├── background_jobs.rb │ │ ├── database_cleaner.rb │ │ ├── factory_girl.rb │ │ ├── matchers │ │ └── .keep │ │ ├── mixins │ │ └── .keep │ │ ├── request_headers.rb │ │ ├── response_json.rb │ │ ├── shared_examples │ │ └── .keep │ │ └── shoulda_matchers.rb │ └── vendor │ └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep └── release ├── images ├── cover.pdf ├── cover.png ├── humon-database-representation.png ├── ios_app_skeleton_1.png ├── ios_app_skeleton_2.png ├── ios_app_skeleton_3.png ├── ios_event_object_1.png ├── ios_new_xcode_project_1.png ├── ios_new_xcode_project_2.png └── ios_new_xcode_project_3.png ├── ios-on-rails-sample.epub ├── ios-on-rails-sample.html ├── ios-on-rails-sample.md ├── ios-on-rails-sample.mobi ├── ios-on-rails-sample.pdf ├── ios-on-rails-sample.toc.html ├── ios-on-rails.epub ├── ios-on-rails.html ├── ios-on-rails.md ├── ios-on-rails.mobi ├── ios-on-rails.pdf └── ios-on-rails.toc.html /.github/workflows/dynamic-security.yml: -------------------------------------------------------------------------------- 1 | name: update-security 2 | 3 | on: 4 | push: 5 | paths: 6 | - SECURITY.md 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | update-security: 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | pages: write 17 | uses: thoughtbot/templates/.github/workflows/dynamic-security.yaml@main 18 | secrets: 19 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | .env 3 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.0 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'paperback', git: 'git@github.com:thoughtbot/paperback.git' 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git@github.com:thoughtbot/paperback.git 3 | revision: b2a2a59c5ea19c42d91461a128ce2e0a6c55b762 4 | specs: 5 | paperback (0.0.1) 6 | activesupport (~> 4.0.0) 7 | asset_sync (~> 1.0.0) 8 | cocaine (~> 0.5.1) 9 | coderay (~> 1.0.9) 10 | dotenv (~> 0.7.0) 11 | kindlegen (~> 2.7.0) 12 | nokogiri (~> 1.5.9) 13 | pdf-reader (~> 1.3.2) 14 | redcarpet (~> 2.2.2) 15 | rmagick (~> 2.13.1) 16 | terminal-table (~> 1.4.5) 17 | thor (~> 0.18.1) 18 | 19 | GEM 20 | remote: https://rubygems.org/ 21 | specs: 22 | Ascii85 (1.0.2) 23 | activemodel (4.0.4) 24 | activesupport (= 4.0.4) 25 | builder (~> 3.1.0) 26 | activesupport (4.0.4) 27 | i18n (~> 0.6, >= 0.6.9) 28 | minitest (~> 4.2) 29 | multi_json (~> 1.3) 30 | thread_safe (~> 0.1) 31 | tzinfo (~> 0.3.37) 32 | afm (0.2.0) 33 | asset_sync (1.0.0) 34 | activemodel 35 | fog (>= 1.8.0) 36 | builder (3.1.4) 37 | climate_control (0.0.3) 38 | activesupport (>= 3.0) 39 | cocaine (0.5.4) 40 | climate_control (>= 0.0.3, < 1.0) 41 | coderay (1.0.9) 42 | dotenv (0.7.0) 43 | excon (0.33.0) 44 | fog (1.22.0) 45 | fog-brightbox 46 | fog-core (~> 1.21, >= 1.21.1) 47 | fog-json 48 | nokogiri (~> 1.5, >= 1.5.11) 49 | fog-brightbox (0.0.2) 50 | fog-core 51 | fog-json 52 | fog-core (1.22.0) 53 | builder 54 | excon (~> 0.33) 55 | formatador (~> 0.2) 56 | mime-types 57 | net-scp (~> 1.1) 58 | net-ssh (>= 2.1.3) 59 | fog-json (1.0.0) 60 | multi_json (~> 1.0) 61 | formatador (0.2.4) 62 | hashery (2.1.1) 63 | i18n (0.6.9) 64 | kindlegen (2.7.0) 65 | systemu 66 | mime-types (2.2) 67 | minitest (4.7.5) 68 | multi_json (1.9.3) 69 | net-scp (1.2.0) 70 | net-ssh (>= 2.6.5) 71 | net-ssh (2.8.0) 72 | nokogiri (1.5.11) 73 | pdf-reader (1.3.3) 74 | Ascii85 (~> 1.0.0) 75 | afm (~> 0.2.0) 76 | hashery (~> 2.0) 77 | ruby-rc4 78 | ttfunk 79 | redcarpet (2.2.2) 80 | rmagick (2.13.2) 81 | ruby-rc4 (0.1.5) 82 | systemu (2.6.4) 83 | terminal-table (1.4.5) 84 | thor (0.18.1) 85 | thread_safe (0.3.3) 86 | ttfunk (1.1.1) 87 | tzinfo (0.3.39) 88 | 89 | PLATFORMS 90 | ruby 91 | 92 | DEPENDENCIES 93 | paperback! 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | End-User Warranty and License Agreement 2 | 3 | 1. Grant of License 4 | thoughtbot has authorized download by you of one copy of the electronic book 5 | (ebook). thoughtbot grants you a nonexclusive, nontransferable license to use 6 | the ebook according to the terms and conditions herein. This 7 | License Agreement permits you to install the ebook for your use only. 8 | 9 | 2. Restrictions 10 | You shall not: (1) resell, rent, assign, timeshare, distribute, or transfer 11 | all or part of the ebook or any rights granted hereunder to any 12 | other person; (2) duplicate the ebook, except for a single backup 13 | or archival copy; (3) remove any proprietary notices, labels, or marks 14 | from the ebook ; (4) transfer or sublicense title to the ebook 15 | to any other party. 16 | 17 | 3. Intellectual Property Protection 18 | The ebook is owned by thoughtbot and is protected by United States 19 | and international copyright and other intellectual property 20 | laws. thoughtbot reserves all rights in the ebook not expressly 21 | granted herein. This license and your right to use the ebook 22 | terminate automatically if you violate any part of this Agreement. In 23 | the event of termination, you must destroy the original and all copies 24 | of the ebook. 25 | 26 | 4. Limited Warranty 27 | thoughtbot warrants that the files containing the ebook a copy of 28 | which you authorized to download are free from defects in the 29 | operational sense that they can be read by a PDF Reader. EXCEPT FOR 30 | THIS EXPRESS LIMITED WARRANTY, MANNING MAKES AND YOU RECEIVE NO 31 | WARRANTIES, EXPRESS, IMPLIED, STATUTORY OR IN ANY COMMUNICATION WITH 32 | YOU, AND MANNING SPECIFICALLY DISCLAIMS ANY OTHER WARRANTY INCLUDING 33 | THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS OR A PARTICULAR 34 | PURPOSE. MANNING DOES NOT WARRANT THAT THE OPERATION OF THE EBOOK 35 | WILL BE UNINTERRUPTED OR ERROR FREE. If the ebook was purchased in 36 | the United States, the above exclusions may not apply to you as some 37 | states do not allow the exclusion of implied warranties. In addition 38 | to the above warranty rights, you may also have other rights that vary 39 | from state to state. 40 | 41 | 5. Limitation of Liability 42 | IN NO EVENT WILL MANNING BE LIABLE FOR ANY DAMAGES, WHETHER RISING FOR 43 | TORT OR CONTRACT, INCLUDING LOSS OF DATA, LOST PROFITS, OR OTHER 44 | SPECIAL, INCIDENTAL, CONSEQUENTIAL, OR INDIRECT DAMAGES RISING OUT OF 45 | THE USE OR INABILITY TO USE THE EBOOK. 46 | 47 | 6. General 48 | This Agreement constitutes the entire agreement between you and 49 | thoughtbot and supersedes any prior agreement concerning the 50 | ebook. This Agreement is governed by the laws of the Commonwealth of 51 | Massachusetts without reference to conflicts of laws provisions. 52 | 53 | (c) 2013 by thoughtbot 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Get the latest release of the book 2 | 3 | The quickest way to start reading right now is to view the PDF version here: 4 | 5 | 6 | 7 | The book is currently available in the following formats: 8 | 9 | * PDF: [release/ios-on-rails.pdf](https://github.com/thoughtbot/ios-on-rails/raw/master/release/ios-on-rails.pdf) 10 | * Single-page HTML: [release/ios-on-rails.html](https://github.com/thoughtbot/ios-on-rails/raw/master/release/ios-on-rails.html) 11 | * Epub (iPad, Nook): [release/ios-on-rails.epub](https://github.com/thoughtbot/ios-on-rails/raw/master/release/ios-on-rails.epub) 12 | * Mobi (Kindle): [release/ios-on-rails.mobi](https://github.com/thoughtbot/ios-on-rails/raw/master/release/ios-on-rails.mobi) 13 | 14 | For the HTML version, clone the repository and look at the HTML so that images 15 | and other assets are properly loaded. 16 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | # Security Policy 3 | 4 | ## Supported Versions 5 | 6 | Only the the latest version of this project is supported at a given time. If 7 | you find a security issue with an older version, please try updating to the 8 | latest version first. 9 | 10 | If for some reason you can't update to the latest version, please let us know 11 | your reasons so that we can have a better understanding of your situation. 12 | 13 | ## Reporting a Vulnerability 14 | 15 | For security inquiries or vulnerability reports, visit 16 | . 17 | 18 | If you have any suggestions to improve this policy, visit . 19 | 20 | 21 | -------------------------------------------------------------------------------- /bin/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Run this script to deploy the app to Heroku. 4 | 5 | set -e 6 | 7 | target="${1:-staging}" 8 | 9 | git subtree push --prefix=example_apps/rails "$target" "master" 10 | heroku run rake db:migrate --remote "$target" 11 | heroku restart --remote "$target" 12 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Set up the staging and production apps. 4 | if heroku join --app humon-staging &> /dev/null; then 5 | git remote add staging git@heroku.com:humon-staging.git || true 6 | printf 'You are a collaborator on the "humon-staging" Heroku app 7 | ' 8 | else 9 | printf 'Ask for access to the "humon-staging" Heroku app 10 | ' 11 | fi 12 | 13 | if heroku join --app humon-production &> /dev/null; then 14 | git remote add production git@heroku.com:humon-production.git || true 15 | printf 'You are a collaborator on the "humon-production" Heroku app 16 | ' 17 | else 18 | printf 'Ask for access to the "humon-production" Heroku app 19 | ' 20 | fi 21 | -------------------------------------------------------------------------------- /book/book.md: -------------------------------------------------------------------------------- 1 | % iOS on Rails 2 | % thoughtbot; Jessie Young; Diana Zmuda 3 | 4 | \clearpage 5 | 6 | <<[introduction/introduction.md] 7 | 8 | \mainmatter 9 | 10 | \part{Building the Humon Rails App} 11 | 12 | <<[rails/introduction.md] 13 | 14 | <<[rails/creating_a_get_request.md] 15 | 16 | <<[rails/creating_a_post_request.md] 17 | 18 | <<[rails/creating_a_patch_request.md] 19 | 20 | <<[rails/creating_a_geocoded_get_request.md] 21 | 22 | \part{Building the Humon iOS App} 23 | 24 | <<[iOS/introduction.md] 25 | 26 | <<[iOS/a_new_xcode_project.md] 27 | 28 | <<[iOS/cocoapods.md] 29 | 30 | <<[iOS/app_skeleton.md] 31 | 32 | <<[iOS/map_view_controller.md] 33 | 34 | <<[iOS/add_an_event_view_controller.md] 35 | 36 | <<[iOS/api_client.md] 37 | 38 | <<[iOS/user_object.md] 39 | 40 | <<[iOS/posting_a_user.md] 41 | 42 | <<[iOS/making_the_post_user_request.md] 43 | 44 | <<[iOS/event_object.md] 45 | 46 | <<[iOS/posting_an_event.md] 47 | 48 | <<[iOS/making_the_post_event_request.md] 49 | 50 | <<[iOS/posting_with_the_event_view_controller.md] 51 | 52 | <<[iOS/getting_an_event.md] 53 | 54 | <<[iOS/displaying_events.md] 55 | 56 | <<[iOS/viewing_with_the_event_view_controller.md] 57 | 58 | <<[iOS/finishing_touches.md] 59 | -------------------------------------------------------------------------------- /book/iOS/a_new_xcode_project.md: -------------------------------------------------------------------------------- 1 | # A New Xcode Project 2 | 3 | For all iOS apps, the first step is to create a new project in Xcode. 4 | Create a new "Single View Project" with your own name and identifier. 5 | Running the project for the first time will yield a white screen. 6 | 7 | ![Pick an Empty Application](images/ios_new_xcode_project_1.png) 8 | 9 | We want our app to target iOS 8.0 and above. 10 | Open the Humon project in the file navigator and select the Humon target. 11 | Under the "General" tab, select 8.0 as the Deployment Target. 12 | 13 | ![Change the Deployment Target](images/ios_new_xcode_project_2.png) 14 | 15 | A useful resource for new Objective-C projects is GitHub's 16 | [gitignore template. ](https://github.com/github/gitignore/blob/master/Objective-C.gitignore) 17 | Add a gitignore to your new project before creating your first commit. 18 | -------------------------------------------------------------------------------- /book/iOS/add_an_event_view_controller.md: -------------------------------------------------------------------------------- 1 | # The Event View Controller 2 | 3 | ![Table View for event details](images/ios_app_skeleton_2.png) 4 | 5 | ### Subclassing UITableViewController 6 | 7 | Create a new subclass of `UITableViewController` called `HUMEventViewController`. `UITableViewController` is a subclass of `UIViewController` that has a `tableView` property and conforms to the `` and `` protocols. This means that we have to implement the `tableView:numberOfRowsInSection:` and `tableView:cellForRowAtIndexPath:` so the `tableView` will know how many cells to display and what these cells will look like. You can remove any other `-tableView:...` methods you see in the implementation file. 8 | 9 | // HUMEventViewController.m 10 | 11 | - (NSInteger)tableView:(UITableView *)tableView 12 | numberOfRowsInSection:(NSInteger)section 13 | { 14 | return 5; 15 | } 16 | 17 | - (UITableViewCell *)tableView:(UITableView *)tableView 18 | cellForRowAtIndexPath:(NSIndexPath *)indexPath 19 | { 20 | UITableViewCell *cell = [tableView 21 | dequeueReusableCellWithIdentifier:HUMEventCellIdentifier 22 | forIndexPath:indexPath]; 23 | 24 | return cell; 25 | } 26 | 27 | The method `-tableView:cellForRowAtIndexPath:` returns a cell for every row in the tableview. Instead of instantiating and returning a new cell every time, we use `-dequeueReusableCellWithIdentifier:forIndexPath:` so we can reuse cells that have already been instantiated. The identifier argument allows you to recycle different types of cells, in case you wanted to have a `@"GreenCellIdentifier"` and a `@"BlueCellIdentifier"`. 28 | 29 | We use static strings as cell identifiers, since there's no need to create a new instance of the identifier every time we want to use it. This is why we are using `HUMEventCellIdentifier` here instead of a string literal like `@"cell"`. 30 | 31 | Create a static string named `HUMEventCellIdentifier` and place it just below your imports. Now you can refer to this `@"HUMEventCellIdentifier"` string as `HUMEventCellIdentifier` throughout the file. 32 | 33 | // HUMEventViewController.m 34 | 35 | #import "HUMEventViewController.h" 36 | 37 | static NSString *const HUMEventCellIdentifier = @"HUMEventCellIdentifier"; 38 | 39 | ... 40 | 41 | - (void)viewDidLoad 42 | { 43 | [super viewDidLoad]; 44 | 45 | [self.tableView registerClass:[UITableViewCell class] 46 | forCellReuseIdentifier:HUMEventCellIdentifier]; 47 | } 48 | 49 | If we want to be able to reuse cells using the `HUMEventCellIdentifier`, we have to register a class that the `tableView` will create or reuse an instance of when we call `dequeueReusableCellWithIdentifier:forIndexPath:`. We do this inside of `viewDidLoad`. 50 | 51 | ### Linking the Add Button to the HUMEventViewController 52 | 53 | Now that we have created a `HUMEventViewController` we can create and show the add view from the `HUMMapViewController`. Go back to the `HUMMapViewController`'s implementation file and add `#import "HUMEventViewController.h"` below the `#import "HUMMapViewController.h"` to import the header file we created in the previous section. 54 | 55 | Now we can replace the `-addButtonPressed` method to present a `HUMEventViewController`. When we press the "Add" button on top of the map view, we can either: 56 | 57 | 1. Push a new `HUMEventViewController` onto the navigation stack managed by the `UINavigationController` we created in the `AppDelegate.m`. 58 | 59 | 2. Present a new `HUMEventViewController` modally. 60 | 61 | Having the `HUMMapViewController` present modally means the `HUMEventViewController` would animate sliding up from the bottom. 62 | 63 | Pushing onto the navigation stack means the `UINavigationController` we created in the `AppDelegate.m` would contain a `HUMMapViewController` at the bottom, and a `HUMEventViewController` on the top. That topmost view controller, the `HUMEventViewController`. will be visible. 64 | 65 | This is what we're going to do, and it will also give us the "Back" functionality for dismissing the `HUMEventViewController`. 66 | 67 | // HUMMapViewController.m 68 | 69 | - (void)addButtonPressed 70 | { 71 | HUMEventViewController *eventViewController = [[HUMEventViewController alloc] init]; 72 | [self.navigationController pushViewController:eventViewController 73 | animated:YES]; 74 | } 75 | 76 | You can run the Humon app now and press the "Add" button to see your new event view controller. 77 | -------------------------------------------------------------------------------- /book/iOS/app_skeleton.md: -------------------------------------------------------------------------------- 1 | # The Mobile App's Skeleton 2 | 3 | The Humon app is going to have two view controllers: 4 | 5 | 1. The initial view will be a large map view with pins for events that are near you. It will also contain a button for creating a new event. 6 | 7 | 2. The views for creating and viewing an event will be very similar. The view will be a table with cells for the address, name, and time of an event. The only difference is that the text fields for creating an event will be editable. 8 | 9 | We can either create our view controllers programatically or use Storyboards. 10 | For the purposes of this book, we will be creating all our view controllers programatically. 11 | Storyboards are incredibly useful tools for visually laying out an app's view controllers, 12 | but learning about Interface Builder is out of scope for this book. 13 | Apple provides a good introduction to Interface Builder and 14 | [Storyboards](https://developer.apple.com/library/ios/recipes/xcode_help-IB_storyboard/_index.html) 15 | in their Developer Library. 16 | 17 | To use programatically created view controllers instead of the Storyboard 18 | provided by Xcode: 19 | 20 | 1. Delete the Main.storyboard file in the file navigator. Choose "Move to Trash" rather than just "Remove Reference". 21 | 22 | 2. Select the Humon project in the file navigator. Select the Humon target and under the "Info" tab, delete the "Main storyboard file base name". This will remove the reference to the Storyboard you deleted. 23 | 24 | ![Delete the Storyboard from the Info Plist](images/ios_new_xcode_project_3.png) 25 | -------------------------------------------------------------------------------- /book/iOS/cocoapods.md: -------------------------------------------------------------------------------- 1 | # Managing Dependencies 2 | 3 | ### Using CococaPods 4 | 5 | Before we create our new iOS project, 6 | lets discuss the libraries and resources we will use. 7 | 8 | We'll be using CocoaPods to manage our dependencies. 9 | CocoaPods is a Ruby gem and command line tool 10 | that makes it easy to add dependencies to your project. 11 | We prefer CocoaPods over Git submodules 12 | due to its ease of implementation 13 | and the wide variety of third-party libraries available as pods. 14 | CocoaPods will not only download the libraries we need 15 | and link them to our project in Xcode, 16 | it will also allow us to easily manage 17 | and update which version of each library we want to use. 18 | 19 | ### CocoaPods Setup 20 | 21 | What follows is a succinct version of the instructions on the [CocoaPods](http://guides.cocoapods.org/using/getting-started.html) website: 22 | 23 | 1. `$ gem install cocoapods` or `$ gem install cocoapods --pre` to use the latest version. The Podfile we provide in the next section uses CocoaPods v1.0 syntax. 24 | 25 | 2. Navigate to your iOS project's root directory. 26 | 27 | 3. Create a text file named `Podfile` using your editor of choice. 28 | 29 | 4. `$ pod install` 30 | 31 | 5. If you have your iOS project open in Xcode, close it and reopen the workspace that CocoaPods generated for you. 32 | 33 | 6. When using CocoaPods in conjunction with Git, you may choose to ignore the Pods directory so the libraries that CocoaPods downloads are not under version control. If you want to do this, add `Pods` your .gitignore. Anyone who clones your project will need to `$ pod install` to retrieve the libraries that the project requires. 34 | 35 | ### Humon's Podfile 36 | 37 | Installing the CocoaPods gem and creating a podfile is covered in more detail on their website. 38 | Below is the podfile we're going to use for this project. 39 | 40 | platform :ios, '7.0' 41 | 42 | target 'Humon' do 43 | pod 'SSKeychain', '~> 1.2.2' 44 | pod 'SVProgressHUD', '~> 1.0' 45 | end 46 | 47 | SSKeychain will help us save user info to the keychain. 48 | SVProgressHUD will let us display loading views to the user. 49 | Once you've updated your podfile, go ahead and run `$ pod install` 50 | -------------------------------------------------------------------------------- /book/iOS/finishing_touches.md: -------------------------------------------------------------------------------- 1 | # Finishing Touches 2 | 3 | Now that we've created an app that can interact with our API using the POST and GET methods 4 | we can think about implementing more features. 5 | 6 | These features are currently implemented in the sample app 7 | so feel free to reference the code there if you choose to try any of these. 8 | 9 | 1. Implement unit tests using Kiwi or the built-in XCTest. Tests in both languages are included in the example app for you to reference. 10 | 11 | 2. Use [Auto Layout](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/index.html) on all the views in the app so the app can be used in landscape. 12 | 13 | 3. Add a confirmation view controller that pushes onto the nav stack after you POST to users, with a button that lets you share your event to Facebook and Twitter. 14 | 15 | 4. Add a `user` property to the Event class. Then you can conditionally let the user PATCH an event if they are the owner. 16 | 17 | 5. Implement a POST to attendences method to let a user indicate they plan to attend an event. 18 | 19 | 6. Add custom map pin images and pick a tint color for your app. 20 | 21 | 7. Insert a date picker into the table view, rather than having it pop up from the bottom as the `textView`'s `-inputView`. 22 | 23 | 8. Prevent the user from attempting to POST an event if they haven't filled out the required fields. Ignore optional fields when you're determining validity. 24 | 25 | 9. Let the user pick any location, not just their current location. Use geocoding to automatically fill out the address for that location. 26 | 27 | 10. Use a different date formatter to format all the user-facing dates in a human-readable format. 28 | -------------------------------------------------------------------------------- /book/iOS/getting_an_event.md: -------------------------------------------------------------------------------- 1 | # Getting Events With NSURLSession 2 | 3 | ### Declaring the Get Events Method 4 | 5 | To make the event GET request, typedef a completion block that will return an array of events or an error once we receive event JSON from the API. 6 | 7 | //HUMRailsClient.h 8 | 9 | typedef void(^HUMRailsClientEventsCompletionBlock)(NSArray *events, NSError *error); 10 | 11 | Then declare a method for fetching events whose parameters are a map region and a completion block of this new type. The `region` will be the visible map region in our HUMMapViewController, since we only want to load events within the region we're viewing. Unlike our other API client methods, we'll return an `NSURLSessionDataTask` from this method so we can cancel the task. 12 | 13 | //HUMRailsClient.h 14 | 15 | - (NSURLSessionDataTask *)fetchEventsInRegion:(MKCoordinateRegion)region 16 | withCompletionBlock:(HUMRailsClientEventsCompletionBlock)block; 17 | 18 | ### Creating the Get Events Request 19 | 20 | Now we can implement this method: 21 | 22 | // HUMRailsClient.m 23 | 24 | - (NSURLSessionDataTask *)fetchEventsInRegion:(MKCoordinateRegion)region 25 | withCompletionBlock:(HUMRailsClientEventsCompletionBlock)block 26 | { 27 | // region.span.latitudeDelta/2*111 is how we find the aproximate radius 28 | // that the screen is displaying in km. 29 | NSString *parameters = [NSString stringWithFormat: 30 | @"?lat=%@&lon=%@&radius=%@", 31 | @(region.center.latitude), 32 | @(region.center.longitude), 33 | @(region.span.latitudeDelta/2*111)]; 34 | 35 | NSString *urlString = [NSString stringWithFormat:@"%@events/nearests%@", 36 | ROOT_URL, parameters]; 37 | NSURL *url = [NSURL URLWithString:urlString]; 38 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:url]; 39 | [request setHTTPMethod:@"GET"]; 40 | 41 | return nil; 42 | } 43 | 44 | The `parameters` for our GET request contain `lat`, `lon`, and `radius`. The Rails app will use these values to return a list of events that are less than the `radius` (in kilometers) away from the map region's centerpoint. 45 | 46 | We want to inscribe our square `mapView` span inside our circular API search area so we receive more events than need to be displayed, rather than too few. We use half the width of the `mapView` (the `latitudeDelta` property) as our radius since the lateral span is the larger value in portrait mode. Multiplying by 111 is simply the conversion from degrees of latitude to kilometers. 47 | 48 | ### Creating the Get Events Task 49 | 50 | Now that we've created the request for fetching events in a region, we can create the actual task. 51 | 52 | // HUMRailsClient.m 53 | 54 | - (NSURLSessionDataTask *)fetchEventsInRegion:(MKCoordinateRegion)region 55 | withCompletionBlock:(HUMRailsClientEventsCompletionBlock)block 56 | { 57 | 58 | ... 59 | 60 | NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request 61 | completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 62 | 63 | NSArray *events; 64 | 65 | if (!error) { 66 | id responseJSON = [NSJSONSerialization JSONObjectWithData:data 67 | options:kNilOptions 68 | error:nil]; 69 | 70 | if ([responseJSON isKindOfClass:[NSArray class]]) { 71 | events = [HUMEvent eventsWithJSON:responseJSON]; 72 | } 73 | } 74 | 75 | dispatch_async(dispatch_get_main_queue(), ^{ 76 | block(events, error); 77 | }); 78 | 79 | }]; 80 | [task resume]; 81 | 82 | return task; 83 | } 84 | 85 | Since our rails API returns from a successful GET request with either a "No events in area" dictionary or an array of event JSON, our success block has to handle both cases. If we receive an array, we execute the completion block with an array of events. Otherwise, `events` will be `nil`. 86 | 87 | In the case of failure, we simply execute our completion block with an error. 88 | 89 | -------------------------------------------------------------------------------- /book/iOS/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The iOS portion of this book will cover creating an Xcode project, 4 | using CocoaPods, 5 | making requests to the API you just created, 6 | and displaying the JSON you receive. 7 | 8 | If you haven't created a project with Xcode before, 9 | we have included a few images 10 | to assist your understanding of Apple's dev tools. 11 | Xcode is an exciting editor that takes a bit of getting used to, 12 | so if you would like a primer please visit 13 | [Apple's Xcode Overview.](https://developer.apple.com/library/mac/documentation/ToolsLanguages/Conceptual/Xcode_Overview/index.html) 14 | 15 | CocoaPods should feel quite familiar to Ruby developers, 16 | since it is written in Ruby 17 | and allows you to use iOS libraries similarly to how you use Ruby gems. 18 | Like Rails, iOS uses the Model-View-Controller design pattern, 19 | with the small caveat that most of your controllers 20 | will instead be called ViewControllers. 21 | -------------------------------------------------------------------------------- /book/iOS/making_the_post_event_request.md: -------------------------------------------------------------------------------- 1 | # Making the POST Event Request 2 | 3 | ### Creating a New Init Method 4 | 5 | Our POST to events will happen in the `HUMEventViewController`. This view controller will be used for creating a new event as well as viewing other people's events, so we'll create an init method that encompasses both these cases. 6 | 7 | // HUMEventViewController.h 8 | 9 | @class HUMEvent; 10 | 11 | @interface HUMEventViewController : UITableViewController 12 | 13 | - (instancetype)initWithEvent:(HUMEvent *)event editable:(BOOL)editable; 14 | 15 | @end 16 | 17 | Now, lets implement this method. Don't forget to `#import "HUMEvent.h"` 18 | 19 | // HUMEventViewController.m 20 | 21 | - (instancetype)initWithEvent:(HUMEvent *)event editable:(BOOL)editable; 22 | { 23 | self = [super initWithStyle:UITableViewStylePlain]; 24 | if (!self) { 25 | return nil; 26 | } 27 | 28 | _event = event; 29 | _editable = editable; 30 | 31 | return self; 32 | } 33 | 34 | This method implementation references two properties we don't have yet, so place declarations for those in the hidden interface. 35 | 36 | // HUMEventViewController.m 37 | 38 | @interface HUMEventViewController () 39 | 40 | @property (strong, nonatomic) HUMEvent *event; 41 | @property (assign, nonatomic) BOOL editable; 42 | 43 | @end 44 | 45 | ### Adding a Submit Event Method 46 | 47 | Let's take a step back and remember what this view controller is supposed to look like. The `HUMEventViewController` will have a cell for each property of our event (name, address, startDate, endDate) and a submit cell. Instead of referring to these by their indexes, let's define these cell indexes in an enum at the top of `HUMEventViewController.h`. This enum starts at `HUMEventCellName = 0` and goes to `HUMEventCellCount = 5`. 48 | 49 | // HUMEventViewController.h 50 | 51 | typedef NS_ENUM(NSUInteger, HUMEventCell) { 52 | HUMEventCellName, 53 | HUMEventCellAddress, 54 | HUMEventCellStart, 55 | HUMEventCellEnd, 56 | HUMEventCellSubmit, 57 | HUMEventCellCount 58 | }; 59 | 60 | Now we can change the `-tableView:numberOfRowsInSection:` to `return HUMEventCellCount;` instead of `return 5;` 61 | 62 | We can also use this new enum when we declare `-tableView:didSelectRowAtIndexPath:` to determine which cell was selected. 63 | 64 | // HUMEventViewController.m 65 | 66 | - (void)tableView:(UITableView *)tableView 67 | didSelectRowAtIndexPath:(NSIndexPath *)indexPath 68 | { 69 | // Return if the user didn't select the submit cell 70 | // This is the same as (indexPath.row != 5) but much more readable 71 | if (indexPath.row != HUMEventCellSubmit) { 72 | return; 73 | } 74 | 75 | // Post the event 76 | [SVProgressHUD show]; 77 | [[HUMRailsClient sharedClient] createEvent:self.event 78 | withCompletionBlock:^(NSString *eventID, NSError *error) { 79 | 80 | // Handle the error or dismiss the view controller on success 81 | if (error) { 82 | [SVProgressHUD showErrorWithStatus: 83 | NSLocalizedString(@"Failed to create event.", nil)]; 84 | } else { 85 | [SVProgressHUD dismiss]; 86 | [self.navigationController popToRootViewControllerAnimated:YES]; 87 | } 88 | 89 | }]; 90 | } 91 | 92 | ### Using the New Init Method 93 | 94 | Now we just have to use this new init method in the `HUMMapViewController`. Change the `-addButtonPressed` method to use this new initializer and be sure to `#import "HUMEvent.h"`. 95 | 96 | // HUMMapViewController.m 97 | 98 | - (void)addButtonPressed 99 | { 100 | // Create a fake event at the centerpoint of the map 101 | HUMEvent *event = [[HUMEvent alloc] init]; 102 | event.name = @"Picnic"; 103 | event.address = @"123 Fake St."; 104 | event.coordinate = self.mapView.centerCoordinate; 105 | event.startDate = [NSDate date]; 106 | event.endDate = [NSDate dateWithTimeIntervalSinceNow:100]; 107 | 108 | // Push an event controller with the fake event 109 | HUMTableViewController *eventViewController = 110 | [[HUMTableViewController alloc] initWithEvent:event editable:YES]; 111 | [self.navigationController pushViewController:eventViewController 112 | animated:YES]; 113 | } 114 | 115 | Now, if you run the app, you should be able to press the add button, tap the 5th cell in the event table view, and make a successful POST to events. 116 | -------------------------------------------------------------------------------- /book/iOS/making_the_post_user_request.md: -------------------------------------------------------------------------------- 1 | # Making the POST User Request 2 | 3 | We want to make a POST request to create and save a user only once on each device. So let's conditionally call the `-createCurrentUserWithCompletionBlock:` we just created inside `HUMMapViewController`'s `-viewDidAppear:` method. Remember to `#import "HUMRailsClient.h"`. 4 | 5 | // HUMMapViewController.m 6 | 7 | - (void)viewDidAppear:(BOOL)animated 8 | { 9 | [super viewDidAppear:animated]; 10 | 11 | if (![HUMUserSession userIsLoggedIn]) { 12 | 13 | [SVProgressHUD show]; 14 | 15 | [[HUMRailsClient sharedClient] 16 | createCurrentUserWithCompletionBlock:^(NSError *error) { 17 | 18 | if (error) { 19 | [SVProgressHUD showErrorWithStatus: 20 | NSLocalizedString(@"App authentication error", nil)]; 21 | } else { 22 | [SVProgressHUD dismiss]; 23 | } 24 | 25 | }]; 26 | 27 | } 28 | } 29 | 30 | If `+[HUMUserSession userIsLoggedIn]` returns `NO`, then we haven't successfully made a POST request to /users. So we can call `-createCurrentUserWithCompletionBlock:` to make our POST request. That method saves the user ID that returns from the API request and changes the request headers to include this user ID. 31 | 32 | We'll also present a heads-up-display to users to indicate that an API call is in progress. SVProgressHUD is a CocoaPod that provides a clean and easy-to-use view for showing loading and percent completion. We simply call the SVProgressHUD class method `show` to display the HUD, and `+dismiss` to remove it. Remember to `#import ` since we're using it in this file. 33 | 34 | If you run the app and get back a completionBlock with no error, you've officially made a successful POST request and created a user on the database! 35 | -------------------------------------------------------------------------------- /book/iOS/posting_an_event.md: -------------------------------------------------------------------------------- 1 | # Posting an Event With NSURLSession 2 | 3 | Our `HUMRailsClient` is already all set up to use the appropriate headers that we need for POST to events: namely, the token we receive back from POST to users. So we simply need to define a new method for making a POST to events request. 4 | 5 | ### Declaring a Task for Making Requests 6 | 7 | The completion block we'll use for our create event method should return an event ID and an error. If our request is successful, the API will return the event ID for the event it created and a nil error. If our request fails, we'll return a nil event ID and the error. 8 | 9 | Typedef this new type of event completion block: 10 | 11 | // HUMRailsClient.h 12 | 13 | typedef void(^HUMRailsClientEventIDCompletionBlock)(NSString *eventID, NSError *error); 14 | 15 | and declare the event creation method: 16 | 17 | // HUMRailsClient.h 18 | 19 | - (void)createEvent:(HUMEvent *)event 20 | withCompletionBlock:(HUMRailsClientEventIDCompletionBlock)block; 21 | 22 | ### Creating a Task for Making Requests 23 | 24 | Define the event creation method as follows: 25 | 26 | // HUMRailsClient.m 27 | 28 | - (void)createEvent:(HUMEvent *)event 29 | withCompletionBlock:(HUMRailsClientEventIDCompletionBlock)block 30 | { 31 | NSData *JSONdata = [NSJSONSerialization 32 | dataWithJSONObject:[event JSONDictionary] 33 | options:kNilOptions 34 | error:nil]; 35 | 36 | NSString *urlString = [NSString stringWithFormat:@"%@events", HUMRootURL]; 37 | NSURL *url = [NSURL URLWithString:urlString]; 38 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:url]; 39 | [request setHTTPMethod:@"POST"]; 40 | 41 | NSURLSessionUploadTask *task = [self.session uploadTaskWithRequest:request 42 | fromData:JSONdata 43 | completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 44 | 45 | // See the section 'Handle the Response' 46 | 47 | }]; 48 | [task resume]; 49 | } 50 | 51 | This POST /events method is slightly different from the POST /users method we created before. 52 | 53 | 1. We need to serialize a JSON dictionary of required event information into data so we can set that as the POST request data. This is why we created the method `[event JSONDictionary]` on our event object. 54 | 55 | 2. We don't need to change the headers at all. When we call `-createEvent:withCompletionBlock:`, we have already set the headers to include the current user's ID with `-createCurrentUserWithCompletionBlock:`. 56 | 57 | ### Handle the Response 58 | 59 | Now that we've serialized the event's JSON dictionary, created a POST request, and created a task to handle that request, we can fill in the completion block. Replace the error log in `task`'s completion block with the following: 60 | 61 | // HUMRailsClient.m 62 | 63 | NSString *eventID; 64 | 65 | if (!error) { 66 | NSDictionary *responseDictionary =[NSJSONSerialization 67 | JSONObjectWithData:data 68 | options:kNilOptions 69 | error:nil]; 70 | eventID = [NSString stringWithFormat:@"%@", 71 | responseDictionary[@"id"]]; 72 | } 73 | 74 | dispatch_async(dispatch_get_main_queue(), ^{ 75 | block(eventID, error); 76 | }); 77 | 78 | If the task completes without an error, we can serialize the data we receive into a dictionary and set the event's ID from that response dictionary. 79 | 80 | If the the task completes with an error, `eventID` will remain nil. 81 | 82 | Either way, we want to execute the completion block with whatever the `eventID` and the `error` are. We also need to execute the completion block on the main queue, since the block will be updating the UI. 83 | 84 | -------------------------------------------------------------------------------- /book/iOS/user_object.md: -------------------------------------------------------------------------------- 1 | # The User Object 2 | 3 | Our app doesn't require username/password login. Instead, we will create a user object on the app's first run and then consistantly sign our requests as this user. This behavior is useful for apps that don't require login, or have some sort of guest mode. 4 | 5 | The user entity on the database has two relevant properties: `device_token` and `id`. We will pass along the device token with our user requests, and we will use the ID to compare users. 6 | 7 | ### Creating the User Session Object 8 | 9 | When we make a POST request to /users, the backend confirms that we sent the correct app secret, creates a new user with the device_token we pass, and returns the account's ID and token. 10 | 11 | Create a subclass of `NSObject` called `HUMUserSession`. This object will manage the current user's session. That means it will be responsible for keeping track of one user ID and one `device_token` that we'll be signing our requests with. 12 | 13 | The interface for our user session manager should contain 5 class methods: 14 | 15 | // HUMUserSession.h 16 | 17 | @class HUMUser; 18 | 19 | @interface HUMUserSession : NSObject 20 | 21 | + (NSString *)userID; 22 | + (NSString *)userToken; 23 | + (void)setUserID:(NSNumber *)userID; 24 | + (void)setUserToken:(NSString *)userToken; 25 | + (BOOL)userIsLoggedIn; 26 | 27 | @end 28 | 29 | The first four class methods are for getting and setting the current user's ID and token. These methods will access the keychain to keep track of this information. We want to use the keychain whenever we are storing sensitive information, like the user's token. 30 | 31 | Since we're using SSKeychain, we'll want to create a few static strings above our `@implementation`. Don't forget to `#import ` at the top of the file as well. 32 | 33 | // HUMUserSession.m 34 | 35 | static NSString *const HUMService = @"Humon"; 36 | static NSString *const HUMUserID = @"currentUserID"; 37 | static NSString *const HUMUserToken = @"currentUserToken"; 38 | 39 | Now we can use these strings as keys when querying the keychain for our `userID` and `userToken`. 40 | 41 | // HUMUserSession.m 42 | 43 | + (NSString *)userID 44 | { 45 | NSString *userID = [SSKeychain passwordForService:HUMService 46 | account:HUMUserID]; 47 | 48 | return userID; 49 | } 50 | 51 | + (NSString *)userToken 52 | { 53 | NSString *userToken = [SSKeychain passwordForService:HUMService 54 | account:HUMUserToken]; 55 | 56 | return userToken; 57 | } 58 | 59 | Next we'll want to implement the methods we defined for setting our ID and token. 60 | 61 | // HUMUserSession.m 62 | 63 | + (void)setUserID:(NSNumber *)userID 64 | { 65 | if (!userID) { 66 | [SSKeychain deletePasswordForService:HUMService account:HUMUserID]; 67 | return; 68 | } 69 | 70 | NSString *IDString = [NSString stringWithFormat:@"%@", userID]; 71 | [SSKeychain setPassword:IDString 72 | forService:HUMService 73 | account:HUMUserID 74 | error:nil]; 75 | } 76 | 77 | + (void)setUserToken:(NSString *)userToken 78 | { 79 | if (!userToken) { 80 | [SSKeychain deletePasswordForService:HUMService account:HUMUserToken]; 81 | return; 82 | } 83 | 84 | [SSKeychain setPassword:userToken 85 | forService:HUMService 86 | account:HUMUserToken 87 | error:nil]; 88 | } 89 | 90 | You'll notice that we created an `IDString` with `[NSString stringWithFormat:@"%@", userID]`. This is because our `userID` returned from the API is a number, while we need a string `IDString` password to store in the keychain. 91 | 92 | Finally, we need to implement the method that we will use in our client singleton to determine if we currently have a valid user session. It's easiest to think of this as whether the user is logged in. 93 | 94 | // HUMUserSession.m 95 | 96 | + (BOOL)userIsLoggedIn 97 | { 98 | BOOL hasUserID = [self userID] ? YES : NO; 99 | BOOL hasUserToken = [self userToken] ? YES : NO; 100 | return hasUserID && hasUserToken; 101 | } 102 | 103 | Now we can use this `userIsLoggedIn` method to determine if we need to make a POST to users, and to determine what headers we need in our API client. 104 | -------------------------------------------------------------------------------- /book/iOS/viewing_with_the_event_view_controller.md: -------------------------------------------------------------------------------- 1 | # Viewing Individual Events 2 | 3 | ### Showing a Callout for an Event 4 | 5 | Since we want to place event objects as pins on a `MKMapView`, we need to make sure our `HUMEvent` class conforms to the `` protocol. We already declared the required property, `coordinate`, which corresponds to where the pin is placed. Now we can set the text in the pin's callout view. 6 | 7 | Check the `HUMEvent` `@interface` to confirm that it conforms to this protocol. 8 | 9 | // HUMEvent.h 10 | 11 | @import MapKit; 12 | 13 | @interface HUMEvent : NSObject 14 | 15 | Since we already declared a `coordinate` property, we just need to add the `title` and `subtitle`. 16 | 17 | // HUMEvent.h 18 | 19 | // Properties used for placing the event on a map 20 | @property (copy, nonatomic) NSString *title; 21 | @property (copy, nonatomic) NSString *subtitle; 22 | @property (assign, nonatomic) CLLocationCoordinate2D coordinate; 23 | 24 | We want the title and subtitle in an event's callout view to display the event's name and address, so overwrite the getter methods for `-title` and `-subtitle`. 25 | 26 | // HUMEvent.m 27 | 28 | - (NSString *)title 29 | { 30 | return self.name; 31 | } 32 | 33 | - (NSString *)subtitle 34 | { 35 | return self.address; 36 | } 37 | 38 | Now, once we place our event objects on our `mapView`, they'll display their event information when we tap on the annotation. 39 | 40 | ### Pushing an Event View Controller 41 | 42 | In addition, we want to present a read-only `HUMEventViewController` when we tap on a annotation. When we hit the back button, we'll still see the annotation callout reminding us which event we tapped. 43 | 44 | Presenting the `HUMEventViewController` means responding to the mapView delegate method `-mapView:didSelectAnnotationView:`. As long as the annotation is a `HUMEvent` object, we want to push a new view controller with that object. 45 | 46 | // HUMMapViewController.m 47 | 48 | - (void)mapView:(MKMapView *)mapView 49 | didSelectAnnotationView:(MKAnnotationView *)view 50 | { 51 | if ([view.annotation isKindOfClass:[HUMEvent class]]) { 52 | HUMEvent *event = view.annotation; 53 | [self.navigationController pushViewController: 54 | [[HUMEventViewController alloc] initWithEvent:event] 55 | animated:YES]; 56 | 57 | } 58 | } 59 | 60 | ### Removing the Submit Button when Viewing 61 | 62 | In order to prevent users from interacting with the "Submit" button on the `HUMEventViewController`, we simply need to change the method that determines the number of rows in the table view. If our view controller is meant to be editable, return the number of cells that includes the "Submit" button. Otherwise, return one less than that. 63 | 64 | // HUMEventViewController.m 65 | 66 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 67 | if (self.editable) { 68 | return HUMEventCellCount; 69 | } else { 70 | return HUMEventCellCount - 1; 71 | } 72 | } 73 | 74 | Now you should be able to tap on an annotation and see the appropriate read-only view. 75 | -------------------------------------------------------------------------------- /book/images/cover.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/book/images/cover.pdf -------------------------------------------------------------------------------- /book/images/humon-database-representation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/book/images/humon-database-representation.png -------------------------------------------------------------------------------- /book/images/ios_app_skeleton_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/book/images/ios_app_skeleton_1.png -------------------------------------------------------------------------------- /book/images/ios_app_skeleton_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/book/images/ios_app_skeleton_2.png -------------------------------------------------------------------------------- /book/images/ios_app_skeleton_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/book/images/ios_app_skeleton_3.png -------------------------------------------------------------------------------- /book/images/ios_event_object_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/book/images/ios_event_object_1.png -------------------------------------------------------------------------------- /book/images/ios_new_xcode_project_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/book/images/ios_new_xcode_project_1.png -------------------------------------------------------------------------------- /book/images/ios_new_xcode_project_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/book/images/ios_new_xcode_project_2.png -------------------------------------------------------------------------------- /book/images/ios_new_xcode_project_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/book/images/ios_new_xcode_project_3.png -------------------------------------------------------------------------------- /book/introduction/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ### Why this book? 4 | 5 | There are many ways to build the backend for an iOS application but you only 6 | need one. And depending on the complexity of the API you are going to create, 7 | different solutions work best for different applications. 8 | 9 | Just as Rails makes it possible to set up a basic web application in a matter of 10 | minutes, Rails makes it possible to set up a basic API in a matter of minutes. 11 | But deciding how to structure your API isn't easy. While experimenting with all 12 | the options is a fun weekend project, sometimes you just want to get going. 13 | This book will help you do just that. While your API will no doubt require some 14 | tweaking while you flesh out your iOS app, the approach we will be taking is to 15 | define and build the API first, and then consume this API through our iOS app. 16 | 17 | The Rails portions of *iOS on Rails* will guide you through what we have found 18 | to be a robust, clean, flexible way of building out a JSON API with Rails. We 19 | provide code samples for GET, POST, and PATCH requests. In addition, we will 20 | explore some of the alternative approaches that we didn't choose and explain 21 | why we made the choices that we did. 22 | 23 | The iOS portion of the book will then walk, step-by-step, through creating an 24 | iOS application that works with the Rails API you just created. The iOS 25 | application will use each endpoint to post up objects and get back necessary 26 | data for the user. Our model objects in the iOS app will correspond with the 27 | model objects in the database, and be populated with response data from the 28 | API. 29 | 30 | ### Who is this book for? 31 | 32 | This book is for a developer who wants to build an iOS application with a Rails 33 | backend. It's also a book for both a Rails developer and an iOS developer to 34 | share and use in concert. This will permit them to create an app quickly and 35 | with more flexibility to change it than a backend-as-a-service provider like 36 | StackMob or Parse. 37 | 38 | The approach shared in this book is the result of our own experiments as Rails 39 | and iOS developers working together to build an application. The Rails portions 40 | of this book assume a basic working knowledge of how to build a web application 41 | with Rails as well as familiarity with the Ruby programming language. The iOS 42 | portions of this book assume experience with object oriented programming and a 43 | basic familiarity with the Objective-C programming language. 44 | 45 | This book is intended to be used as a guide rather than a recipe. While our aim 46 | is to give you all the tools necessary to build great Rails APIs and iOS 47 | clients, it does not cover the fundamentals of Ruby, Rails or Objective-C. That 48 | being said, if any part of the book strikes you as incomplete or confusing, we 49 | are always happy to receive pull requests and issue submissions on 50 | [GitHub](https://github.com/thoughtbot/ios-on-rails). 51 | -------------------------------------------------------------------------------- /book/sample.md: -------------------------------------------------------------------------------- 1 | % iOS on Rails 2 | 3 | \clearpage 4 | 5 | ## About this book 6 | 7 | Welcome to the iOS on Rails eBook sample. This is published directly from the 8 | book, so that you can get a sense for the content, style, and delivery of the 9 | product. We've included three sample sections. One is specific to Rails and 10 | shows how to handle GET requests. The last two are iOS specific, and cover 11 | creating your API client from scratch or with AFNetworking. 12 | 13 | If you enjoy the sample, you can get access to the entire book and sample 14 | application at: 15 | 16 | 17 | 18 | The eBook covers intermediate to advanced topics on creating iOS client and Ruby 19 | on Rails server applications. 20 | 21 | In addition to the book (in HTML, PDF, EPUB, and Kindle formats), you also get 22 | two complete example applications, an iOS and a Rails app. 23 | 24 | The book is written using Markdown and pandoc, and hosted on GitHub. You get 25 | access to all this. You can also use the GitHub comment and issue features to 26 | give us feedback about what we’ve written and what you’d like to see. 27 | 28 | ## Contact us 29 | 30 | If you have any questions, or just want to get in touch, drop us a line at 31 | [books@thoughtbot.com](mailto:books@thoughtbot.com). 32 | 33 | \mainmatter 34 | 35 | <<[introduction/introduction.md] 36 | 37 | \part{Building the Humon Rails App} 38 | 39 | <<[rails/creating_a_get_request.md] 40 | 41 | \part{Building the Humon iOS App} 42 | 43 | <<[iOS/api_client.md] 44 | 45 | # Closing 46 | 47 | Thanks for checking out the sample of our iOS on Rails eBook. If you’d 48 | like to get access to the full content, the example applications, ongoing 49 | updates, you can pick it up on our website: 50 | 51 | 52 | -------------------------------------------------------------------------------- /example_apps/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/.gitkeep -------------------------------------------------------------------------------- /example_apps/iOS/Humon.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon.xcodeproj/project.xcworkspace/xcuserdata/apprentice.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/iOS/Humon.xcodeproj/project.xcworkspace/xcuserdata/apprentice.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /example_apps/iOS/Humon.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface AppDelegate : UIResponder 4 | 5 | @property (strong, nonatomic) UIWindow *window; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import "HUMMapViewController.h" 3 | #import "HUMAppearanceManager.h" 4 | 5 | @interface AppDelegate () 6 | 7 | @end 8 | 9 | @implementation AppDelegate 10 | 11 | - (BOOL)application:(UIApplication *)application 12 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 13 | { 14 | [HUMAppearanceManager setupDefaultAppearances]; 15 | 16 | self.window = [[UIWindow alloc] initWithFrame: 17 | [[UIScreen mainScreen] bounds]]; 18 | 19 | HUMMapViewController *mapViewController = 20 | [[HUMMapViewController alloc] init]; 21 | UINavigationController *navigationController = 22 | [[UINavigationController alloc] 23 | initWithRootViewController:mapViewController]; 24 | 25 | self.window.rootViewController = navigationController; 26 | [self.window makeKeyAndVisible]; 27 | 28 | return YES; 29 | } 30 | 31 | - (void)applicationWillResignActive:(UIApplication *)application { 32 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 33 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 34 | } 35 | 36 | - (void)applicationDidEnterBackground:(UIApplication *)application { 37 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 38 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 39 | } 40 | 41 | - (void)applicationWillEnterForeground:(UIApplication *)application { 42 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 43 | } 44 | 45 | - (void)applicationDidBecomeActive:(UIApplication *)application { 46 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 47 | } 48 | 49 | - (void)applicationWillTerminate:(UIApplication *)application { 50 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 51 | } 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "size" : "60x60", 25 | "idiom" : "iphone", 26 | "filename" : "HUMIcon@2x.png", 27 | "scale" : "2x" 28 | }, 29 | { 30 | "idiom" : "iphone", 31 | "size" : "60x60", 32 | "scale" : "3x" 33 | } 34 | ], 35 | "info" : { 36 | "version" : 1, 37 | "author" : "xcode" 38 | } 39 | } -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/AppIcon.appiconset/HUMIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/iOS/Humon/Assets.xcassets/AppIcon.appiconset/HUMIcon@2x.png -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMButtonQuestion.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "HUMButtonQuestion@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMButtonQuestion.imageset/HUMButtonQuestion@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/iOS/Humon/Assets.xcassets/HUMButtonQuestion.imageset/HUMButtonQuestion@2x.png -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMChevronBack.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "HUMChevronBack@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMChevronBack.imageset/HUMChevronBack@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/iOS/Humon/Assets.xcassets/HUMChevronBack.imageset/HUMChevronBack@2x.png -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMChevronForward.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "HUMChevronForward@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMChevronForward.imageset/HUMChevronForward@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/iOS/Humon/Assets.xcassets/HUMChevronForward.imageset/HUMChevronForward@2x.png -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkLarge.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "HUMPlacemarkLarge@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkLarge.imageset/HUMPlacemarkLarge@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkLarge.imageset/HUMPlacemarkLarge@2x.png -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkLargeTalking.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "HUMPlacemarkLargeTalking@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkLargeTalking.imageset/HUMPlacemarkLargeTalking@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkLargeTalking.imageset/HUMPlacemarkLargeTalking@2x.png -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkSmall.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "HUMPlacemarkSmall@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkSmall.imageset/HUMPlacemarkSmall@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkSmall.imageset/HUMPlacemarkSmall@2x.png -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkSmallGrey.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "HUMPlacemarkSmallGrey@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkSmallGrey.imageset/HUMPlacemarkSmallGrey@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkSmallGrey.imageset/HUMPlacemarkSmallGrey@2x.png -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkSmallGreyTalking.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "HUMPlacemarkSmallGreyTalking@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkSmallGreyTalking.imageset/HUMPlacemarkSmallGreyTalking@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkSmallGreyTalking.imageset/HUMPlacemarkSmallGreyTalking@2x.png -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkSmallTalking.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "HUMPlacemarkSmallTalking@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkSmallTalking.imageset/HUMPlacemarkSmallTalking@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/iOS/Humon/Assets.xcassets/HUMPlacemarkSmallTalking.imageset/HUMPlacemarkSmallTalking@2x.png -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMSuccessImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "HUMSuccessImage.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Assets.xcassets/HUMSuccessImage.imageset/HUMSuccessImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/iOS/Humon/Assets.xcassets/HUMSuccessImage.imageset/HUMSuccessImage.png -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Categories/NSDateFormatter+HUMDefaultDateFormatter.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @interface NSDateFormatter (HUMDefaultDateFormatter) 4 | 5 | + (instancetype)hum_RFC3339DateFormatter; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Categories/NSDateFormatter+HUMDefaultDateFormatter.m: -------------------------------------------------------------------------------- 1 | #import "NSDateFormatter+HUMDefaultDateFormatter.h" 2 | 3 | @implementation NSDateFormatter (HUMDefaultDateFormatter) 4 | 5 | + (instancetype)hum_RFC3339DateFormatter 6 | { 7 | static NSDateFormatter *dateFormatter = nil; 8 | 9 | static dispatch_once_t onceToken; 10 | dispatch_once(&onceToken, ^{ 11 | dateFormatter = [[NSDateFormatter alloc] init]; 12 | NSLocale *enUSPOSIXLocale = [[NSLocale alloc] 13 | initWithLocaleIdentifier:@"en_US_POSIX"]; 14 | 15 | [dateFormatter setLocale:enUSPOSIXLocale]; 16 | [dateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'"]; 17 | [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; 18 | }); 19 | 20 | return dateFormatter; 21 | } 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Categories/NSObject+HUMNullCheck.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @interface NSObject (HUMNullCheck) 4 | 5 | - (BOOL)hum_isNotNull; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Categories/NSObject+HUMNullCheck.m: -------------------------------------------------------------------------------- 1 | #import "NSObject+HUMNullCheck.h" 2 | 3 | @implementation NSObject (HUMNullCheck) 4 | 5 | - (BOOL)hum_isNotNull 6 | { 7 | if ([self isKindOfClass:[NSNull class]]) { 8 | return NO; 9 | } else { 10 | return YES; 11 | } 12 | } 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Categories/UIImage+HUMColorImage.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface UIImage (HUMColorImage) 4 | 5 | + (UIImage *)hum_imageOfSize:(CGSize)size color:(UIColor *)color; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Categories/UIImage+HUMColorImage.m: -------------------------------------------------------------------------------- 1 | #import "UIImage+HUMColorImage.h" 2 | 3 | @implementation UIImage (HUMColorImage) 4 | 5 | + (UIImage *)hum_imageOfSize:(CGSize)size color:(UIColor *)color 6 | { 7 | UIGraphicsBeginImageContextWithOptions(size, YES, 0.0); 8 | 9 | CGContextRef context = UIGraphicsGetCurrentContext(); 10 | CGContextSetFillColorWithColor(context, [color CGColor]); 11 | CGContextFillRect(context, CGRectMake(0.0, 0.0, size.width, size.height)); 12 | 13 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 14 | 15 | UIGraphicsEndImageContext(); 16 | 17 | return image; 18 | } 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Cells/HUMNameCell.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | static NSString *kNameCellID = @"kNameCellID"; 4 | 5 | @interface HUMNameCell : UITableViewCell 6 | 7 | @property (strong, nonatomic) UITextField *textField; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Cells/HUMNameCell.m: -------------------------------------------------------------------------------- 1 | #import "HUMNameCell.h" 2 | 3 | @interface HUMNameCell () 4 | 5 | 6 | @end 7 | 8 | @implementation HUMNameCell 9 | 10 | - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 11 | { 12 | self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier]; 13 | if (!self) { 14 | return nil; 15 | } 16 | 17 | [self setupTextField]; 18 | self.selectionStyle = UITableViewCellSelectionStyleNone; 19 | 20 | return self; 21 | } 22 | 23 | - (void)setupTextField 24 | { 25 | UITextField *textField = [[UITextField alloc] init]; 26 | textField.translatesAutoresizingMaskIntoConstraints = NO; 27 | [textField setPlaceholder:NSLocalizedString(@"Name your event", nil)]; 28 | [self.contentView addSubview:textField]; 29 | 30 | [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:textField attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeHeight multiplier:1.0 constant:0.0]]; 31 | [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:textField attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:-8.0]]; 32 | 33 | [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:textField attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:4.0]]; 34 | [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:textField attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]]; 35 | 36 | _textField = textField; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Cells/HUMTextFieldCell.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | static NSString *kTextFieldCellID = @"kTextFieldCellID"; 4 | 5 | @interface HUMTextFieldCell : UITableViewCell 6 | 7 | @property (strong, nonatomic) UITextField *textField; 8 | 9 | - (void)setDate:(NSDate *)date; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Cells/HUMTextFieldCell.m: -------------------------------------------------------------------------------- 1 | #import "HUMTextFieldCell.h" 2 | #import "HUMAppearanceManager.h" 3 | #import "NSDateFormatter+HUMDefaultDateFormatter.h" 4 | 5 | @interface HUMTextFieldCell () 6 | 7 | 8 | @end 9 | 10 | @implementation HUMTextFieldCell 11 | 12 | - (id)initWithStyle:(UITableViewCellStyle)style 13 | reuseIdentifier:(NSString *)reuseIdentifier 14 | { 15 | self = [super initWithStyle:UITableViewCellStyleDefault 16 | reuseIdentifier:reuseIdentifier]; 17 | if (!self) { 18 | return nil; 19 | } 20 | 21 | [self setupTextField]; 22 | self.selectionStyle = UITableViewCellSelectionStyleNone; 23 | self.backgroundColor = [HUMAppearanceManager humonWhite]; 24 | 25 | return self; 26 | } 27 | 28 | - (void)setupTextField 29 | { 30 | UITextField *textField = [[UITextField alloc] init]; 31 | textField.translatesAutoresizingMaskIntoConstraints = NO; 32 | 33 | textField.placeholder = NSLocalizedString(@"...", nil); 34 | textField.delegate = self; 35 | 36 | [self.contentView addSubview:textField]; 37 | [self.contentView addConstraint:[NSLayoutConstraint 38 | constraintWithItem:textField 39 | attribute:NSLayoutAttributeHeight 40 | relatedBy:NSLayoutRelationEqual 41 | toItem:self.contentView 42 | attribute:NSLayoutAttributeHeight 43 | multiplier:1.0 44 | constant:0.0]]; 45 | [self.contentView addConstraint:[NSLayoutConstraint 46 | constraintWithItem:textField 47 | attribute:NSLayoutAttributeWidth 48 | relatedBy:NSLayoutRelationEqual 49 | toItem:self.contentView 50 | attribute:NSLayoutAttributeWidth 51 | multiplier:1.0 52 | constant:-18.0]]; 53 | 54 | [self.contentView addConstraint:[NSLayoutConstraint 55 | constraintWithItem:textField 56 | attribute:NSLayoutAttributeLeft 57 | relatedBy:NSLayoutRelationEqual 58 | toItem:self.contentView 59 | attribute:NSLayoutAttributeLeft 60 | multiplier:1.0 61 | constant:14.0]]; 62 | [self.contentView addConstraint:[NSLayoutConstraint 63 | constraintWithItem:textField 64 | attribute:NSLayoutAttributeCenterY 65 | relatedBy:NSLayoutRelationEqual 66 | toItem:self.contentView 67 | attribute:NSLayoutAttributeCenterY 68 | multiplier:1.0 69 | constant:0.0]]; 70 | 71 | _textField = textField; 72 | } 73 | 74 | - (BOOL)textFieldShouldReturn:(UITextField *)textField 75 | { 76 | [textField resignFirstResponder]; 77 | 78 | return YES; 79 | } 80 | 81 | #pragma mark - Text field cell as a date picker methods 82 | 83 | - (void)setDate:(NSDate *)date 84 | { 85 | UIDatePicker *picker = [[UIDatePicker alloc] init]; 86 | [picker addTarget:self 87 | action:@selector(changeTextField:) 88 | forControlEvents:UIControlEventValueChanged]; 89 | [picker setDate:date]; 90 | 91 | self.textField.inputView = picker; 92 | } 93 | 94 | - (void)changeTextField:(UIDatePicker *)picker 95 | { 96 | self.textField.text = [[NSDateFormatter hum_RFC3339DateFormatter] 97 | stringFromDate:picker.date]; 98 | } 99 | 100 | @end 101 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Cells/HUMTimeCell.h: -------------------------------------------------------------------------------- 1 | #import "HUMTextFieldCell.h" 2 | 3 | static NSString *kTimeCellID = @"kTimeCellID"; 4 | 5 | @interface HUMTimeCell : HUMTextFieldCell 6 | 7 | @property (strong, nonatomic) NSDate *date; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Cells/HUMTimeCell.m: -------------------------------------------------------------------------------- 1 | #import "HUMTimeCell.h" 2 | 3 | @implementation HUMTimeCell 4 | 5 | - (id)initWithStyle:(UITableViewCellStyle)style 6 | reuseIdentifier:(NSString *)reuseIdentifier 7 | { 8 | self = [super initWithStyle:UITableViewCellStyleValue1 9 | reuseIdentifier:reuseIdentifier]; 10 | if (!self) { 11 | return nil; 12 | } 13 | 14 | self.textField.placeholder = NSLocalizedString(@"Pick a date", nil); 15 | self.textField.userInteractionEnabled = NO; 16 | 17 | return self; 18 | } 19 | 20 | - (void)setDate:(NSDate *)date 21 | { 22 | if (!date) { 23 | return; 24 | } 25 | 26 | _date = date; 27 | 28 | self.textField.text = [NSDateFormatter 29 | localizedStringFromDate:date 30 | dateStyle:NSDateFormatterShortStyle 31 | timeStyle:NSDateFormatterShortStyle]; 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Cells/HUMTimePickerCell.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | static NSString *kTimePickerCellID = @"kTimePickerCellID"; 4 | static NSInteger kTimePickerCellHeight = 216; 5 | 6 | @interface HUMTimePickerCell : UITableViewCell 7 | 8 | @property (strong, nonatomic) UIDatePicker *datePicker; 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Cells/HUMTimePickerCell.m: -------------------------------------------------------------------------------- 1 | #import "HUMTimePickerCell.h" 2 | 3 | @implementation HUMTimePickerCell 4 | 5 | - (id)initWithStyle:(UITableViewCellStyle)style 6 | reuseIdentifier:(NSString *)reuseIdentifier 7 | { 8 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 9 | if (self) { 10 | _datePicker = [[UIDatePicker alloc] initWithFrame:self.frame]; 11 | [self.contentView addSubview:_datePicker]; 12 | } 13 | return self; 14 | } 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Clients/HUMRailsAFNClient.h: -------------------------------------------------------------------------------- 1 | #import "AFHTTPSessionManager.h" 2 | @class HUMEvent; 3 | @import MapKit; 4 | 5 | typedef void(^HUMRailsAFNClientErrorCompletionBlock)(NSError *error); 6 | typedef void(^HUMRailsAFNClientEventIDCompletionBlock)(NSString *eventID, NSError *error); 7 | typedef void(^HUMRailsAFNClientEventsCompletionBlock)(NSArray *events, NSError *error); 8 | 9 | @interface HUMRailsAFNClient : AFHTTPSessionManager 10 | 11 | + (instancetype)sharedClient; 12 | - (void)createCurrentUserWithCompletionBlock: 13 | (HUMRailsAFNClientErrorCompletionBlock)block; 14 | - (void)createEvent:(HUMEvent *)event 15 | withCompletionBlock:(HUMRailsAFNClientEventIDCompletionBlock)block; 16 | - (void)changeEvent:(HUMEvent *)event 17 | withCompletionBlock:(HUMRailsAFNClientEventIDCompletionBlock)block; 18 | - (NSURLSessionDataTask *)fetchEventsInRegion:(MKCoordinateRegion)region 19 | withCompletionBlock:(HUMRailsAFNClientEventsCompletionBlock)block; 20 | - (void)createAttendanceForEvent:(HUMEvent *)event 21 | withCompletionBlock:(HUMRailsAFNClientErrorCompletionBlock)block; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Clients/HUMRailsAFNClient.m: -------------------------------------------------------------------------------- 1 | #import "HUMEvent.h" 2 | #import "HUMRailsAFNClient.h" 3 | #import "HUMUserSession.h" 4 | 5 | static NSString *const HUMAFNAppSecret = 6 | @"yourOwnUniqueAppSecretThatYouShouldRandomlyGenerateAndKeepSecret"; 7 | static NSString *const HUMRootURL = @"https://humon-staging.herokuapp.com/v1/"; 8 | 9 | @implementation HUMRailsAFNClient 10 | 11 | + (instancetype)sharedClient 12 | { 13 | static HUMRailsAFNClient *_sharedClient = nil; 14 | static dispatch_once_t onceToken; 15 | dispatch_once(&onceToken, ^{ 16 | 17 | NSURL *baseURL = [NSURL URLWithString:HUMRootURL]; 18 | _sharedClient = [[HUMRailsAFNClient alloc] initWithBaseURL:baseURL]; 19 | 20 | if ([HUMUserSession userIsLoggedIn]) { 21 | [_sharedClient.requestSerializer setValue:[HUMUserSession userToken] 22 | forHTTPHeaderField:@"tb-auth-token"]; 23 | } else { 24 | [_sharedClient.requestSerializer setValue:HUMAFNAppSecret 25 | forHTTPHeaderField:@"tb-app-secret"]; 26 | } 27 | 28 | }); 29 | 30 | return _sharedClient; 31 | } 32 | 33 | - (void)createCurrentUserWithCompletionBlock: 34 | (HUMRailsAFNClientErrorCompletionBlock)block 35 | { 36 | [self POST:@"users" parameters:nil 37 | success:^(NSURLSessionDataTask *task, id responseObject) { 38 | 39 | [HUMUserSession setUserID:responseObject[@"id"]]; 40 | [HUMUserSession setUserToken:responseObject[@"auth_token"]]; 41 | [self.requestSerializer setValue:responseObject[@"auth_token"] 42 | forHTTPHeaderField:@"tb-auth-token"]; 43 | block(nil); 44 | 45 | } failure:^(NSURLSessionDataTask *task, NSError *error) { 46 | 47 | block(error); 48 | 49 | }]; 50 | } 51 | 52 | - (void)createEvent:(HUMEvent *)event 53 | withCompletionBlock:(HUMRailsAFNClientEventIDCompletionBlock)block 54 | { 55 | [self POST:@"events" 56 | parameters:[event JSONDictionary] 57 | success:^(NSURLSessionDataTask *task, id responseObject) { 58 | 59 | NSString *eventID = [NSString stringWithFormat:@"%@", 60 | responseObject[@"id"]]; 61 | block(eventID, nil); 62 | 63 | } failure:^(NSURLSessionDataTask *task, NSError *error) { 64 | 65 | block(nil, error); 66 | 67 | }]; 68 | } 69 | 70 | - (void)changeEvent:(HUMEvent *)event 71 | withCompletionBlock:(HUMRailsAFNClientEventIDCompletionBlock)block 72 | { 73 | NSString *path = [NSString stringWithFormat:@"events/%@", event.eventID]; 74 | 75 | [self PATCH:path 76 | parameters:[event JSONDictionary] 77 | success:^(NSURLSessionDataTask *task, id responseObject) { 78 | 79 | NSString *eventID = [NSString stringWithFormat:@"%@", 80 | responseObject[@"id"]]; 81 | block(eventID, nil); 82 | 83 | } failure:^(NSURLSessionDataTask *task, NSError *error) { 84 | 85 | block(nil, error); 86 | 87 | }]; 88 | } 89 | 90 | - (NSURLSessionDataTask *)fetchEventsInRegion:(MKCoordinateRegion)region 91 | withCompletionBlock:(HUMRailsAFNClientEventsCompletionBlock)block 92 | { 93 | // region.span.latitudeDelta/2*111 is how we find the aproximate radius 94 | // that the screen is displaying in km. 95 | NSDictionary *parameters = @{ 96 | @"lat" : @(region.center.latitude), 97 | @"lon" : @(region.center.longitude), 98 | @"radius" : @(region.span.latitudeDelta/2*111) 99 | }; 100 | 101 | return [self GET:@"events/nearests" 102 | parameters:parameters 103 | success:^(NSURLSessionDataTask *task, id responseObject) { 104 | 105 | NSArray *events; 106 | if ([responseObject isKindOfClass:[NSArray class]]) { 107 | events = [HUMEvent eventsWithJSON:responseObject]; 108 | } 109 | block(events, nil); 110 | 111 | } failure:^(NSURLSessionDataTask *task, NSError *error) { 112 | 113 | block(nil, error); 114 | 115 | }]; 116 | } 117 | 118 | - (void)createAttendanceForEvent:(HUMEvent *)event 119 | withCompletionBlock:(HUMRailsAFNClientErrorCompletionBlock)block 120 | { 121 | NSDictionary *parameters = @{@"event" : @{@"id" : event.eventID}}; 122 | [self POST:@"attendances" 123 | parameters:parameters 124 | success:^(NSURLSessionDataTask *task, id responseObject) { 125 | block(nil); 126 | } failure:^(NSURLSessionDataTask *task, NSError *error) { 127 | block(error); 128 | }]; 129 | } 130 | 131 | @end 132 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Clients/HUMRailsClient.h: -------------------------------------------------------------------------------- 1 | @class HUMEvent, HUMEvent; 2 | @import MapKit; 3 | 4 | typedef void(^HUMRailsClientErrorCompletionBlock)(NSError *error); 5 | typedef void(^HUMRailsClientEventIDCompletionBlock)(NSString *eventID, NSError *error); 6 | typedef void(^HUMRailsClientEventsCompletionBlock)(NSArray *events, NSError *error); 7 | 8 | @interface HUMRailsClient : NSObject 9 | 10 | + (instancetype)sharedClient; 11 | - (void)createCurrentUserWithCompletionBlock: 12 | (HUMRailsClientErrorCompletionBlock)block; 13 | - (void)createEvent:(HUMEvent *)event 14 | withCompletionBlock:(HUMRailsClientEventIDCompletionBlock)block; 15 | - (void)changeEvent:(HUMEvent *)event 16 | withCompletionBlock:(HUMRailsClientEventIDCompletionBlock)block; 17 | - (NSURLSessionDataTask *)fetchEventsInRegion:(MKCoordinateRegion)region 18 | withCompletionBlock:(HUMRailsClientEventsCompletionBlock)block; 19 | - (void)createAttendanceForEvent:(HUMEvent *)event 20 | withCompletionBlock:(HUMRailsClientErrorCompletionBlock)block; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Controllers/HUMAddEventViewController.h: -------------------------------------------------------------------------------- 1 | #import "HUMEventViewController.h" 2 | 3 | @interface HUMAddEventViewController : HUMEventViewController 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Controllers/HUMAddEventViewController.m: -------------------------------------------------------------------------------- 1 | #import "HUMAddEventViewController.h" 2 | #import "HUMConfirmationViewController.h" 3 | #import "HUMEvent.h" 4 | #import "HUMFooterView.h" 5 | #import "HUMRailsClient.h" 6 | #import "HUMTextFieldCell.h" 7 | #import 8 | @import AddressBook; 9 | @import UIKit; 10 | 11 | @interface HUMAddEventViewController () 12 | 13 | @property (strong, nonatomic) CLGeocoder *geocoder; 14 | 15 | @end 16 | 17 | @implementation HUMAddEventViewController 18 | 19 | - (instancetype)initWithEvent:(HUMEvent *)event 20 | { 21 | self = [super initWithEvent:event]; 22 | if (!self) { 23 | return nil; 24 | } 25 | 26 | _geocoder = [[CLGeocoder alloc] init]; 27 | 28 | return self; 29 | } 30 | 31 | - (void)viewDidLoad 32 | { 33 | [super viewDidLoad]; 34 | 35 | self.title = NSLocalizedString(@"New Event", nil); 36 | } 37 | 38 | - (void)viewDidAppear:(BOOL)animated 39 | { 40 | [super viewDidAppear:animated]; 41 | 42 | if (!self.event.address) { 43 | [self reverseGeocodeAddressForCoordinate:self.event.coordinate]; 44 | } 45 | } 46 | 47 | #pragma mark - Cell creation helper methods 48 | 49 | - (void)addActionToFooterButton:(HUMFooterView *)footer 50 | { 51 | [footer.button addTarget:self 52 | action:@selector(createEvent:) 53 | forControlEvents:UIControlEventTouchUpInside]; 54 | [footer.button setTitle:NSLocalizedString(@"Create", nil) 55 | forState:UIControlStateNormal]; 56 | } 57 | 58 | #pragma mark - Cell selection methods 59 | 60 | - (void)createEvent:(id)sender 61 | { 62 | if ([sender isKindOfClass:[UIButton class]]) { 63 | [sender setEnabled:NO]; 64 | } 65 | 66 | [SVProgressHUD show]; 67 | 68 | [[HUMRailsClient sharedClient] createEvent:self.event 69 | withCompletionBlock:^(NSString *eventID, NSError *error) { 70 | 71 | if (error) { 72 | NSLog(@"Event creation error: %@", error); 73 | [SVProgressHUD showErrorWithStatus: 74 | NSLocalizedString(@"Event creation error", nil)]; 75 | return; 76 | } 77 | 78 | self.event.eventID = eventID; 79 | [SVProgressHUD dismiss]; 80 | HUMConfirmationViewController *confirmationViewController = 81 | [[HUMConfirmationViewController alloc] initWithEvent:self.event]; 82 | [self.navigationController pushViewController:confirmationViewController 83 | animated:YES]; 84 | 85 | }]; 86 | } 87 | 88 | #pragma mark - Geocoding methods 89 | 90 | - (void)reverseGeocodeAddressForCoordinate:(CLLocationCoordinate2D)coordinate 91 | { 92 | __weak HUMAddEventViewController *weakSelf = self; 93 | 94 | CLLocation *location = [[CLLocation alloc] 95 | initWithLatitude:coordinate.latitude 96 | longitude:coordinate.longitude]; 97 | [self.geocoder reverseGeocodeLocation:location 98 | completionHandler:^(NSArray *placemarks,NSError *error){ 99 | 100 | if (error || !placemarks.count) { 101 | NSLog(@"Reverse geocode error: %@", error); 102 | return; 103 | } 104 | 105 | CLPlacemark *placemark = [placemarks firstObject]; 106 | NSDictionary *addressDictionary = placemark.addressDictionary; 107 | NSString *address = 108 | addressDictionary[(NSString *)kABPersonAddressStreetKey]; 109 | 110 | weakSelf.event.address = address; 111 | if (!weakSelf.addressField.text.length) { 112 | weakSelf.addressField.text = address; 113 | } 114 | }]; 115 | } 116 | 117 | @end 118 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Controllers/HUMConfirmationViewController.h: -------------------------------------------------------------------------------- 1 | @class HUMEvent; 2 | @import UIKit; 3 | 4 | @interface HUMConfirmationViewController : UIViewController 5 | 6 | - (id)initWithEvent:(HUMEvent *)event; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Controllers/HUMEditEventViewController.h: -------------------------------------------------------------------------------- 1 | #import "HUMEventViewController.h" 2 | 3 | @interface HUMEditEventViewController : HUMEventViewController 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Controllers/HUMEditEventViewController.m: -------------------------------------------------------------------------------- 1 | #import "HUMConfirmationViewController.h" 2 | #import "HUMEditEventViewController.h" 3 | #import "HUMEvent.h" 4 | #import "HUMFooterView.h" 5 | #import "HUMRailsClient.h" 6 | #import 7 | 8 | @interface HUMEditEventViewController () 9 | 10 | @end 11 | 12 | @implementation HUMEditEventViewController 13 | 14 | - (void)viewDidLoad 15 | { 16 | [super viewDidLoad]; 17 | 18 | self.title = NSLocalizedString(@"Edit Event", nil); 19 | } 20 | 21 | #pragma mark - Cell creation helper methods 22 | 23 | - (void)addActionToFooterButton:(HUMFooterView *)footer 24 | { 25 | [footer.button addTarget:self action:@selector(changeEvent:) forControlEvents:UIControlEventTouchUpInside]; 26 | [footer.button setTitle:NSLocalizedString(@"Save", nil) forState:UIControlStateNormal]; 27 | } 28 | 29 | #pragma mark - Cell selection methods 30 | 31 | - (void)changeEvent:(id)sender 32 | { 33 | if ([sender isKindOfClass:[UIButton class]]) { 34 | [sender setEnabled:NO]; 35 | } 36 | 37 | [SVProgressHUD show]; 38 | 39 | [[HUMRailsClient sharedClient] changeEvent:self.event 40 | withCompletionBlock:^(NSString *eventID, NSError *error) { 41 | 42 | if (error || ![eventID isEqualToString:self.event.eventID]) { 43 | NSLog(@"Event edit error: %@", error); 44 | [SVProgressHUD showErrorWithStatus: 45 | NSLocalizedString(@"Event edit error", nil)]; 46 | return; 47 | } 48 | 49 | [SVProgressHUD dismiss]; 50 | HUMConfirmationViewController *confirmationViewController = 51 | [[HUMConfirmationViewController alloc] initWithEvent:self.event]; 52 | [self.navigationController pushViewController:confirmationViewController 53 | animated:YES]; 54 | 55 | }]; 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Controllers/HUMEventViewController.h: -------------------------------------------------------------------------------- 1 | @class HUMEvent; 2 | @import UIKit; 3 | 4 | typedef NS_ENUM(NSUInteger, HUMEventSection) { 5 | HUMEventSectionName, 6 | HUMEventSectionAddress, 7 | HUMEventSectionStart, 8 | HUMEventSectionEnd, 9 | HUMEventSectionCount 10 | }; 11 | 12 | @interface HUMEventViewController : UITableViewController 13 | 14 | - (instancetype)initWithEvent:(HUMEvent *)event; 15 | 16 | @property (strong, nonatomic) HUMEvent *event; 17 | @property (strong, nonatomic) UITextField *nameField; 18 | @property (strong, nonatomic) UITextField *addressField; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Controllers/HUMEventViewControllerFromBook.h: -------------------------------------------------------------------------------- 1 | @class HUMEvent; 2 | @import UIKit; 3 | 4 | typedef NS_ENUM(NSUInteger, HUMEventCell) { 5 | HUMEventCellName, 6 | HUMEventCellAddress, 7 | HUMEventCellStart, 8 | HUMEventCellEnd, 9 | HUMEventCellSubmit, 10 | HUMEventCellCount 11 | }; 12 | 13 | @interface HUMEventViewControllerFromBook : UITableViewController 14 | 15 | - (instancetype)initWithEvent:(HUMEvent *)event editable:(BOOL)editable; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Controllers/HUMEventViewControllerFromBook.m: -------------------------------------------------------------------------------- 1 | #import "HUMEvent.h" 2 | #import "HUMEventViewControllerFromBook.h" 3 | #import "HUMRailsClient.h" 4 | #import "HUMTextFieldCell.h" 5 | #import 6 | 7 | @interface HUMEventViewControllerFromBook () 8 | 9 | @property (strong, nonatomic) HUMEvent *event; 10 | @property (assign, nonatomic) BOOL editable; 11 | 12 | @property (strong, nonatomic) HUMTextFieldCell *nameCell; 13 | @property (strong, nonatomic) HUMTextFieldCell *addressCell; 14 | @property (strong, nonatomic) HUMTextFieldCell *startCell; 15 | @property (strong, nonatomic) HUMTextFieldCell *endCell; 16 | 17 | @end 18 | 19 | @implementation HUMEventViewControllerFromBook 20 | 21 | - (instancetype)initWithEvent:(HUMEvent *)event editable:(BOOL)editable; 22 | { 23 | self = [super initWithStyle:UITableViewStylePlain]; 24 | if (!self) { 25 | return nil; 26 | } 27 | 28 | _event = event; 29 | _editable = editable; 30 | 31 | [self.tableView registerClass:[HUMTextFieldCell class] 32 | forCellReuseIdentifier:kTextFieldCellID]; 33 | 34 | return self; 35 | } 36 | 37 | #pragma mark - Table view data source 38 | 39 | - (NSInteger)tableView:(UITableView *)tableView 40 | numberOfRowsInSection:(NSInteger)section 41 | { 42 | if (self.editable) { 43 | return HUMEventCellCount; 44 | } else { 45 | return HUMEventCellCount - 1; 46 | } 47 | } 48 | 49 | - (UITableViewCell *)tableView:(UITableView *)tableView 50 | cellForRowAtIndexPath:(NSIndexPath *)indexPath 51 | { 52 | HUMTextFieldCell *cell = [tableView 53 | dequeueReusableCellWithIdentifier:kTextFieldCellID 54 | forIndexPath:indexPath]; 55 | cell.textField.userInteractionEnabled = self.editable; 56 | cell.textField.inputView = nil; 57 | 58 | switch (indexPath.row) { 59 | case HUMEventCellName: 60 | self.nameCell = cell; 61 | cell.textField.placeholder = NSLocalizedString(@"Name", nil); 62 | cell.textField.text = self.event.name; 63 | break; 64 | case HUMEventCellAddress: 65 | self.addressCell = cell; 66 | cell.textField.placeholder = NSLocalizedString(@"Address", nil); 67 | cell.textField.text = self.event.address; 68 | break; 69 | case HUMEventCellStart: 70 | self.startCell = cell; 71 | cell.textField.placeholder = NSLocalizedString(@"Start Date", nil); 72 | [cell setDate:self.event.startDate ?: [NSDate date]]; 73 | break; 74 | case HUMEventCellEnd: 75 | self.endCell = cell; 76 | cell.textField.placeholder = NSLocalizedString(@"End Date", nil); 77 | [cell setDate:self.event.endDate ?: [NSDate date]]; 78 | break; 79 | case HUMEventCellSubmit: 80 | cell.textField.text = NSLocalizedString(@"Submit", nil); 81 | cell.textField.userInteractionEnabled = NO; 82 | break; 83 | default: 84 | break; 85 | } 86 | 87 | return cell; 88 | } 89 | 90 | - (void)tableView:(UITableView *)tableView 91 | didSelectRowAtIndexPath:(NSIndexPath *)indexPath 92 | { 93 | if (indexPath.row != HUMEventCellSubmit) 94 | return; 95 | 96 | self.event.name = self.nameCell.textField.text; 97 | self.event.address = self.addressCell.textField.text; 98 | self.event.startDate = [(UIDatePicker *) 99 | self.startCell.textField.inputView date]; 100 | self.event.endDate = [(UIDatePicker *) 101 | self.endCell.textField.inputView date]; 102 | 103 | [SVProgressHUD show]; 104 | 105 | [[HUMRailsClient sharedClient] createEvent:self.event 106 | withCompletionBlock:^(NSString *eventID, NSError *error) { 107 | 108 | if (error) { 109 | [SVProgressHUD showErrorWithStatus: 110 | NSLocalizedString(@"Failed to create event.", nil)]; 111 | } else { 112 | [SVProgressHUD dismiss]; 113 | [self.navigationController popToRootViewControllerAnimated:YES]; 114 | } 115 | 116 | }]; 117 | } 118 | 119 | @end 120 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Controllers/HUMLocateEventViewController.h: -------------------------------------------------------------------------------- 1 | @import MapKit; 2 | @import UIKit; 3 | 4 | @interface HUMLocateEventViewController : UIViewController 5 | 6 | - (id)initWithVisibleRect:(MKMapRect)mapRect; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Controllers/HUMMapViewController.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface HUMMapViewController : UIViewController 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Controllers/HUMViewEventViewController.h: -------------------------------------------------------------------------------- 1 | #import "HUMEventViewController.h" 2 | 3 | @interface HUMViewEventViewController : HUMEventViewController 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Controllers/HUMViewEventViewController.m: -------------------------------------------------------------------------------- 1 | #import "HUMConfirmationViewController.h" 2 | #import "HUMEvent.h" 3 | #import "HUMFooterView.h" 4 | #import "HUMRailsClient.h" 5 | #import "HUMViewEventViewController.h" 6 | #import 7 | @import UIKit; 8 | 9 | @interface HUMViewEventViewController () 10 | 11 | @end 12 | 13 | @implementation HUMViewEventViewController 14 | 15 | - (void)viewDidLoad 16 | { 17 | [super viewDidLoad]; 18 | 19 | self.title = NSLocalizedString(@"View Event", nil); 20 | } 21 | 22 | #pragma mark - Cell creation helper methods 23 | 24 | - (void)addActionToFooterButton:(HUMFooterView *)footer 25 | { 26 | [footer.button addTarget:self action:@selector(joinEvent:) 27 | forControlEvents:UIControlEventTouchUpInside]; 28 | [footer.button setTitle:NSLocalizedString(@"Join", nil) 29 | forState:UIControlStateNormal]; 30 | } 31 | 32 | #pragma mark - Cell selection methods 33 | 34 | - (void)joinEvent:(id)sender 35 | { 36 | if ([sender isKindOfClass:[UIButton class]]) { 37 | [sender setEnabled:NO]; 38 | } 39 | 40 | [SVProgressHUD show]; 41 | 42 | [[HUMRailsClient sharedClient] 43 | createAttendanceForEvent:self.event 44 | withCompletionBlock:^(NSError *error) { 45 | 46 | if (error) { 47 | NSLog(@"Event join error: %@", error); 48 | [SVProgressHUD showErrorWithStatus: 49 | NSLocalizedString(@"Event join error", nil)]; 50 | return; 51 | } 52 | 53 | [SVProgressHUD dismiss]; 54 | HUMConfirmationViewController *confirmationViewController = 55 | [[HUMConfirmationViewController alloc] initWithEvent:self.event]; 56 | [self.navigationController pushViewController:confirmationViewController 57 | animated:YES]; 58 | 59 | }]; 60 | } 61 | 62 | # pragma mark - UITableViewDelegate 63 | 64 | - (void)tableView:(UITableView *)tableView 65 | didSelectRowAtIndexPath:(NSIndexPath *)indexPath 66 | { 67 | // Do nothing. 68 | } 69 | 70 | #pragma mark - UITextFieldDelegate 71 | 72 | - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField 73 | { 74 | return NO; 75 | } 76 | 77 | @end 78 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSLocationWhenInUseUsageDescription 26 | Humon needs to use your location to find events. 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIStatusBarStyle 34 | UIStatusBarStyleLightContent 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | 39 | UIViewControllerBasedStatusBarAppearance 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Models/HUMAppearanceManager.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface HUMAppearanceManager : NSObject 4 | 5 | + (UIColor *)humonGreen; 6 | + (UIColor *)humonGreenDark; 7 | + (UIColor *)humonGrey; 8 | + (UIColor *)humonGreyDark; 9 | + (UIColor *)humonBlack; 10 | + (UIColor *)humonWhite; 11 | 12 | + (void)setupDefaultAppearances; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Models/HUMAppearanceManager.m: -------------------------------------------------------------------------------- 1 | #import "HUMAppearanceManager.h" 2 | #import "UIImage+HUMColorImage.h" 3 | 4 | @implementation HUMAppearanceManager 5 | 6 | + (UIColor *)humonGreen 7 | { 8 | return [UIColor colorWithRed:0.0 green:0.7 blue:0.6 alpha:1.0]; 9 | } 10 | 11 | + (UIColor *)humonGreenDark 12 | { 13 | return [UIColor colorWithRed:0.0 green:0.4 blue:0.3 alpha:1.0]; 14 | } 15 | 16 | + (UIColor *)humonGrey 17 | { 18 | return [UIColor colorWithRed:0.97 green:0.98 blue:0.97 alpha:1.0]; 19 | } 20 | 21 | + (UIColor *)humonGreyDark 22 | { 23 | return [UIColor colorWithRed:0.32 green:0.33 blue:0.32 alpha:1.0]; 24 | } 25 | 26 | + (UIColor *)humonBlack 27 | { 28 | return [UIColor colorWithRed:0.0 green:0.1 blue:0.05 alpha:1.0]; 29 | } 30 | 31 | + (UIColor *)humonWhite 32 | { 33 | return [UIColor colorWithRed:0.98 green:1.0 blue:0.99 alpha:1.0]; 34 | } 35 | 36 | + (void)setupDefaultAppearances 37 | { 38 | UIImage *whiteImage = [UIImage hum_imageOfSize:CGSizeMake(1, 1) 39 | color:[HUMAppearanceManager humonWhite]]; 40 | UIImage *greenImage = [UIImage hum_imageOfSize:CGSizeMake(1, 1) 41 | color:[self humonGreen]]; 42 | 43 | [[UINavigationBar appearance] setBarTintColor:[self humonGreen]]; 44 | [[UINavigationBar appearance] setBackgroundImage:greenImage 45 | forBarPosition:UIBarPositionAny 46 | barMetrics:UIBarMetricsDefault]; 47 | [[UINavigationBar appearance] setShadowImage:whiteImage]; 48 | 49 | NSDictionary *textAttributes = 50 | @{ NSForegroundColorAttributeName : [HUMAppearanceManager humonWhite] }; 51 | 52 | [[UINavigationBar appearance] setTintColor:[HUMAppearanceManager humonWhite]]; 53 | [[UINavigationBar appearance] setTitleTextAttributes:textAttributes]; 54 | 55 | [[UILabel appearanceWhenContainedIn:[UITableViewHeaderFooterView class],nil] 56 | setTextColor:[HUMAppearanceManager humonGreyDark]]; 57 | 58 | [[UITextField appearance] setTextColor:[HUMAppearanceManager humonBlack]]; 59 | 60 | [[UIApplication sharedApplication] 61 | setStatusBarStyle:UIStatusBarStyleLightContent]; 62 | } 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Models/HUMEvent.h: -------------------------------------------------------------------------------- 1 | @class HUMUser; 2 | @import MapKit; 3 | @import Foundation; 4 | 5 | @interface HUMEvent : NSObject 6 | 7 | @property (copy, nonatomic) NSString *name; 8 | @property (copy, nonatomic) NSString *address; 9 | @property (strong, nonatomic) NSDate *startDate; 10 | @property (strong, nonatomic) NSDate *endDate; 11 | 12 | @property (strong, nonatomic) HUMUser *user; 13 | @property (copy, nonatomic) NSString *eventID; 14 | @property (assign, nonatomic) NSInteger attendees; 15 | 16 | @property (copy, nonatomic) NSString *title; 17 | @property (copy, nonatomic) NSString *subtitle; 18 | @property (assign, nonatomic) CLLocationCoordinate2D coordinate; 19 | 20 | + (NSArray *)eventsWithJSON:(NSArray *)JSON; 21 | - (instancetype)initWithJSON:(NSDictionary *)JSON; 22 | - (NSDictionary *)JSONDictionary; 23 | - (NSString *)humanReadableString; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Models/HUMEvent.m: -------------------------------------------------------------------------------- 1 | #import "HUMEvent.h" 2 | #import "HUMUser.h" 3 | #import "HUMUserSession.h" 4 | #import "NSDateFormatter+HUMDefaultDateFormatter.h" 5 | #import "NSObject+HUMNullCheck.h" 6 | 7 | @implementation HUMEvent 8 | 9 | + (NSArray *)eventsWithJSON:(NSArray *)JSON 10 | { 11 | NSMutableArray *events = [[NSMutableArray alloc] init]; 12 | 13 | for (NSDictionary *eventJSON in JSON) { 14 | HUMEvent *event = [[HUMEvent alloc] initWithJSON:eventJSON]; 15 | [events addObject:event]; 16 | } 17 | 18 | return [events copy]; 19 | } 20 | 21 | - (instancetype)initWithJSON:(NSDictionary *)JSON 22 | { 23 | self = [super init]; 24 | 25 | if (!self) { 26 | return nil; 27 | } 28 | 29 | _name = JSON[@"name"]; 30 | if ([JSON[@"address"] hum_isNotNull]) { 31 | _address = JSON[@"address"]; 32 | } 33 | 34 | _startDate = [[NSDateFormatter hum_RFC3339DateFormatter] 35 | dateFromString:JSON[@"started_at"]]; 36 | if ([JSON[@"ended_at"] hum_isNotNull]) { 37 | _endDate = [[NSDateFormatter hum_RFC3339DateFormatter] 38 | dateFromString:JSON[@"ended_at"]]; 39 | } 40 | 41 | double lat = [JSON[@"lat"] doubleValue]; 42 | double lon = [JSON[@"lon"] doubleValue]; 43 | _coordinate = CLLocationCoordinate2DMake(lat, lon); 44 | 45 | _user = [[HUMUser alloc] initWithJSON:JSON[@"owner"]]; 46 | _eventID = [NSString stringWithFormat:@"%@", JSON[@"id"]]; 47 | _attendees = [JSON[@"attendees"] integerValue]; 48 | 49 | return self; 50 | } 51 | 52 | - (NSDictionary *)JSONDictionary 53 | { 54 | NSMutableDictionary *JSONDictionary = [[NSMutableDictionary alloc] init]; 55 | 56 | [JSONDictionary setObject:self.name forKey:@"name"]; 57 | if (self.address) { 58 | [JSONDictionary setObject:self.address forKey:@"address"]; 59 | } 60 | 61 | [JSONDictionary setObject:@(self.coordinate.latitude) forKey:@"lat"]; 62 | [JSONDictionary setObject:@(self.coordinate.longitude) forKey:@"lon"]; 63 | 64 | if (!self.startDate) { 65 | self.startDate = [NSDate date]; 66 | } 67 | NSString *start = [[NSDateFormatter hum_RFC3339DateFormatter] 68 | stringFromDate:self.startDate]; 69 | [JSONDictionary setObject:start forKey:@"started_at"]; 70 | 71 | NSString *end = [[NSDateFormatter hum_RFC3339DateFormatter] 72 | stringFromDate:self.endDate]; 73 | if (end) { 74 | [JSONDictionary setObject:end forKey:@"ended_at"]; 75 | } 76 | 77 | if (self.eventID) { 78 | [JSONDictionary setObject:self.eventID forKey:@"id"]; 79 | } 80 | 81 | return [JSONDictionary copy]; 82 | } 83 | 84 | - (NSString *)humanReadableString 85 | { 86 | NSString *dateString = [NSDateFormatter 87 | localizedStringFromDate:self.startDate 88 | dateStyle:NSDateFormatterShortStyle 89 | timeStyle:NSDateFormatterShortStyle]; 90 | NSString *sharingString = [NSString stringWithFormat:@"%@ at %@, %@", 91 | self.name, self.address, dateString]; 92 | return sharingString; 93 | } 94 | 95 | #pragma mark - Methods for comparing events 96 | 97 | - (NSUInteger)hash 98 | { 99 | if (!self.eventID) { 100 | return [super hash]; 101 | } 102 | 103 | NSString *hashString = [NSString stringWithFormat: 104 | @"%@%@", 105 | self.eventID, 106 | self.user.userID]; 107 | return [hashString hash]; 108 | } 109 | 110 | - (BOOL)isEqual:(id)object 111 | { 112 | if (self == object) { 113 | return YES; 114 | } 115 | 116 | if (![self isKindOfClass:[object class]] || !object) { 117 | return NO; 118 | } 119 | 120 | HUMEvent *event = (HUMEvent *)object; 121 | 122 | BOOL objectsHaveSameID = 123 | [self.eventID isEqualToString:event.eventID]; 124 | BOOL objectsHaveSameUser = 125 | [self.user.userID isEqualToString:event.user.userID]; 126 | 127 | return objectsHaveSameID && objectsHaveSameUser; 128 | } 129 | 130 | #pragma mark - MKAnnotation Protocol 131 | 132 | - (NSString *)title 133 | { 134 | return self.name; 135 | } 136 | 137 | - (NSString *)subtitle 138 | { 139 | return self.address; 140 | } 141 | 142 | @end 143 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Models/HUMUser.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @interface HUMUser : NSObject 4 | 5 | @property (copy, nonatomic) NSString *userID; 6 | 7 | - (id)initWithJSON:(NSDictionary *)JSONDictionary; 8 | - (BOOL)isCurrentUser; 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Models/HUMUser.m: -------------------------------------------------------------------------------- 1 | #import "HUMUser.h" 2 | #import "NSObject+HUMNullCheck.h" 3 | #import "HUMUserSession.h" 4 | 5 | @implementation HUMUser 6 | 7 | - (id)initWithJSON:(NSDictionary *)JSONDictionary 8 | { 9 | self = [super init]; 10 | 11 | if (!self) { 12 | return nil; 13 | } 14 | 15 | _userID = [NSString stringWithFormat:@"%@", JSONDictionary[@"id"]]; 16 | 17 | return self; 18 | } 19 | 20 | - (BOOL)isCurrentUser 21 | { 22 | return [self.userID isEqualToString:[HUMUserSession userID]]; 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Models/HUMUserSession.h: -------------------------------------------------------------------------------- 1 | @class HUMUser; 2 | @import Foundation; 3 | 4 | @interface HUMUserSession : NSObject 5 | 6 | + (NSString *)userID; 7 | + (NSString *)userToken; 8 | + (void)setUserID:(NSNumber *)userID; 9 | + (void)setUserToken:(NSString *)userToken; 10 | + (BOOL)userIsLoggedIn; 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Models/HUMUserSession.m: -------------------------------------------------------------------------------- 1 | #import "HUMUserSession.h" 2 | #import 3 | #import "NSObject+HUMNullCheck.h" 4 | 5 | static NSString *const HUMService = @"Humon"; 6 | static NSString *const HUMUserID = @"currentUserID"; 7 | static NSString *const HUMUserToken = @"currentUserToken"; 8 | 9 | 10 | @implementation HUMUserSession 11 | 12 | + (NSString *)userID 13 | { 14 | NSString *userID = [SSKeychain passwordForService:HUMService 15 | account:HUMUserID]; 16 | 17 | return userID; 18 | } 19 | 20 | + (NSString *)userToken 21 | { 22 | NSString *userToken = [SSKeychain passwordForService:HUMService 23 | account:HUMUserToken]; 24 | 25 | return userToken; 26 | } 27 | 28 | + (void)setUserID:(NSNumber *)userID 29 | { 30 | if (!userID || ![userID hum_isNotNull]) { 31 | [SSKeychain deletePasswordForService:HUMService account:HUMUserID]; 32 | return; 33 | } 34 | 35 | NSString *IDstring = [NSString stringWithFormat:@"%@", userID]; 36 | NSError *error; 37 | [SSKeychain setPassword:IDstring 38 | forService:HUMService 39 | account:HUMUserID 40 | error:&error]; 41 | if (error) { 42 | NSLog(@"USER ID SAVE ERROR: %@", error); 43 | } 44 | } 45 | 46 | + (void)setUserToken:(NSString *)userToken 47 | { 48 | if (!userToken || ![userToken hum_isNotNull]) { 49 | [SSKeychain deletePasswordForService:HUMService account:HUMUserToken]; 50 | return; 51 | } 52 | 53 | NSError *error; 54 | [SSKeychain setPassword:userToken 55 | forService:HUMService 56 | account:HUMUserToken 57 | error:&error]; 58 | if (error) { 59 | NSLog(@"USER TOKEN SAVE ERROR: %@", error); 60 | } 61 | } 62 | 63 | + (BOOL)userIsLoggedIn 64 | { 65 | BOOL hasUserID = [self userID] ? YES : NO; 66 | BOOL hasUserToken = [self userToken] ? YES : NO; 67 | return hasUserID && hasUserToken; 68 | } 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Views/HUMAnnotationView.h: -------------------------------------------------------------------------------- 1 | @import MapKit; 2 | 3 | static NSString *const HUMMapViewControllerAnnotationGreen = @"HUMMapViewControllerAnnotationGreen"; 4 | static NSString *const HUMMapViewControllerAnnotationGrey = @"HUMMapViewControllerAnnotationGrey"; 5 | 6 | @interface HUMAnnotationView : MKAnnotationView 7 | 8 | - (void)startAnimating; 9 | - (void)stopAnimating; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Views/HUMAnnotationView.m: -------------------------------------------------------------------------------- 1 | #import "HUMAnnotationView.h" 2 | 3 | @interface HUMAnnotationView () 4 | 5 | @property (assign, nonatomic) BOOL shouldAnimate; 6 | 7 | 8 | @end 9 | 10 | @implementation HUMAnnotationView 11 | 12 | - (id)initWithAnnotation:(id)annotation 13 | reuseIdentifier:(NSString *)reuseIdentifier 14 | { 15 | self = [super initWithAnnotation:annotation 16 | reuseIdentifier:reuseIdentifier]; 17 | if (!self) { 18 | return nil; 19 | } 20 | 21 | UIButton *button = [[UIButton alloc] init]; 22 | [button setBackgroundImage:[UIImage imageNamed:@"HUMButtonQuestion"] 23 | forState:UIControlStateNormal]; 24 | [button sizeToFit]; 25 | self.rightCalloutAccessoryView = button; 26 | self.canShowCallout = YES; 27 | 28 | UIImage *image; 29 | if (reuseIdentifier == HUMMapViewControllerAnnotationGreen) { 30 | image = [UIImage imageNamed:@"HUMPlacemarkSmall"]; 31 | } else if (reuseIdentifier == HUMMapViewControllerAnnotationGrey) { 32 | image = [UIImage imageNamed:@"HUMPlacemarkSmallGrey"]; 33 | } 34 | self.image = image; 35 | self.centerOffset = CGPointMake(0, -25/2); 36 | 37 | return self; 38 | } 39 | 40 | - (void)startAnimating 41 | { 42 | if (self.shouldAnimate == YES) { 43 | return; 44 | } 45 | 46 | self.shouldAnimate = YES; 47 | [self changeToNormalImage]; 48 | } 49 | 50 | - (void)changeToNormalImage 51 | { 52 | UIImage *image; 53 | if (self.reuseIdentifier == HUMMapViewControllerAnnotationGreen) { 54 | image = [UIImage imageNamed:@"HUMPlacemarkSmall"]; 55 | } else if (self.reuseIdentifier == HUMMapViewControllerAnnotationGrey) { 56 | image = [UIImage imageNamed:@"HUMPlacemarkSmallGrey"]; 57 | } 58 | self.image = image; 59 | 60 | if (self.shouldAnimate == NO) { 61 | return; 62 | } 63 | 64 | [self performSelector:@selector(changeToTalkingImage) 65 | withObject:nil 66 | afterDelay:0.2]; 67 | } 68 | 69 | - (void)changeToTalkingImage 70 | { 71 | if (self.shouldAnimate == NO) { 72 | return; 73 | } 74 | 75 | UIImage *image; 76 | if (self.reuseIdentifier == HUMMapViewControllerAnnotationGreen) { 77 | image = [UIImage imageNamed:@"HUMPlacemarkSmallTalking"]; 78 | } else if (self.reuseIdentifier == HUMMapViewControllerAnnotationGrey) { 79 | image = [UIImage imageNamed:@"HUMPlacemarkSmallGreyTalking"]; 80 | } 81 | self.image = image; 82 | 83 | [self performSelector:@selector(changeToNormalImage) 84 | withObject:nil 85 | afterDelay:0.2]; 86 | } 87 | 88 | - (void)stopAnimating 89 | { 90 | self.shouldAnimate = NO; 91 | } 92 | 93 | @end 94 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Views/HUMButton.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | typedef NS_ENUM(NSUInteger, HUMButtonColor) { 4 | HUMButtonColorWhite, 5 | HUMButtonColorGreen 6 | }; 7 | 8 | static NSInteger const HUMButtonHeight = 44.0; 9 | 10 | @interface HUMButton : UIButton 11 | 12 | - (id)initWithColor:(HUMButtonColor)color; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Views/HUMButton.m: -------------------------------------------------------------------------------- 1 | #import "HUMButton.h" 2 | #import "UIImage+HUMColorImage.h" 3 | #import "HUMAppearanceManager.h" 4 | 5 | @implementation HUMButton 6 | 7 | - (id)initWithColor:(HUMButtonColor)color 8 | { 9 | self = [super init]; 10 | if (!self) { 11 | return nil; 12 | } 13 | 14 | self.translatesAutoresizingMaskIntoConstraints = NO; 15 | if (color == HUMButtonColorWhite) { 16 | [self setupWhiteButton]; 17 | } else if (color == HUMButtonColorGreen) { 18 | [self setupGreenButton]; 19 | } 20 | 21 | return self; 22 | } 23 | 24 | - (void)setupWhiteButton 25 | { 26 | self.layer.borderColor = [[HUMAppearanceManager humonWhite] CGColor]; 27 | self.layer.borderWidth = 1.0; 28 | self.layer.cornerRadius = HUMButtonHeight/2; 29 | self.layer.masksToBounds = YES; 30 | 31 | [self setTitleColor:[HUMAppearanceManager humonWhite] 32 | forState:UIControlStateNormal]; 33 | [self setTitleColor:[UIColor grayColor] 34 | forState:UIControlStateHighlighted]; 35 | [self setBackgroundImage:[UIImage hum_imageOfSize:CGSizeMake(1, 1) 36 | color:[HUMAppearanceManager humonWhite]] 37 | forState:UIControlStateHighlighted]; 38 | } 39 | 40 | - (void)setupGreenButton 41 | { 42 | self.layer.cornerRadius = 8; 43 | self.layer.masksToBounds = YES; 44 | 45 | [self setTitleColor:[HUMAppearanceManager humonWhite] 46 | forState:UIControlStateNormal]; 47 | [self setBackgroundImage:[UIImage hum_imageOfSize:CGSizeMake(1, 1) 48 | color:[HUMAppearanceManager humonGreen]] 49 | forState:UIControlStateNormal]; 50 | 51 | [self setTitleColor:[UIColor grayColor] 52 | forState:UIControlStateHighlighted]; 53 | [self setBackgroundImage:[UIImage hum_imageOfSize:CGSizeMake(1, 1) 54 | color:[HUMAppearanceManager humonGreenDark]] 55 | forState:UIControlStateHighlighted]; 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Views/HUMFooterView.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface HUMFooterView : UIView 4 | 5 | @property (strong, nonatomic) UIButton *button; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/Views/HUMFooterView.m: -------------------------------------------------------------------------------- 1 | #import "HUMFooterView.h" 2 | #import "HUMButton.h" 3 | 4 | @implementation HUMFooterView 5 | 6 | - (id)init 7 | { 8 | self = [super init]; 9 | if (!self) { 10 | return nil; 11 | } 12 | 13 | HUMButton *button = [[HUMButton alloc] initWithColor:HUMButtonColorGreen]; 14 | [self addSubview:button]; 15 | 16 | [self addConstraint:[NSLayoutConstraint 17 | constraintWithItem:button 18 | attribute:NSLayoutAttributeHeight 19 | relatedBy:NSLayoutRelationEqual 20 | toItem:nil 21 | attribute:NSLayoutAttributeNotAnAttribute 22 | multiplier:1.0 23 | constant:HUMButtonHeight]]; 24 | [self addConstraint:[NSLayoutConstraint 25 | constraintWithItem:button 26 | attribute:NSLayoutAttributeWidth 27 | relatedBy:NSLayoutRelationEqual 28 | toItem:nil 29 | attribute:NSLayoutAttributeNotAnAttribute 30 | multiplier:1.0 31 | constant:200.0]]; 32 | [self addConstraint:[NSLayoutConstraint 33 | constraintWithItem:button 34 | attribute:NSLayoutAttributeCenterX 35 | relatedBy:NSLayoutRelationEqual 36 | toItem:self 37 | attribute:NSLayoutAttributeCenterX 38 | multiplier:1.0 39 | constant:0.0]]; 40 | [self addConstraint:[NSLayoutConstraint 41 | constraintWithItem:button 42 | attribute:NSLayoutAttributeCenterY 43 | relatedBy:NSLayoutRelationEqual 44 | toItem:self 45 | attribute:NSLayoutAttributeCenterY 46 | multiplier:1.0 47 | constant:0.0]]; 48 | 49 | self.button = button; 50 | 51 | return self; 52 | } 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /example_apps/iOS/Humon/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Humon 4 | // 5 | // Created by Diana Zmuda on 4/4/16. 6 | // Copyright © 2016 thoughtbot. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example_apps/iOS/HumonTests/HUMEventJson.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Picnic", 3 | "id" : "123456789", 4 | "address" : "Dolores Park", 5 | "lat" : 37.759705, 6 | "lon" : -122.427103, 7 | "ended_at" : "2013-09-17T00:00:00.000Z", 8 | "started_at" : "2013-09-16T00:00:00.000Z", 9 | "owner" : { 10 | "id" : 123 11 | } 12 | } -------------------------------------------------------------------------------- /example_apps/iOS/HumonTests/HUMEventKiwiTests.m: -------------------------------------------------------------------------------- 1 | #import "Kiwi.h" 2 | #import "HUMEvent.h" 3 | #import "HUMJSONFileManager.h" 4 | 5 | SPEC_BEGIN(HUMEventSpec) 6 | 7 | describe(@"Event object", ^{ 8 | 9 | __block NSDictionary *JSON; 10 | __block HUMEvent *event; 11 | 12 | beforeAll(^{ 13 | JSON = [HUMJSONFileManager singleEventJSON]; 14 | event = [[HUMEvent alloc] initWithJSON:JSON]; 15 | }); 16 | 17 | it(@"should not be nil", ^{ 18 | [[event shouldNot] beNil]; 19 | }); 20 | 21 | it(@"should initialize multiple events", ^{ 22 | NSArray *JSONarray = [HUMJSONFileManager multipleEventsJSON]; 23 | NSArray *eventArray = [HUMEvent eventsWithJSON:JSONarray]; 24 | [[theValue(eventArray.count) should] equal:theValue(JSONarray.count)]; 25 | }); 26 | 27 | it(@"should generate JSON from the object", ^{ 28 | NSMutableDictionary *desiredJSON = [JSON mutableCopy]; 29 | [desiredJSON removeObjectForKey:@"owner"]; 30 | BOOL jsonIsTheSame = [[event JSONDictionary] isEqualToDictionary:desiredJSON]; 31 | [[theValue(jsonIsTheSame) should] equal:theValue(YES)]; 32 | }); 33 | 34 | it(@"should have a human readable string", ^{ 35 | NSString *readableString = @"Picnic at Dolores Park, 9/15/13, 5:00 PM"; 36 | [[event.humanReadableString should] equal:readableString]; 37 | }); 38 | 39 | it(@"should be the same as an event with the same ID and user", ^{ 40 | HUMEvent *sameEvent = [[HUMEvent alloc] initWithJSON:JSON]; 41 | [[event should] equal:sameEvent]; 42 | }); 43 | 44 | }); 45 | 46 | SPEC_END 47 | -------------------------------------------------------------------------------- /example_apps/iOS/HumonTests/HUMEventTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "HUMEvent.h" 3 | #import "HUMJSONFileManager.h" 4 | 5 | @interface HUMEventTests : XCTestCase 6 | 7 | @property (copy, nonatomic) NSDictionary *JSON; 8 | @property (strong, nonatomic) HUMEvent *event; 9 | 10 | @end 11 | 12 | @implementation HUMEventTests 13 | 14 | - (void)setUp 15 | { 16 | [super setUp]; 17 | 18 | self.JSON = [HUMJSONFileManager singleEventJSON]; 19 | self.event = [[HUMEvent alloc] initWithJSON:self.JSON]; 20 | } 21 | 22 | - (void)testInitialization 23 | { 24 | XCTAssertNotNil(self.event, @"Should initialize event with JSON"); 25 | } 26 | 27 | - (void)testMultipleEventInitialization 28 | { 29 | NSArray *JSONarray = [HUMJSONFileManager multipleEventsJSON]; 30 | NSArray *eventArray = [HUMEvent eventsWithJSON:JSONarray]; 31 | XCTAssertEqual(eventArray.count, JSONarray.count, 32 | @"Should initialize multiple events with JSON"); 33 | } 34 | 35 | - (void)testJSONGeneration 36 | { 37 | NSMutableDictionary *desiredJSON = [self.JSON mutableCopy]; 38 | [desiredJSON removeObjectForKey:@"owner"]; 39 | BOOL jsonIsTheSame = [[self.event JSONDictionary] 40 | isEqualToDictionary:desiredJSON]; 41 | XCTAssertTrue(jsonIsTheSame, @"Event should return valid JSON"); 42 | } 43 | 44 | - (void)testHumanReadableString 45 | { 46 | NSString *readableString = @"Picnic at Dolores Park, 9/15/13, 5:00 PM"; 47 | XCTAssertEqualObjects(self.event.humanReadableString, readableString, 48 | @"Event should initialize with JSON"); 49 | } 50 | 51 | - (void)testEquality 52 | { 53 | HUMEvent *sameEvent = [[HUMEvent alloc] initWithJSON:self.JSON]; 54 | XCTAssertEqualObjects(self.event, sameEvent, 55 | @"Events should be equal with the same ID and user"); 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /example_apps/iOS/HumonTests/HUMJSONFileManager.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @interface HUMJSONFileManager : NSObject 4 | 5 | + (NSDictionary *)singleEventJSON; 6 | + (NSArray *)multipleEventsJSON; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /example_apps/iOS/HumonTests/HUMJSONFileManager.m: -------------------------------------------------------------------------------- 1 | #import "HUMJSONFileManager.h" 2 | 3 | static NSString *const HUMSingleTestEventFileName = @"HUMEventJson.json"; 4 | 5 | @implementation HUMJSONFileManager 6 | 7 | + (NSDictionary *)singleEventJSON 8 | { 9 | NSDictionary *dictionary; 10 | NSString *filePath = [[NSBundle bundleForClass:[self class]] 11 | pathForResource:[HUMSingleTestEventFileName stringByDeletingPathExtension] 12 | ofType:[HUMSingleTestEventFileName pathExtension]]; 13 | NSData *data = [NSData dataWithContentsOfFile:filePath]; 14 | NSError *error; 15 | id result = [NSJSONSerialization JSONObjectWithData:data 16 | options:kNilOptions 17 | error:&error]; 18 | 19 | if (!error && [result isKindOfClass:[NSDictionary class]]) { 20 | dictionary = result; 21 | } else { 22 | NSLog(@"ERROR CREATING JSON DICTIONARY: %@", error); 23 | } 24 | 25 | return dictionary; 26 | } 27 | 28 | + (NSArray *)multipleEventsJSON 29 | { 30 | NSDictionary *singleEventJSON = [self singleEventJSON]; 31 | 32 | if (!singleEventJSON) { 33 | return @[]; 34 | } else { 35 | return @[[singleEventJSON copy], [singleEventJSON copy]]; 36 | } 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /example_apps/iOS/HumonTests/HUMJSONFileManagerKiwiTests.m: -------------------------------------------------------------------------------- 1 | #import "Kiwi.h" 2 | #import "HUMJSONFileManager.h" 3 | 4 | SPEC_BEGIN(HUMJSONFileManagerSpec) 5 | 6 | describe(@"JSON File Manager object", ^{ 7 | 8 | it(@"should create an event JSON dictionary", ^{ 9 | NSDictionary *event = [HUMJSONFileManager singleEventJSON]; 10 | [[theValue(event.count) should] equal:theValue(8)]; 11 | }); 12 | 13 | it(@"should create an events JSON dictionary", ^{ 14 | NSArray *events = [HUMJSONFileManager multipleEventsJSON]; 15 | [[theValue(events.count) should] equal:theValue(2)]; 16 | }); 17 | 18 | }); 19 | 20 | SPEC_END 21 | -------------------------------------------------------------------------------- /example_apps/iOS/HumonTests/HUMJSONFileManagerTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "HUMJSONFileManager.h" 3 | 4 | @interface HUMJSONFileManagerTests : XCTestCase 5 | 6 | @end 7 | 8 | @implementation HUMJSONFileManagerTests 9 | 10 | - (void)testSingleEventDictionary 11 | { 12 | NSDictionary *event = [HUMJSONFileManager singleEventJSON]; 13 | XCTAssertEqual(event.count, 8, 14 | @"Should initialize single event JSON from file"); 15 | } 16 | 17 | - (void)testMultipleEventArray 18 | { 19 | NSArray *events = [HUMJSONFileManager multipleEventsJSON]; 20 | XCTAssertEqual(events.count, 2, 21 | @"Should initialize multiple events JSON from file"); 22 | } 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /example_apps/iOS/HumonTests/HUMUserKiwiTests.m: -------------------------------------------------------------------------------- 1 | #import "Kiwi.h" 2 | #import "HUMUser.h" 3 | #import "HUMUserSession.h" 4 | 5 | SPEC_BEGIN(HUMUserSpec) 6 | 7 | describe(@"User object", ^{ 8 | 9 | __block HUMUser *user; 10 | __block NSNumber *userID = @123; 11 | 12 | beforeAll(^{ 13 | user = [[HUMUser alloc] initWithJSON:@{@"id" : userID}]; 14 | }); 15 | 16 | it(@"should not be nil", ^{ 17 | [[user shouldNot] beNil]; 18 | }); 19 | 20 | it(@"should have the correct ID", ^{ 21 | [[user.userID should] equal:@"123"]; 22 | }); 23 | 24 | it(@"should match ID to current user", ^{ 25 | [HUMUserSession setUserID:userID]; 26 | BOOL isCurrentUser = [user isCurrentUser]; 27 | [[theValue(isCurrentUser) should] equal:theValue(YES)]; 28 | }); 29 | 30 | }); 31 | 32 | SPEC_END 33 | -------------------------------------------------------------------------------- /example_apps/iOS/HumonTests/HUMUserSessionKiwiTests.m: -------------------------------------------------------------------------------- 1 | #import "Kiwi.h" 2 | #import "HUMUserSession.h" 3 | #import "HUMUser.h" 4 | 5 | SPEC_BEGIN(HUMUserSessionSpec) 6 | 7 | describe(@"User session object", ^{ 8 | 9 | __block NSNumber *userID = @123; 10 | __block NSString *userToken = @"fakeUserToken"; 11 | 12 | it(@"should set the user ID", ^{ 13 | [HUMUserSession setUserID:userID]; 14 | NSString *currentUserID = [HUMUserSession userID]; 15 | [[currentUserID should] equal:@"123"]; 16 | }); 17 | 18 | it(@"should remove the user ID", ^{ 19 | [HUMUserSession setUserID:nil]; 20 | NSString *currentUserID = [HUMUserSession userID]; 21 | [[currentUserID should] beNil]; 22 | }); 23 | 24 | it(@"should set the user token", ^{ 25 | [HUMUserSession setUserToken:userToken]; 26 | NSString *currentUserToken = [HUMUserSession userToken]; 27 | [[currentUserToken should] equal:userToken]; 28 | }); 29 | 30 | it(@"should remove the user token", ^{ 31 | [HUMUserSession setUserID:nil]; 32 | NSString *currentUserToken = [HUMUserSession userID]; 33 | [[currentUserToken should] beNil]; 34 | }); 35 | 36 | it(@"should determine if the user is logged in", ^{ 37 | [HUMUserSession setUserID:userID]; 38 | [HUMUserSession setUserToken:userToken]; 39 | BOOL userLoggedIn = [HUMUserSession userIsLoggedIn]; 40 | [[theValue(userLoggedIn) should] equal:theValue(YES)]; 41 | }); 42 | 43 | it(@"should determine if the user is logged out", ^{ 44 | [HUMUserSession setUserID:nil]; 45 | [HUMUserSession setUserToken:nil]; 46 | BOOL userLoggedIn = [HUMUserSession userIsLoggedIn]; 47 | [[theValue(userLoggedIn) should] equal:theValue(NO)]; 48 | }); 49 | 50 | }); 51 | 52 | SPEC_END 53 | -------------------------------------------------------------------------------- /example_apps/iOS/HumonTests/HUMUserSessionTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "HUMUserSession.h" 3 | 4 | @interface HUMUserSessionTests : XCTestCase 5 | 6 | @property (strong, nonatomic) NSNumber *userID; 7 | @property (strong, nonatomic) NSString *userToken; 8 | 9 | @end 10 | 11 | @implementation HUMUserSessionTests 12 | 13 | - (void)setUp 14 | { 15 | [super setUp]; 16 | self.userID = @123; 17 | self.userToken = @"fakeUserToken"; 18 | } 19 | 20 | - (void)testUserIDSetting 21 | { 22 | [HUMUserSession setUserID:self.userID]; 23 | NSString *currentUserID = [HUMUserSession userID]; 24 | XCTAssertEqualObjects(currentUserID, @"123", 25 | @"User ID should equal '123'"); 26 | } 27 | 28 | - (void)testUserIDRemoval 29 | { 30 | [HUMUserSession setUserID:nil]; 31 | NSString *currentUserID = [HUMUserSession userID]; 32 | XCTAssertNil(currentUserID, @"User ID should be nil"); 33 | } 34 | 35 | - (void)testUserTokenSetting 36 | { 37 | [HUMUserSession setUserToken:self.userToken]; 38 | NSString *currentUserToken = [HUMUserSession userToken]; 39 | XCTAssertEqualObjects(currentUserToken, self.userToken, @"User token should equal %@", self.userToken); 40 | } 41 | 42 | - (void)testUserTokenRemoval 43 | { 44 | [HUMUserSession setUserToken:nil]; 45 | NSString *currentUserToken = [HUMUserSession userToken]; 46 | XCTAssertNil(currentUserToken, @"User token should be nil"); 47 | } 48 | 49 | - (void)testUserIsLoggedIn 50 | { 51 | [HUMUserSession setUserToken:self.userToken]; 52 | [HUMUserSession setUserID:self.userID]; 53 | BOOL userIsLoggedIn = [HUMUserSession userIsLoggedIn]; 54 | XCTAssertTrue(userIsLoggedIn, @"User session should be logged in"); 55 | } 56 | 57 | - (void)testUserIsLoggedOut 58 | { 59 | [HUMUserSession setUserToken:nil]; 60 | [HUMUserSession setUserID:nil]; 61 | BOOL userIsLoggedIn = [HUMUserSession userIsLoggedIn]; 62 | XCTAssertFalse(userIsLoggedIn, @"User session should be logged out"); 63 | } 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /example_apps/iOS/HumonTests/HUMUserTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "HUMUser.h" 3 | #import "HUMUserSession.h" 4 | 5 | @interface HUMUserTests : XCTestCase 6 | 7 | @property (strong, nonatomic) NSNumber *userID; 8 | @property (strong, nonatomic) HUMUser *user; 9 | 10 | @end 11 | 12 | @implementation HUMUserTests 13 | 14 | - (void)setUp 15 | { 16 | [super setUp]; 17 | self.userID = @123; 18 | self.user = [[HUMUser alloc] initWithJSON:@{@"id" : self.userID}]; 19 | [HUMUserSession setUserID:self.userID]; 20 | } 21 | 22 | - (void)testNotNil 23 | { 24 | XCTAssertNotNil(self.user, @"User object should not be nil"); 25 | } 26 | 27 | - (void)testID 28 | { 29 | XCTAssertEqualObjects(self.user.userID, @"123", 30 | @"User ID should equal '123'"); 31 | } 32 | 33 | - (void)testIsCurrentUser 34 | { 35 | [HUMUserSession setUserID:self.userID]; 36 | BOOL isCurrentUser = [self.user isCurrentUser]; 37 | XCTAssertTrue(isCurrentUser, @"User ID should match current user ID"); 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /example_apps/iOS/HumonTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /example_apps/iOS/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '8.0' 2 | 3 | target 'Humon' do 4 | pod 'SSKeychain', '~> 1.2.2' 5 | pod 'SVProgressHUD', '~> 1.0' 6 | pod 'AFNetworking', '~> 2.4.1' 7 | 8 | target 'HumonTests' do 9 | inherit! :search_paths 10 | pod 'Kiwi', '~> 2.3.0' 11 | pod 'OHHTTPStubs', '~> 3.0.2' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /example_apps/iOS/README.md: -------------------------------------------------------------------------------- 1 | Setup 2 | ==== 3 | 4 | This project uses [CocoaPods][1] to manage third party dependencies. 5 | 6 | To install CocoaPods: 7 | 8 | $ gem install cocoapods 9 | $ pod setup 10 | 11 | Then install the pods for this project 12 | 13 | $ cd path/to/humon-ios 14 | $ pod install 15 | 16 | From now on, open `Humon.xcworkspace` instead of `Humon.xcodeproj` 17 | 18 | [1]: http://www.cocoapods.org 19 | -------------------------------------------------------------------------------- /example_apps/rails/.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | log/* 3 | -------------------------------------------------------------------------------- /example_apps/rails/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.0 2 | -------------------------------------------------------------------------------- /example_apps/rails/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '2.3.0' 4 | 5 | gem 'delayed_job_active_record', '>= 4.0.0' 6 | gem 'email_validator' 7 | gem 'geocoder' 8 | gem 'jbuilder' 9 | gem 'oj' 10 | gem 'pg' 11 | gem 'rack-timeout' 12 | gem 'rails' 13 | gem 'recipient_interceptor' 14 | gem 'unicorn' 15 | 16 | group :development do 17 | gem 'better_errors' 18 | gem 'binding_of_caller' 19 | gem 'foreman' 20 | end 21 | 22 | group :development, :test do 23 | gem 'factory_girl_rails' 24 | gem 'rspec-rails', '>= 2.14' 25 | end 26 | 27 | group :test do 28 | gem 'database_cleaner' 29 | gem 'shoulda-matchers' 30 | gem 'webmock' 31 | end 32 | 33 | group :staging, :production do 34 | gem 'rails_12factor' 35 | end 36 | -------------------------------------------------------------------------------- /example_apps/rails/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.4) 5 | actionpack (= 4.2.4) 6 | actionview (= 4.2.4) 7 | activejob (= 4.2.4) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.4) 11 | actionview (= 4.2.4) 12 | activesupport (= 4.2.4) 13 | rack (~> 1.6) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 17 | actionview (4.2.4) 18 | activesupport (= 4.2.4) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 23 | activejob (4.2.4) 24 | activesupport (= 4.2.4) 25 | globalid (>= 0.3.0) 26 | activemodel (4.2.4) 27 | activesupport (= 4.2.4) 28 | builder (~> 3.1) 29 | activerecord (4.2.4) 30 | activemodel (= 4.2.4) 31 | activesupport (= 4.2.4) 32 | arel (~> 6.0) 33 | activesupport (4.2.4) 34 | i18n (~> 0.7) 35 | json (~> 1.7, >= 1.7.7) 36 | minitest (~> 5.1) 37 | thread_safe (~> 0.3, >= 0.3.4) 38 | tzinfo (~> 1.1) 39 | addressable (2.3.8) 40 | arel (6.0.3) 41 | better_errors (2.1.1) 42 | coderay (>= 1.0.0) 43 | erubis (>= 2.6.6) 44 | rack (>= 0.9.0) 45 | binding_of_caller (0.7.2) 46 | debug_inspector (>= 0.0.1) 47 | builder (3.2.2) 48 | coderay (1.1.0) 49 | crack (0.4.2) 50 | safe_yaml (~> 1.0.0) 51 | database_cleaner (1.5.1) 52 | debug_inspector (0.0.2) 53 | delayed_job (4.1.1) 54 | activesupport (>= 3.0, < 5.0) 55 | delayed_job_active_record (4.1.0) 56 | activerecord (>= 3.0, < 5) 57 | delayed_job (>= 3.0, < 5) 58 | diff-lcs (1.2.5) 59 | email_validator (1.6.0) 60 | activemodel 61 | erubis (2.7.0) 62 | factory_girl (4.5.0) 63 | activesupport (>= 3.0.0) 64 | factory_girl_rails (4.5.0) 65 | factory_girl (~> 4.5.0) 66 | railties (>= 3.0.0) 67 | foreman (0.78.0) 68 | thor (~> 0.19.1) 69 | geocoder (1.2.11) 70 | globalid (0.3.6) 71 | activesupport (>= 4.1.0) 72 | hashdiff (0.2.2) 73 | i18n (0.7.0) 74 | jbuilder (2.3.2) 75 | activesupport (>= 3.0.0, < 5) 76 | multi_json (~> 1.2) 77 | json (1.8.3) 78 | kgio (2.10.0) 79 | loofah (2.0.3) 80 | nokogiri (>= 1.5.9) 81 | mail (2.6.3) 82 | mime-types (>= 1.16, < 3) 83 | mime-types (2.6.2) 84 | mini_portile (0.6.2) 85 | minitest (5.8.1) 86 | multi_json (1.11.2) 87 | nokogiri (1.6.6.2) 88 | mini_portile (~> 0.6.0) 89 | oj (2.13.0) 90 | pg (0.18.3) 91 | rack (1.6.4) 92 | rack-test (0.6.3) 93 | rack (>= 1.0) 94 | rack-timeout (0.3.2) 95 | rails (4.2.4) 96 | actionmailer (= 4.2.4) 97 | actionpack (= 4.2.4) 98 | actionview (= 4.2.4) 99 | activejob (= 4.2.4) 100 | activemodel (= 4.2.4) 101 | activerecord (= 4.2.4) 102 | activesupport (= 4.2.4) 103 | bundler (>= 1.3.0, < 2.0) 104 | railties (= 4.2.4) 105 | sprockets-rails 106 | rails-deprecated_sanitizer (1.0.3) 107 | activesupport (>= 4.2.0.alpha) 108 | rails-dom-testing (1.0.7) 109 | activesupport (>= 4.2.0.beta, < 5.0) 110 | nokogiri (~> 1.6.0) 111 | rails-deprecated_sanitizer (>= 1.0.1) 112 | rails-html-sanitizer (1.0.2) 113 | loofah (~> 2.0) 114 | rails_12factor (0.0.3) 115 | rails_serve_static_assets 116 | rails_stdout_logging 117 | rails_serve_static_assets (0.0.4) 118 | rails_stdout_logging (0.0.4) 119 | railties (4.2.4) 120 | actionpack (= 4.2.4) 121 | activesupport (= 4.2.4) 122 | rake (>= 0.8.7) 123 | thor (>= 0.18.1, < 2.0) 124 | raindrops (0.15.0) 125 | rake (10.4.2) 126 | recipient_interceptor (0.1.2) 127 | mail 128 | rspec-core (3.3.2) 129 | rspec-support (~> 3.3.0) 130 | rspec-expectations (3.3.1) 131 | diff-lcs (>= 1.2.0, < 2.0) 132 | rspec-support (~> 3.3.0) 133 | rspec-mocks (3.3.2) 134 | diff-lcs (>= 1.2.0, < 2.0) 135 | rspec-support (~> 3.3.0) 136 | rspec-rails (3.3.3) 137 | actionpack (>= 3.0, < 4.3) 138 | activesupport (>= 3.0, < 4.3) 139 | railties (>= 3.0, < 4.3) 140 | rspec-core (~> 3.3.0) 141 | rspec-expectations (~> 3.3.0) 142 | rspec-mocks (~> 3.3.0) 143 | rspec-support (~> 3.3.0) 144 | rspec-support (3.3.0) 145 | safe_yaml (1.0.4) 146 | shoulda-matchers (3.0.1) 147 | activesupport (>= 4.0.0) 148 | sprockets (3.4.0) 149 | rack (> 1, < 3) 150 | sprockets-rails (2.3.3) 151 | actionpack (>= 3.0) 152 | activesupport (>= 3.0) 153 | sprockets (>= 2.8, < 4.0) 154 | thor (0.19.1) 155 | thread_safe (0.3.5) 156 | tzinfo (1.2.2) 157 | thread_safe (~> 0.1) 158 | unicorn (4.9.0) 159 | kgio (~> 2.6) 160 | rack 161 | raindrops (~> 0.7) 162 | webmock (1.22.1) 163 | addressable (>= 2.3.6) 164 | crack (>= 0.3.2) 165 | hashdiff 166 | 167 | PLATFORMS 168 | ruby 169 | 170 | DEPENDENCIES 171 | better_errors 172 | binding_of_caller 173 | database_cleaner 174 | delayed_job_active_record (>= 4.0.0) 175 | email_validator 176 | factory_girl_rails 177 | foreman 178 | geocoder 179 | jbuilder 180 | oj 181 | pg 182 | rack-timeout 183 | rails 184 | rails_12factor 185 | recipient_interceptor 186 | rspec-rails (>= 2.14) 187 | shoulda-matchers 188 | unicorn 189 | webmock 190 | 191 | BUNDLED WITH 192 | 1.10.6 193 | -------------------------------------------------------------------------------- /example_apps/rails/Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb 2 | worker: bundle exec rake jobs:work 3 | -------------------------------------------------------------------------------- /example_apps/rails/README.md: -------------------------------------------------------------------------------- 1 | Humon 2 | ===== 3 | 4 | ## Getting Started 5 | 6 | After you have cloned this repo, run this setup script to set up your machine 7 | with the necessary dependencies to run and test this app: 8 | 9 | % ./bin/setup 10 | 11 | It assumes you have a machine equipped with Ruby, Postgres, etc. If not, set up 12 | your machine with [this script]. 13 | 14 | [this script]: https://github.com/thoughtbot/laptop 15 | 16 | After setting up, you can run the application using [Heroku Local]: 17 | 18 | % heroku local 19 | 20 | [Heroku Local]: https://devcenter.heroku.com/articles/heroku-local 21 | 22 | ## Guidelines 23 | 24 | Use the following guides for getting things done, programming well, and 25 | programming in style. 26 | 27 | * [Protocol](http://github.com/thoughtbot/guides/blob/master/protocol) 28 | * [Best Practices](http://github.com/thoughtbot/guides/blob/master/best-practices) 29 | * [Style](http://github.com/thoughtbot/guides/blob/master/style) 30 | 31 | Credits 32 | ------- 33 | 34 | ![thoughtbot](https://thoughtbot.com/logo.png) 35 | -------------------------------------------------------------------------------- /example_apps/rails/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Humon::Application.load_tasks 7 | if defined?(RSpec) 8 | desc 'Run factory specs.' 9 | 10 | RSpec::Core::RakeTask.new(:factory_specs) do |t| 11 | t.pattern = './spec/models/factories_spec.rb' 12 | end 13 | 14 | task spec: :factory_specs 15 | end 16 | task(:default).clear 17 | task :default => [:spec] 18 | -------------------------------------------------------------------------------- /example_apps/rails/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/app/assets/images/.keep -------------------------------------------------------------------------------- /example_apps/rails/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | -------------------------------------------------------------------------------- /example_apps/rails/app/assets/javascripts/prefilled_input.js: -------------------------------------------------------------------------------- 1 | // clear inputs with starter values 2 | new function($) { 3 | $.fn.prefilledInput = function() { 4 | 5 | var focus = function () { 6 | $(this).removeClass('prefilled'); 7 | if (this.value == this.prefilledValue) { 8 | this.value = ''; 9 | } 10 | }; 11 | 12 | var blur = function () { 13 | if (this.value == '') { 14 | $(this).addClass('prefilled').val(this.prefilledValue); 15 | } else if (this.value != this.prefilledValue) { 16 | $(this).removeClass('prefilled'); 17 | } 18 | }; 19 | 20 | var extractPrefilledValue = function () { 21 | if (this.title) { 22 | this.prefilledValue = this.title; 23 | this.title = ''; 24 | } else if (this.id) { 25 | this.prefilledValue = $('label[for=' + this.id + ']').hide().text(); 26 | } 27 | if (this.prefilledValue) { 28 | this.prefilledValue = this.prefilledValue.replace(/\*$/, ''); 29 | } 30 | }; 31 | 32 | var initialize = function (index) { 33 | if (!this.prefilledValue) { 34 | this.extractPrefilledValue = extractPrefilledValue; 35 | this.extractPrefilledValue(); 36 | $(this).trigger('blur'); 37 | } 38 | }; 39 | 40 | return this.filter(":input"). 41 | focus(focus). 42 | blur(blur). 43 | each(initialize); 44 | }; 45 | 46 | var clearPrefilledInputs = function () { 47 | var form = this.form || this; 48 | $(form).find("input.prefilled, textarea.prefilled").val(""); 49 | }; 50 | 51 | var prefilledSetup = function () { 52 | $('input.prefilled, textarea.prefilled').prefilledInput(); 53 | $('form').submit(clearPrefilledInputs); 54 | $('input:submit, button:submit').click(clearPrefilledInputs); 55 | }; 56 | 57 | $(document).ready(prefilledSetup); 58 | $(document).ajaxComplete(prefilledSetup); 59 | }(jQuery); 60 | -------------------------------------------------------------------------------- /example_apps/rails/app/controllers/api/v1/attendances_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::AttendancesController < ApiController 2 | def create 3 | authorize do |user| 4 | event = Event.find(params[:event][:id]) 5 | user = user 6 | 7 | Attendance.create(event: event, user: user) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /example_apps/rails/app/controllers/api/v1/events/nearests_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::Events::NearestsController < ApiController 2 | def index 3 | @events = Event.near( 4 | [params[:lat], params[:lon]], 5 | params[:radius], 6 | units: :km 7 | ) 8 | 9 | if @events.count(:all) > 0 10 | render 11 | else 12 | render json: { message: 'No Events Found' }, status: 200 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example_apps/rails/app/controllers/api/v1/events_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::EventsController < ApiController 2 | def create 3 | authorize do |user| 4 | @user = user 5 | @event = Event.new(event_params) 6 | 7 | if @event.save 8 | render 9 | else 10 | render json: { 11 | message: 'Validation Failed', 12 | errors: @event.errors.full_messages 13 | }, status: 422 14 | end 15 | end 16 | end 17 | 18 | def show 19 | @event = Event.find(params[:id]) 20 | end 21 | 22 | def update 23 | authorize do |user| 24 | @user = user 25 | @event = Event.find(params[:id]) 26 | 27 | if @event.update(event_params) 28 | render 29 | else 30 | render json: { 31 | message: 'Validation Failed', 32 | errors: @event.errors.full_messages 33 | }, status: 422 34 | end 35 | end 36 | end 37 | 38 | private 39 | 40 | def event_params 41 | { 42 | address: params[:address], 43 | ended_at: params[:ended_at], 44 | lat: params[:lat], 45 | lon: params[:lon], 46 | name: params[:name], 47 | started_at: params[:started_at], 48 | owner: @user 49 | } 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /example_apps/rails/app/controllers/api/v1/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::UsersController < ApiController 2 | before_filter :authorize_app_secret, only: [:create] 3 | 4 | def create 5 | @user = User.new 6 | 7 | if @user.save 8 | render 9 | else 10 | render json: { 11 | message: 'Validation Failed', 12 | errors: @user.errors.full_messages 13 | }, status: 422 14 | end 15 | end 16 | 17 | private 18 | 19 | def authorize_app_secret 20 | unless correct_app_secret? 21 | render nothing: true, status: 404 22 | end 23 | end 24 | 25 | def correct_app_secret? 26 | request.headers['tb-app-secret'] == ENV.fetch('TB_APP_SECRET') 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /example_apps/rails/app/controllers/api_controller.rb: -------------------------------------------------------------------------------- 1 | class ApiController < ApplicationController 2 | protect_from_forgery with: :null_session 3 | 4 | def authorize 5 | if authorization_token 6 | yield User.find_by(auth_token: authorization_token) 7 | else 8 | render nothing: true, status: 401 9 | end 10 | end 11 | 12 | private 13 | 14 | def authorization_token 15 | @authorization_token ||= authorization_header 16 | end 17 | 18 | def authorization_header 19 | request.headers['tb-auth-token'] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /example_apps/rails/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | end 4 | -------------------------------------------------------------------------------- /example_apps/rails/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /example_apps/rails/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /example_apps/rails/app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/app/mailers/.keep -------------------------------------------------------------------------------- /example_apps/rails/app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/app/models/.keep -------------------------------------------------------------------------------- /example_apps/rails/app/models/attendance.rb: -------------------------------------------------------------------------------- 1 | class Attendance < ActiveRecord::Base 2 | belongs_to :event 3 | belongs_to :user 4 | 5 | validates :event, presence: true 6 | validates :user, presence: true 7 | validates :event_id, uniqueness: { scope: :user_id, 8 | message: 'Can only RSVP once per event' } 9 | end 10 | -------------------------------------------------------------------------------- /example_apps/rails/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/app/models/concerns/.keep -------------------------------------------------------------------------------- /example_apps/rails/app/models/event.rb: -------------------------------------------------------------------------------- 1 | class Event < ActiveRecord::Base 2 | validates :lat, presence: true 3 | validates :lon, presence: true 4 | validates :name, presence: true 5 | validates :started_at, presence: true 6 | 7 | has_many :attendances 8 | belongs_to :owner, foreign_key: 'user_id', class_name: 'User' 9 | has_many :users, through: :attendances 10 | 11 | reverse_geocoded_by :lat, :lon 12 | end 13 | -------------------------------------------------------------------------------- /example_apps/rails/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | validates :auth_token, uniqueness: true, presence: true 3 | 4 | before_validation :set_auth_token 5 | 6 | private 7 | 8 | def set_auth_token 9 | self.auth_token ||= SecureRandom.uuid 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /example_apps/rails/app/views/api/v1/attendances/create.json.jbuilder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/app/views/api/v1/attendances/create.json.jbuilder -------------------------------------------------------------------------------- /example_apps/rails/app/views/api/v1/events/_event.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.cache! event do 2 | json.address event.address 3 | json.ended_at event.ended_at 4 | json.id event.id 5 | json.lat event.lat 6 | json.lon event.lon 7 | json.name event.name 8 | json.started_at event.started_at 9 | 10 | json.owner do 11 | json.id event.owner.id 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /example_apps/rails/app/views/api/v1/events/create.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.id @event.id 2 | -------------------------------------------------------------------------------- /example_apps/rails/app/views/api/v1/events/nearests/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'api/v1/events/event', collection: @events, as: :event 2 | -------------------------------------------------------------------------------- /example_apps/rails/app/views/api/v1/events/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'event', event: @event 2 | -------------------------------------------------------------------------------- /example_apps/rails/app/views/api/v1/events/update.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.id @event.id 2 | -------------------------------------------------------------------------------- /example_apps/rails/app/views/api/v1/users/create.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.auth_token @user.auth_token 2 | json.id @user.id 3 | -------------------------------------------------------------------------------- /example_apps/rails/app/views/application/_flashes.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% flash.each do |key, value| -%> 3 |
<%= value %>
4 | <% end -%> 5 |
6 | -------------------------------------------------------------------------------- /example_apps/rails/app/views/application/_javascript.html.erb: -------------------------------------------------------------------------------- 1 | <%= javascript_include_tag :application %> 2 | 3 | <%= yield :javascript %> 4 | 5 | <% if Rails.env.test? %> 6 | <%= javascript_tag do %> 7 | $.ajaxSetup({ async: false }); 8 | <% end %> 9 | <% end %> 10 | -------------------------------------------------------------------------------- /example_apps/rails/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= page_title %> 7 | <%= stylesheet_link_tag :application, :media => 'all' %> 8 | <%= csrf_meta_tags %> 9 | 10 | 11 | <%= render 'flashes' -%> 12 | <%= yield %> 13 | <%= render 'javascript' %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /example_apps/rails/app/views/pages/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/app/views/pages/.keep -------------------------------------------------------------------------------- /example_apps/rails/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /example_apps/rails/bin/delayed_job: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) 4 | require 'delayed/command' 5 | Delayed::Command.new(ARGV).daemonize 6 | -------------------------------------------------------------------------------- /example_apps/rails/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /example_apps/rails/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /example_apps/rails/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Set up Rails app. Run this script immediately after cloning the codebase. 4 | # https://github.com/thoughtbot/guides/tree/master/protocol 5 | 6 | # Exit if any subcommand fails 7 | set -e 8 | 9 | # Set up Ruby dependencies via Bundler 10 | gem install bundler --conservative 11 | bundle check || bundle install 12 | -------------------------------------------------------------------------------- /example_apps/rails/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /example_apps/rails/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_mailer/railtie" 7 | require "sprockets/railtie" 8 | # require "rails/test_unit/railtie" 9 | 10 | # Require the gems listed in Gemfile, including any gems 11 | # you've limited to :test, :development, or :production. 12 | Bundler.require(:default, Rails.env) 13 | 14 | module Humon 15 | class Application < Rails::Application 16 | config.active_record.default_timezone = :utc 17 | 18 | config.generators do |generate| 19 | generate.helper false 20 | generate.javascript_engine false 21 | generate.request_specs false 22 | generate.routing_specs false 23 | generate.stylesheets false 24 | generate.test_framework :rspec 25 | generate.view_specs false 26 | end 27 | 28 | # Settings in config/environments/* take precedence over those specified here. 29 | # Application configuration should go into files in config/initializers 30 | # -- all .rb files in that directory are automatically loaded. 31 | 32 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 33 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 34 | # config.time_zone = 'Central Time (US & Canada)' 35 | 36 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 37 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 38 | # config.i18n.default_locale = :de 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /example_apps/rails/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 5 | -------------------------------------------------------------------------------- /example_apps/rails/config/database.yml: -------------------------------------------------------------------------------- 1 | development: &default 2 | adapter: postgresql 3 | database: humon_development 4 | encoding: utf8 5 | min_messages: warning 6 | pool: 5 7 | timeout: 5000 8 | 9 | test: 10 | <<: *default 11 | database: humon_test 12 | -------------------------------------------------------------------------------- /example_apps/rails/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Humon::Application.initialize! 6 | -------------------------------------------------------------------------------- /example_apps/rails/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Humon::Application.configure do 2 | config.cache_classes = false 3 | config.eager_load = false 4 | config.consider_all_requests_local = true 5 | config.action_controller.perform_caching = false 6 | config.action_mailer.raise_delivery_errors = true 7 | config.active_support.deprecation = :log 8 | config.active_record.migration_error = :page_load 9 | config.assets.debug = true 10 | config.action_controller.action_on_unpermitted_parameters = :raise 11 | config.action_mailer.default_url_options = { host: 'humon.local' } 12 | end 13 | -------------------------------------------------------------------------------- /example_apps/rails/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require Rails.root.join('config/initializers/smtp') 2 | Humon::Application.configure do 3 | config.cache_classes = true 4 | config.eager_load = true 5 | config.consider_all_requests_local = false 6 | config.serve_static_assets = false 7 | config.assets.compile = false 8 | config.assets.digest = true 9 | config.assets.version = '1.0' 10 | config.log_level = :info 11 | config.action_mailer.delivery_method = :smtp 12 | config.action_mailer.smtp_settings = SMTP_SETTINGS 13 | config.i18n.fallbacks = true 14 | config.active_support.deprecation = :notify 15 | config.log_formatter = ::Logger::Formatter.new 16 | config.action_mailer.default_url_options = { host: 'humon.com' } 17 | end 18 | -------------------------------------------------------------------------------- /example_apps/rails/config/environments/staging.rb: -------------------------------------------------------------------------------- 1 | Mail.register_interceptor RecipientInterceptor.new(ENV['EMAIL_RECIPIENTS']) 2 | require Rails.root.join('config/initializers/smtp') 3 | Humon::Application.configure do 4 | config.cache_classes = true 5 | config.eager_load = true 6 | config.consider_all_requests_local = false 7 | config.action_controller.perform_caching = true 8 | config.serve_static_assets = false 9 | config.assets.compile = false 10 | config.assets.digest = true 11 | config.assets.version = '1.0' 12 | config.log_level = :info 13 | config.action_mailer.delivery_method = :smtp 14 | config.action_mailer.smtp_settings = SMTP_SETTINGS 15 | config.i18n.fallbacks = true 16 | config.active_support.deprecation = :notify 17 | config.log_formatter = ::Logger::Formatter.new 18 | config.action_mailer.default_url_options = { host: 'staging.humon.com' } 19 | end 20 | -------------------------------------------------------------------------------- /example_apps/rails/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Humon::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_assets = true 17 | config.static_cache_control = "public, max-age=3600" 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | 37 | config.action_mailer.default_url_options = { host: 'www.example.com' } 38 | end 39 | -------------------------------------------------------------------------------- /example_apps/rails/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /example_apps/rails/config/initializers/disable_xml_params.rb: -------------------------------------------------------------------------------- 1 | # Protect against injection attacks 2 | # http://www.kb.cert.org/vuls/id/380039 3 | ActionDispatch::ParamsParser::DEFAULT_PARSERS.delete(Mime::XML) 4 | -------------------------------------------------------------------------------- /example_apps/rails/config/initializers/errors.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'net/smtp' 3 | 4 | # Example: 5 | # begin 6 | # some http call 7 | # rescue *HTTP_ERRORS => error 8 | # notify_hoptoad error 9 | # end 10 | 11 | HTTP_ERRORS = [Timeout::Error, 12 | Errno::EINVAL, 13 | Errno::ECONNRESET, 14 | EOFError, 15 | Net::HTTPBadResponse, 16 | Net::HTTPHeaderSyntaxError, 17 | Net::ProtocolError] 18 | 19 | SMTP_SERVER_ERRORS = [TimeoutError, 20 | IOError, 21 | Net::SMTPUnknownError, 22 | Net::SMTPServerBusy, 23 | Net::SMTPAuthenticationError] 24 | 25 | SMTP_CLIENT_ERRORS = [Net::SMTPFatalError, 26 | Net::SMTPSyntaxError] 27 | 28 | SMTP_ERRORS = SMTP_SERVER_ERRORS + SMTP_CLIENT_ERRORS 29 | -------------------------------------------------------------------------------- /example_apps/rails/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /example_apps/rails/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /example_apps/rails/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /example_apps/rails/config/initializers/rack_timeout.rb: -------------------------------------------------------------------------------- 1 | Rack::Timeout.timeout = (ENV['TIMEOUT_IN_SECONDS'] || 5).to_i 2 | -------------------------------------------------------------------------------- /example_apps/rails/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure your secret_key_base is kept private 11 | # if you're sharing your code publicly. 12 | Humon::Application.config.secret_key_base = '0593a64f3617acc87d04d4d8b890e35024431225dda9af00845f8611475651f5ed44984f59be6d75fbe8ec5170f78c33c23cef94617cae2d4369bba48194fb04' 13 | -------------------------------------------------------------------------------- /example_apps/rails/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Humon::Application.config.session_store :cookie_store, key: '_humon_session' 4 | -------------------------------------------------------------------------------- /example_apps/rails/config/initializers/smtp.rb: -------------------------------------------------------------------------------- 1 | if Rails.env.staging? || Rails.env.production? 2 | SMTP_SETTINGS = { 3 | address: ENV['SMTP_ADDRESS'], # example: 'smtp.sendgrid.net' 4 | authentication: :plain, 5 | domain: ENV['SMTP_DOMAIN'], # example: 'this-app.com' 6 | password: ENV['SMTP_PASSWORD'], 7 | port: '587', 8 | user_name: ENV['SMTP_USERNAME'] 9 | } 10 | end 11 | -------------------------------------------------------------------------------- /example_apps/rails/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /example_apps/rails/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | date: 3 | formats: 4 | default: '%m/%d/%Y' 5 | with_weekday: '%a %m/%d/%y' 6 | 7 | time: 8 | formats: 9 | default: '%a, %b %-d, %Y at %r' 10 | date: '%b %-d, %Y' 11 | short: '%B %d' 12 | -------------------------------------------------------------------------------- /example_apps/rails/config/routes.rb: -------------------------------------------------------------------------------- 1 | Humon::Application.routes.draw do 2 | scope module: :api, defaults: { format: 'json' } do 3 | namespace :v1 do 4 | namespace :events do 5 | resources :nearests, only: [:index] 6 | end 7 | 8 | resources :attendances, only: [:create] 9 | resources :events, only: [:create, :show, :update] 10 | resources :users, only: [:create] 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /example_apps/rails/config/unicorn.rb: -------------------------------------------------------------------------------- 1 | # https://devcenter.heroku.com/articles/rails-unicorn 2 | 3 | worker_processes (ENV['WEB_CONCURRENCY'] || 3).to_i 4 | timeout (ENV['WEB_TIMEOUT'] || 5).to_i 5 | preload_app true 6 | 7 | before_fork do |server, worker| 8 | Signal.trap 'TERM' do 9 | puts 'Unicorn master intercepting TERM and sending myself QUIT instead' 10 | Process.kill 'QUIT', Process.pid 11 | end 12 | 13 | if defined? ActiveRecord::Base 14 | ActiveRecord::Base.connection.disconnect! 15 | end 16 | end 17 | 18 | after_fork do |server, worker| 19 | Signal.trap 'TERM' do 20 | puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to sent QUIT' 21 | end 22 | 23 | if defined? ActiveRecord::Base 24 | ActiveRecord::Base.establish_connection 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /example_apps/rails/db/migrate/20131003185021_create_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class CreateDelayedJobs < ActiveRecord::Migration 2 | def self.up 3 | create_table :delayed_jobs, :force => true do |table| 4 | table.integer :priority, :default => 0, :null => false # Allows some jobs to jump to the front of the queue 5 | table.integer :attempts, :default => 0, :null => false # Provides for retries, but still fail eventually. 6 | table.text :handler, :null => false # YAML-encoded string of the object that will do work 7 | table.text :last_error # reason for last failure (See Note below) 8 | table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. 9 | table.datetime :locked_at # Set when a client is working on this object 10 | table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) 11 | table.string :locked_by # Who is working on this object (if locked) 12 | table.string :queue # The name of the queue this job is in 13 | table.timestamps 14 | end 15 | 16 | add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority' 17 | end 18 | 19 | def self.down 20 | drop_table :delayed_jobs 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /example_apps/rails/db/migrate/20131003213856_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.timestamps null: false 5 | t.string :facebook_id, null: false 6 | t.string :first_name, null: false 7 | t.string :image_url, null: false 8 | t.string :last_name, null: false 9 | end 10 | 11 | add_index :users, :facebook_id 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /example_apps/rails/db/migrate/20131028210819_create_events.rb: -------------------------------------------------------------------------------- 1 | class CreateEvents < ActiveRecord::Migration 2 | def change 3 | create_table :events do |t| 4 | t.timestamps null: false 5 | t.datetime :ended_at 6 | t.string :name, null: false 7 | t.integer :place_id, null: false 8 | t.datetime :started_at, null: false 9 | t.integer :user_id, null: false 10 | end 11 | 12 | add_index :events, :place_id 13 | add_index :events, :user_id 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example_apps/rails/db/migrate/20131030180424_add_location_fields_to_events.rb: -------------------------------------------------------------------------------- 1 | class AddLocationFieldsToEvents < ActiveRecord::Migration 2 | def up 3 | add_column :events, :address, :string 4 | add_column :events, :lat, :float, null: false 5 | add_column :events, :lon, :float, null: false 6 | remove_column :events, :place_id 7 | end 8 | 9 | def down 10 | remove_column :events, :address 11 | remove_column :events, :lat 12 | remove_column :events, :lon 13 | add_column :events, :place_id, :integer, null: false 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example_apps/rails/db/migrate/20131030184615_change_user_fields.rb: -------------------------------------------------------------------------------- 1 | class ChangeUserFields < ActiveRecord::Migration 2 | def up 3 | add_column :users, :auth_token, :string 4 | remove_column :users, :facebook_id, :string 5 | remove_column :users, :first_name, :string 6 | remove_column :users, :image_url, :string 7 | remove_column :users, :last_name, :string 8 | add_index :users, :auth_token, unique: true 9 | end 10 | 11 | def down 12 | remove_column :users, :auth_token, :string 13 | add_column :users, :facebook_id, :string, null: false 14 | add_column :users, :first_name, :string, null: false 15 | add_column :users, :image_url, :string, null: false 16 | add_column :users, :last_name, :string, null: false 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /example_apps/rails/db/migrate/20131031224142_create_attendances.rb: -------------------------------------------------------------------------------- 1 | class CreateAttendances < ActiveRecord::Migration 2 | def change 3 | create_table :attendances do |t| 4 | t.timestamps null: false 5 | t.integer :event_id, null: false 6 | t.integer :user_id, null: false 7 | end 8 | 9 | add_index :attendances, [:event_id, :user_id], unique: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /example_apps/rails/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20131031224142) do 15 | 16 | # These are extensions that must be enabled in order to support this database 17 | enable_extension "plpgsql" 18 | 19 | create_table "attendances", force: :cascade do |t| 20 | t.datetime "created_at", null: false 21 | t.datetime "updated_at", null: false 22 | t.integer "event_id", null: false 23 | t.integer "user_id", null: false 24 | end 25 | 26 | add_index "attendances", ["event_id", "user_id"], name: "index_attendances_on_event_id_and_user_id", unique: true, using: :btree 27 | 28 | create_table "delayed_jobs", force: :cascade do |t| 29 | t.integer "priority", default: 0, null: false 30 | t.integer "attempts", default: 0, null: false 31 | t.text "handler", null: false 32 | t.text "last_error" 33 | t.datetime "run_at" 34 | t.datetime "locked_at" 35 | t.datetime "failed_at" 36 | t.string "locked_by" 37 | t.string "queue" 38 | t.datetime "created_at" 39 | t.datetime "updated_at" 40 | end 41 | 42 | add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree 43 | 44 | create_table "events", force: :cascade do |t| 45 | t.datetime "created_at", null: false 46 | t.datetime "updated_at", null: false 47 | t.datetime "ended_at" 48 | t.string "name", null: false 49 | t.datetime "started_at", null: false 50 | t.integer "user_id", null: false 51 | t.string "address" 52 | t.float "lat", null: false 53 | t.float "lon", null: false 54 | end 55 | 56 | add_index "events", ["user_id"], name: "index_events_on_user_id", using: :btree 57 | 58 | create_table "users", force: :cascade do |t| 59 | t.datetime "created_at", null: false 60 | t.datetime "updated_at", null: false 61 | t.string "auth_token" 62 | end 63 | 64 | add_index "users", ["auth_token"], name: "index_users_on_auth_token", unique: true, using: :btree 65 | 66 | end 67 | -------------------------------------------------------------------------------- /example_apps/rails/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /example_apps/rails/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/lib/assets/.keep -------------------------------------------------------------------------------- /example_apps/rails/lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/lib/tasks/.keep -------------------------------------------------------------------------------- /example_apps/rails/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | The page you were looking for doesn't exist (404) 7 | 8 | 9 | 10 | 11 |
12 |

The page you were looking for doesn't exist.

13 |

You may have mistyped the address or the page may have moved.

14 |
15 |

If you are the application owner check the logs for more information.

16 | 17 | 18 | -------------------------------------------------------------------------------- /example_apps/rails/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | The change you wanted was rejected (422) 7 | 8 | 9 | 10 | 11 |
12 |

The change you wanted was rejected.

13 |

Maybe you tried to change something you didn't have access to.

14 |
15 |

If you are the application owner check the logs for more information.

16 | 17 | 18 | -------------------------------------------------------------------------------- /example_apps/rails/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | We're sorry, but something went wrong (500) 7 | 8 | 9 | 10 | 11 |
12 |

We're sorry, but something went wrong.

13 |
14 |

If you are the application owner check the logs for more information.

15 | 16 | 17 | -------------------------------------------------------------------------------- /example_apps/rails/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/public/favicon.ico -------------------------------------------------------------------------------- /example_apps/rails/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /example_apps/rails/spec/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/spec/controllers/.keep -------------------------------------------------------------------------------- /example_apps/rails/spec/controllers/api/v1/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Api::V1::UsersController do 4 | it 'looks for an app secret in the headers' do 5 | app_secret = 'testsecret' 6 | auth_token = 'abc123' 7 | ENV['TB_APP_SECRET'] = app_secret 8 | 9 | request.headers['tb-app-secret'] = app_secret 10 | request.headers['tb-auth-token'] = auth_token 11 | post :create, format: :json 12 | 13 | expect(response).to be_ok 14 | end 15 | 16 | it 'does not allow requests without a valid app secret in the header' do 17 | app_secret = 'testsecret' 18 | auth_token = 'abc123' 19 | ENV['TB_APP_SECRET'] = app_secret 20 | 21 | request.headers['tb-app-secret'] = 'other_secret' 22 | request.headers['tb-auth-token'] = auth_token 23 | post :create, format: :json 24 | 25 | expect(response.status).to eq 404 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /example_apps/rails/spec/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | sequence :lat do |n| 3 | "#{n}.0".to_f 4 | end 5 | 6 | sequence :lon do |n| 7 | "#{n}.0".to_f 8 | end 9 | 10 | sequence :name do |n| 11 | "name #{n}" 12 | end 13 | 14 | sequence :started_at do |n| 15 | Time.zone.now + n.hours 16 | end 17 | 18 | sequence :token do 19 | SecureRandom.hex(3) 20 | end 21 | 22 | factory :attendance do 23 | event 24 | user 25 | end 26 | 27 | factory :event do 28 | lat 29 | lon 30 | name 31 | started_at 32 | owner factory: :user 33 | end 34 | 35 | factory :user do 36 | auth_token { generate(:token) } 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /example_apps/rails/spec/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/spec/helpers/.keep -------------------------------------------------------------------------------- /example_apps/rails/spec/lib/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/spec/lib/.keep -------------------------------------------------------------------------------- /example_apps/rails/spec/models/attendance_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Attendance, 'Validations' do 4 | it { should validate_presence_of(:event) } 5 | it { should validate_presence_of(:user) } 6 | 7 | it 'validates uniqueness of event_id and user_id' do 8 | event = create(:event) 9 | user = create(:user) 10 | create(:attendance, event: event, user: user) 11 | attendance = build(:attendance, event: event, user: user) 12 | 13 | expect(attendance).not_to be_valid 14 | end 15 | end 16 | 17 | describe Attendance, 'Associations' do 18 | it { should belong_to(:event) } 19 | it { should belong_to(:user) } 20 | end 21 | -------------------------------------------------------------------------------- /example_apps/rails/spec/models/event_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Event, 'Validations' do 4 | it { should validate_presence_of(:lat) } 5 | it { should validate_presence_of(:lon) } 6 | it { should validate_presence_of(:name) } 7 | it { should validate_presence_of(:started_at) } 8 | end 9 | 10 | describe Event, 'Associations' do 11 | it { should have_many(:attendances) } 12 | it { should belong_to(:owner).class_name('User') } 13 | it { should have_many(:users).through(:attendances) } 14 | end 15 | -------------------------------------------------------------------------------- /example_apps/rails/spec/models/factories_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | FactoryGirl.factories.map(&:name).each do |factory_name| 4 | describe "factory #{factory_name}" do 5 | it 'is valid' do 6 | factory = build(factory_name) 7 | 8 | if factory.respond_to?(:valid?) 9 | expect(factory).to be_valid, factory.errors.full_messages.join(',') 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /example_apps/rails/spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe User, 'Validations' do 4 | it { should validate_uniqueness_of(:auth_token) } 5 | end 6 | 7 | describe User, '#auth_token' do 8 | context "when creating a user" do 9 | it "returns a unique value" do 10 | uuid = "unique-id" 11 | allow(SecureRandom).to receive(:uuid).and_return(uuid) 12 | 13 | user = User.create 14 | 15 | expect(user.auth_token).to eq(uuid) 16 | end 17 | end 18 | 19 | context "when updating a user" do 20 | it "returns the same token" do 21 | user = User.create 22 | token = user.auth_token 23 | 24 | user.save 25 | 26 | expect(user.auth_token).to eq(token) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /example_apps/rails/spec/requests/api/v1/attendances_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'POST /v1/attendances' do 4 | it 'creates an attendance for a user and event' do 5 | event = create(:event) 6 | user = create(:user) 7 | 8 | post '/v1/attendances', { 9 | event: { id: event.id }, 10 | }.to_json, 11 | set_headers(user.auth_token) 12 | 13 | expect(event.reload.attendances.count).to eq 1 14 | end 15 | 16 | it 'only allows a user to RSVP once per event' do 17 | event = create(:event) 18 | user = create(:user) 19 | 20 | 2.times do 21 | post '/v1/attendances', { 22 | event: { id: event.id }, 23 | }.to_json, 24 | set_headers(user.auth_token) 25 | end 26 | 27 | expect(event.reload.attendances.count).to eq 1 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /example_apps/rails/spec/requests/api/v1/events/events_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'GET /v1/events/:id' do 4 | it 'returns an event by :id' do 5 | event = create(:event) 6 | 7 | get "/v1/events/#{event.id}" 8 | 9 | expect(response_json).to eq( 10 | { 11 | 'address' => event.address, 12 | 'ended_at' => event.ended_at, 13 | 'id' => event.id, 14 | 'lat' => event.lat, 15 | 'lon' => event.lon, 16 | 'name' => event.name, 17 | 'started_at' => event.started_at.as_json, 18 | 'owner' => { 19 | 'id' => event.owner.id 20 | } 21 | } 22 | ) 23 | end 24 | end 25 | 26 | describe 'POST /v1/events' do 27 | it 'saves the address, lat, lon, name, owner, and started_at date' do 28 | date = Time.zone.now 29 | auth_token = '123abcd456xyz' 30 | owner = create(:user, auth_token: auth_token) 31 | 32 | post '/v1/events', { 33 | address: '123 Example St.', 34 | ended_at: date, 35 | lat: 1.0, 36 | lon: 1.0, 37 | name: 'Fun Place!!', 38 | started_at: date, 39 | }.to_json, 40 | set_headers(auth_token) 41 | 42 | event = Event.last 43 | expect(response_json).to eq({ 'id' => event.id }) 44 | expect(event.address).to eq '123 Example St.' 45 | expect(event.ended_at.to_i).to eq date.to_i 46 | expect(event.lat).to eq 1.0 47 | expect(event.lon).to eq 1.0 48 | expect(event.name).to eq 'Fun Place!!' 49 | expect(event.started_at.to_i).to eq date.to_i 50 | expect(event.owner).to eq owner 51 | end 52 | 53 | it 'returns an error message when invalid' do 54 | auth_token = '123abcd456xyz' 55 | 56 | post '/v1/events', 57 | {}.to_json, 58 | set_headers(auth_token) 59 | 60 | expect(response_json).to eq({ 61 | 'message' => 'Validation Failed', 62 | 'errors' => [ 63 | "Lat can't be blank", 64 | "Lon can't be blank", 65 | "Name can't be blank", 66 | "Started at can't be blank", 67 | ] 68 | }) 69 | expect(response.code.to_i).to eq 422 70 | end 71 | end 72 | 73 | describe 'PATCH /v1/events/:id' do 74 | it 'updates the event attributes' do 75 | event = create(:event, name: 'Old name') 76 | new_name = 'New name' 77 | 78 | patch "/v1/events/#{event.id}", { 79 | address: event.address, 80 | ended_at: event.ended_at, 81 | lat: event.lat, 82 | lon: event.lon, 83 | name: new_name, 84 | started_at: event.started_at, 85 | owner: { 86 | id: event.owner.id 87 | } 88 | }.to_json, 89 | set_headers(event.owner.auth_token) 90 | 91 | event = event.reload 92 | expect(event.name).to eq new_name 93 | expect(response_json).to eq({ 'id' => event.id }) 94 | end 95 | 96 | it 'returns an error message when invalid' do 97 | event = create(:event) 98 | 99 | patch "/v1/events/#{event.id}", { 100 | address: event.address, 101 | ended_at: event.ended_at, 102 | lat: event.lat, 103 | lon: event.lon, 104 | name: nil, 105 | started_at: event.started_at, 106 | owner: { 107 | id: event.owner.id 108 | } 109 | }.to_json, 110 | set_headers(event.owner.auth_token) 111 | 112 | event = event.reload 113 | expect(event.name).to_not be nil 114 | expect(response_json).to eq({ 115 | 'message' => 'Validation Failed', 116 | 'errors' => [ 117 | "Name can't be blank" 118 | ] 119 | }) 120 | expect(response.code.to_i).to eq 422 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /example_apps/rails/spec/requests/api/v1/events/nearest_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'GET /v1/events/nearests?lat=&lon=&radius=' do 4 | it 'returns the events closest to the lat and lon' do 5 | near_event = create(:event, lat: 37.760322, lon: -122.429667) 6 | less_near_event = create(:event, lat: 37.760321, lon: -122.429667) 7 | create(:event, lat: 37.687737, lon: -122.470608) 8 | lat = 37.771098 9 | lon = -122.430782 10 | radius = 5 11 | 12 | get "/v1/events/nearests?lat=#{lat}&lon=#{lon}&radius=#{radius}" 13 | 14 | expect(response_json).to eq([ 15 | { 16 | 'address' => near_event.address, 17 | 'ended_at' => near_event.ended_at, 18 | 'id' => near_event.id, 19 | 'lat' => near_event.lat, 20 | 'lon' => near_event.lon, 21 | 'name' => near_event.name, 22 | 'started_at' => near_event.started_at.as_json, 23 | 'owner' => { 'id' => near_event.owner.id } 24 | }, 25 | { 26 | 'address' => less_near_event.address, 27 | 'ended_at' => less_near_event.ended_at, 28 | 'id' => less_near_event.id, 29 | 'lat' => less_near_event.lat, 30 | 'lon' => less_near_event.lon, 31 | 'name' => less_near_event.name, 32 | 'started_at' => less_near_event.started_at.as_json, 33 | 'owner' => { 'id' => less_near_event.owner.id } 34 | } 35 | ]) 36 | end 37 | 38 | it 'returns an error message when no event is found' do 39 | lat = 37.771098 40 | lon = -122.430782 41 | radius = 1 42 | 43 | get "/v1/events/nearests?lat=#{lat}&lon=#{lon}&radius=#{radius}" 44 | 45 | expect(response_json).to eq({ 'message' => 'No Events Found' }) 46 | expect(response.code.to_i).to eq 200 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /example_apps/rails/spec/requests/api/v1/users_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'POST /v1/users' do 4 | it 'creates a new user' do 5 | post '/v1/users', {}, set_headers 6 | 7 | user = User.last 8 | expect(response_json).to eq( 9 | { 10 | 'auth_token' => user.auth_token, 11 | 'id' => user.id 12 | } 13 | ) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example_apps/rails/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] = 'test' 2 | 3 | require File.expand_path('../../config/environment', __FILE__) 4 | 5 | require 'rspec/rails' 6 | require 'webmock/rspec' 7 | 8 | Dir[Rails.root.join('spec/support/**/*.rb')].each { |file| require file } 9 | 10 | RSpec.configure do |config| 11 | config.expect_with :rspec do |c| 12 | c.syntax = :expect 13 | end 14 | 15 | config.infer_spec_type_from_file_location! 16 | config.fail_fast = true 17 | config.infer_base_class_for_anonymous_controllers = false 18 | config.order = 'random' 19 | config.use_transactional_fixtures = false 20 | end 21 | 22 | WebMock.disable_net_connect!(allow_localhost: true) 23 | -------------------------------------------------------------------------------- /example_apps/rails/spec/support/background_jobs.rb: -------------------------------------------------------------------------------- 1 | module BackgroundJobs 2 | def run_background_jobs_immediately 3 | delay_jobs = Delayed::Worker.delay_jobs 4 | Delayed::Worker.delay_jobs = false 5 | yield 6 | ensure 7 | Delayed::Worker.delay_jobs = delay_jobs 8 | end 9 | end 10 | 11 | RSpec.configure do |config| 12 | config.around(:each, type: :feature) do |example| 13 | run_background_jobs_immediately do 14 | example.run 15 | end 16 | end 17 | 18 | config.include BackgroundJobs 19 | end 20 | -------------------------------------------------------------------------------- /example_apps/rails/spec/support/database_cleaner.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before(:suite) do 3 | DatabaseCleaner.clean_with(:deletion) 4 | end 5 | 6 | config.before(:each) do 7 | DatabaseCleaner.strategy = :transaction 8 | end 9 | 10 | config.before(:each, :js => true) do 11 | DatabaseCleaner.strategy = :deletion 12 | end 13 | 14 | config.before(:each) do 15 | DatabaseCleaner.start 16 | end 17 | 18 | config.after(:each) do 19 | DatabaseCleaner.clean 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /example_apps/rails/spec/support/factory_girl.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include FactoryGirl::Syntax::Methods 3 | end 4 | -------------------------------------------------------------------------------- /example_apps/rails/spec/support/matchers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/spec/support/matchers/.keep -------------------------------------------------------------------------------- /example_apps/rails/spec/support/mixins/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/spec/support/mixins/.keep -------------------------------------------------------------------------------- /example_apps/rails/spec/support/request_headers.rb: -------------------------------------------------------------------------------- 1 | module RequestHeaders 2 | def set_headers(auth_token = nil) 3 | app_secret = 'secretkey' 4 | ENV['TB_APP_SECRET'] = app_secret 5 | 6 | { 7 | 'tb-app-secret' => app_secret, 8 | 'tb-auth-token' => auth_token, 9 | 'Content-Type' => 'application/json' 10 | } 11 | end 12 | end 13 | 14 | RSpec.configure do |config| 15 | config.include RequestHeaders 16 | end 17 | -------------------------------------------------------------------------------- /example_apps/rails/spec/support/response_json.rb: -------------------------------------------------------------------------------- 1 | module ResponseJSON 2 | def response_json 3 | JSON.parse(response.body) 4 | end 5 | end 6 | 7 | RSpec.configure do |config| 8 | config.include ResponseJSON 9 | end 10 | -------------------------------------------------------------------------------- /example_apps/rails/spec/support/shared_examples/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/spec/support/shared_examples/.keep -------------------------------------------------------------------------------- /example_apps/rails/spec/support/shoulda_matchers.rb: -------------------------------------------------------------------------------- 1 | Shoulda::Matchers.configure do |config| 2 | config.integrate do |with| 3 | with.test_framework :rspec 4 | with.library :rails 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /example_apps/rails/vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /example_apps/rails/vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/example_apps/rails/vendor/assets/stylesheets/.keep -------------------------------------------------------------------------------- /release/images/cover.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/images/cover.pdf -------------------------------------------------------------------------------- /release/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/images/cover.png -------------------------------------------------------------------------------- /release/images/humon-database-representation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/images/humon-database-representation.png -------------------------------------------------------------------------------- /release/images/ios_app_skeleton_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/images/ios_app_skeleton_1.png -------------------------------------------------------------------------------- /release/images/ios_app_skeleton_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/images/ios_app_skeleton_2.png -------------------------------------------------------------------------------- /release/images/ios_app_skeleton_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/images/ios_app_skeleton_3.png -------------------------------------------------------------------------------- /release/images/ios_event_object_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/images/ios_event_object_1.png -------------------------------------------------------------------------------- /release/images/ios_new_xcode_project_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/images/ios_new_xcode_project_1.png -------------------------------------------------------------------------------- /release/images/ios_new_xcode_project_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/images/ios_new_xcode_project_2.png -------------------------------------------------------------------------------- /release/images/ios_new_xcode_project_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/images/ios_new_xcode_project_3.png -------------------------------------------------------------------------------- /release/ios-on-rails-sample.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/ios-on-rails-sample.epub -------------------------------------------------------------------------------- /release/ios-on-rails-sample.mobi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/ios-on-rails-sample.mobi -------------------------------------------------------------------------------- /release/ios-on-rails-sample.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/ios-on-rails-sample.pdf -------------------------------------------------------------------------------- /release/ios-on-rails-sample.toc.html: -------------------------------------------------------------------------------- 1 |
2 |

Table of Contents

3 |
    4 |
  • Introduction
  • 5 |
  • 6 |

    Building the Humon Rails App

    7 |
      8 |
    • Creating a GET request
    • 9 |
    10 |
  • 11 |
  • 12 |

    Building the Humon iOS App

    13 |
      14 |
    • A Rails API Client With NSURLSession
    • 15 |
    • Closing
    • 16 |
    17 |
  • 18 |
19 |
20 | -------------------------------------------------------------------------------- /release/ios-on-rails.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/ios-on-rails.epub -------------------------------------------------------------------------------- /release/ios-on-rails.mobi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/ios-on-rails.mobi -------------------------------------------------------------------------------- /release/ios-on-rails.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/ios-on-rails/722f59b92e3040b77dcae7cf02e4eceb5185539a/release/ios-on-rails.pdf -------------------------------------------------------------------------------- /release/ios-on-rails.toc.html: -------------------------------------------------------------------------------- 1 |
2 |

Table of Contents

3 |
    4 |
  • Introduction
  • 5 |
  • 6 |

    Building the Humon Rails App

    7 |
      8 |
    • Introduction to our example application and setup
    • 9 |
    • Creating a GET request
    • 10 |
    • Creating a POST request
    • 11 |
    • Creating a PATCH request
    • 12 |
    • Creating a geocoded GET request
    • 13 |
    14 |
  • 15 |
  • 16 |

    Building the Humon iOS App

    17 |
      18 |
    • Introduction
    • 19 |
    • A New Xcode Project
    • 20 |
    • Managing Dependencies
    • 21 |
    • The Mobile App's Skeleton
    • 22 |
    • The Map View Controller
    • 23 |
    • The Event View Controller
    • 24 |
    • A Rails API Client With NSURLSession
    • 25 |
    • The User Object
    • 26 |
    • Posting a User With NSURLSession
    • 27 |
    • Making the POST User Request
    • 28 |
    • The Event Object
    • 29 |
    • Posting an Event With NSURLSession
    • 30 |
    • Making the POST Event Request
    • 31 |
    • Posting with the Event View Controller
    • 32 |
    • Getting Events With NSURLSession
    • 33 |
    • Displaying Events on the Map
    • 34 |
    • Viewing Individual Events
    • 35 |
    • Finishing Touches
    • 36 |
    37 |
  • 38 |
39 |
40 | --------------------------------------------------------------------------------