Repository: adimango/insights-for-instagram Branch: master Commit: 9583abeac1a5 Files: 42 Total size: 137.4 KB Directory structure: gitextract_zbz5km2e/ ├── .gitignore ├── .swiftlint.yml ├── .travis.yml ├── LICENSE ├── Podfile ├── README.md ├── fastlane/ │ ├── Appfile │ ├── Fastfile │ └── README.md ├── insights-for-instagram/ │ ├── Account/ │ │ ├── AccountViewController.swift │ │ ├── AddAccountInteractor.swift │ │ ├── AddAccountPresenter.swift │ │ └── AddAccountViewController.swift │ ├── App/ │ │ ├── AppConfiguration.swift │ │ ├── AppDelegate.swift │ │ ├── AppExtensions.swift │ │ └── AppUserAccount.swift │ ├── Assets.xcassets/ │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── chevron_location_.imageset/ │ │ │ └── Contents.json │ │ ├── icons8-Male User Filled_30.imageset/ │ │ │ └── Contents.json │ │ ├── line-chart-graph.imageset/ │ │ │ └── Contents.json │ │ ├── placeHolder.imageset/ │ │ │ └── Contents.json │ │ └── user-icon.imageset/ │ │ └── Contents.json │ ├── Base.lproj/ │ │ └── Main.storyboard │ ├── Info.plist │ ├── Insights/ │ │ ├── InsightsInteractor.swift │ │ ├── InsightsPresenter.swift │ │ ├── InsightsViewController.swift │ │ └── Views/ │ │ ├── InstagramMediaCollectionViewCell.swift │ │ └── InstagramMediaSectionTableViewCell.swift │ ├── LaunchScreen.storyboard │ ├── Models/ │ │ └── InstagramMedia.swift │ ├── Networking/ │ │ └── InstagramAPI.swift │ └── Services/ │ └── DataService.swift ├── insights-for-instagram.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ └── contents.xcworkspacedata │ └── xcshareddata/ │ └── xcschemes/ │ └── insights-for-instagram.xcscheme ├── insights-for-instagram.xcworkspace/ │ └── contents.xcworkspacedata └── insights-for-instagramTests/ ├── Info.plist ├── InstagramAPITests.swift └── InstagramMediaTests.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## Build generated build/ DerivedData/ ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata/ ## Other *.moved-aside *.xccheckout *.xcscmblueprint ## Obj-C/Swift specific *.hmap *.ipa *.dSYM.zip *.dSYM ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ # Package.pins # Package.resolved .build/ # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # Environments .env .env.default Pods/ # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build # fastlane # # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/#source-control fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output ================================================ FILE: .swiftlint.yml ================================================ disabled_rules: - trailing_whitespace - line_length - type_name - identifier_name opt_in_rules: - empty_count - force_unwrapping excluded: - Pods function_body_length: warning: 50 ================================================ FILE: .travis.yml ================================================ language: objective-c os: osx osx_image: xcode9.2 xcode_workspace: insights-for-instagram.xcworkspace podfile: Podfile cache: cocoapods notifications: email: false before_install: - rvm install ruby --latest - gem install cocoapods - pod repo update master - gem install fastlane --no-ri --no-rdoc --no-document # - brew update # - brew install swiftlint || true jobs: include: - stage: XCTest script: - fastlane scan -s insights-for-instagram ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 alexdimango.me, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Podfile ================================================ # Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'insights-for-instagram' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks use_frameworks! inhibit_all_warnings! # Pods for insights-for-instagram pod 'Moya' pod 'RealmSwift' pod 'ObjectMapper' pod 'Kingfisher', '~> 4.0' pod 'SwiftLint' target 'insights-for-instagramTests' do inherit! :search_paths pod 'Quick' pod 'Nimble' end end ================================================ FILE: README.md ================================================ # Insights for Instagram A simple iOS Instagram's media insights App. ![iOS](https://img.shields.io/badge/iOS-10.0%2B-blue.svg) ![Swift](https://img.shields.io/badge/Swift-4-blue.svg) [![Build Status](https://travis-ci.org/adimango/insights-for-instagram.svg?branch=master)](https://travis-ci.org/adimango/insights-for-instagram) ![](screenshots/app-github-header.png) ### Quick Start Want to get the app running? Run this in your shell: ```sh git clone https://github.com/adimango/insights-for-instagram.git cd insights-for-instagram pod install open insights-for-instagram.xcworkspace ``` You will have a running version of the insights-for-instagram app by hitting `Build > Run`. ### Features * Passwordless Reports, simply add your Instagram username, and start seeing reports right away * Best Engagement, insights for Instagram gives you information about which posts resonate better than others * Top Most Commented and Liked, the posts that generated the most comments or likes ### Planned features * Best Day to Post, find out what your best time to post on Instagram is * Hashtag Performance, discover which hashtags deliver the most engagement ### Questions If you have questions about any aspect of this project, please feel free to [open an issue](https://github.com/adimango/insights-for-instagram/issues/new). ### Credits - [Moya][]: network abstraction layer written in Swift - [Realm][]: data layer written in Swift - [Quick][]: behavior-driven development framework for Swift ### License MIT License. See [LICENSE](LICENSE). [Moya]:https://github.com/Moya/Moya [Realm]:https://realm.io/docs/swift/latest/ [Quick]:https://github.com/Quick/Quick ## Updates ### Apr 04, 2018: Banned private API Instagram has banned getting data from `https://www.instagram.com/{username}/?__a=1` With a bit of javascript we can still get the same json as before, but there are not workarounds yet for the get items queries. ``` // GET /api/users/:user_name -> returns user account details // Below an example with National Geographic https://insights-for-instagram.herokuapp.com/api/users/natgeo ``` ### Dec 09, 2017: Back on Track The app is using a new Instragram API proxy, developed using `/graphql/query` and some web API params. ``` // GET /api/users/:user_name/media -> returns media // Below an example with National Geographic https://insights-for-instagram.herokuapp.com/api/users/natgeo/media ``` ### Dec 01, 2017: Breaking Changes The Instragram API media endpoints now returns to 404-pages. After more of 20k downloads in just few month, Instagram removed the public API. However, the advanced queries are still available and a workaround will be push soon! ================================================ FILE: fastlane/Appfile ================================================ # The Appfile can be used to specify information that's used across all fastlane # tools, like your username or the app's bundle identifier. # # For more details, check out the documentation at: # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Appfile.md # app_identifier "me.alexdimango.insights-for-instagram" # apple_id "alex.dimango@gmail.com" ================================================ FILE: fastlane/Fastfile ================================================ # More documentation about how to customize your build # can be found here: # https://docs.fastlane.tools fastlane_version "1.109.0" default_platform :ios desc "Release a new beta version on Testflight" desc "This action does the following:" desc "" desc "- Ensures a clean git status" desc "- Increment the build number" desc "- Build and sign the app" desc "- Upload the ipa file to Testflight" desc "- Commit and push the version bump" lane :beta do # make sure we start off with a clean slate ensure_git_status_clean # increment build number increment_build_number # increment to the specified version number version = increment_version_number # build your iOS app gym( # scheme: "insights-for-instagram", export_method: "app-store" ) # upload to Testflight pilot(skip_waiting_for_build_processing: true) # make sure our directory is clean, except for changes Fastlane has made clean_build_artifacts # tag release and push to GitHub sh "git add .. ; git commit -m 'Deploying version #{version}.'" add_git_tag tag: version push_to_git_remote end error do |lane, exception| if lane == :deploy puts "Unable to deploy, resetting git repository." clean_build_artifacts reset_git_repo end end ================================================ FILE: fastlane/README.md ================================================ fastlane documentation ================ # Installation Make sure you have the latest version of the Xcode command line tools installed: ``` xcode-select --install ``` ## Choose your installation method:
Homebrew Installer Script Rubygems
macOS macOS macOS or Linux with Ruby 2.0.0 or above
brew cask install fastlane Download the zip file. Then double click on the install script (or run it in a terminal window). sudo gem install fastlane -NV
# Available Actions ### beta ``` fastlane beta ``` Release a new beta version on Testflight This action does the following: - Ensures a clean git status - Increment the build number - Build and sign the app - Upload the ipa file to Testflight - Commit and push the version bump ---- This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). ================================================ FILE: insights-for-instagram/Account/AccountViewController.swift ================================================ import UIKit class AccountViewController: UITableViewController { // MARK: - Properties @IBOutlet weak var accountNameTableCell: UITableViewCell! // MARK: - View lifecycle override func viewWillAppear(_ animated: Bool) { tableView.tableFooterView = UIView() guard let name = AppUserAccount().name else { accountNameTableCell.textLabel?.text = NSLocalizedString("Account Name", comment: "") return } accountNameTableCell.textLabel?.text = name } override func viewDidDisappear(_ animated: Bool) { accountNameTableCell.isSelected = false } // MARK: - Table view data source override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if indexPath.row > 1 { let cell = tableView.cellForRow(at: indexPath) cell?.isSelected = false diplayDeleteAllReportsAlert() } } // MARK: - Actions private func diplayDeleteAllReportsAlert() { let alertController = UIAlertController(title: AppConfiguration.Messages.deleteReportsTitle, message: AppConfiguration.Messages.deleteReportsMessage, preferredStyle: UIAlertControllerStyle.alert) let deleteAllAction = UIAlertAction(title: AppConfiguration.Messages.deleteAllButton, style: UIAlertActionStyle.destructive) { (_: UIAlertAction) -> Void in self.accountNameTableCell.textLabel?.text = NSLocalizedString("Account Name", comment: "") DataService.deleteAll() } let cancelAction = UIAlertAction(title: AppConfiguration.Messages.cancelButton, style: UIAlertActionStyle.default) { (_: UIAlertAction) -> Void in } alertController.addAction(cancelAction) alertController.addAction(deleteAllAction) present(alertController, animated: true, completion: nil) } } ================================================ FILE: insights-for-instagram/Account/AddAccountInteractor.swift ================================================ import UIKit class AddAccountInteractor { // MARK: - Properties var presenter: AddAccountPresenter? // MARK: - Storing/updating account logic func validateAccount(with userName: String) { presenter?.presentLoadingIndicator() if AppUserAccount().name != userName { //remove old account data DataService.deleteAll() } DataService.media(for: userName) { (error) in guard let error = error else { self.presenter?.presentReportsCompleted() NotificationCenter.default.post(name: AppConfiguration.DefaultsNotifications.reload, object: nil) return } self.stopLoading(with: error) } } func loadAccount() { guard let name = AppUserAccount().name else { presenter?.presentAddAccount() return } presenter?.presentUpdateAccount(account: name) } func deleteAccount() { presenter?.presentAddAccount() DataService.deleteAll() } private func stopLoading(with message: String) { presenter?.presentAlertController(title: AppConfiguration.Messages.somethingWrongMessage, message: message) } } ================================================ FILE: insights-for-instagram/Account/AddAccountPresenter.swift ================================================ import Foundation import UIKit class AddAccountPresenter { // MARK: - Properties weak var viewController: AddAccountDisplayLogic? // MARK: AddCoountPresentation Logic func presentAddAccount() { let leftBarButton = UIBarButtonItem() leftBarButton.title = "Account" let rightBarButton = UIBarButtonItem(title: "Done", style: .plain, target: viewController, action: #selector(AddAccountViewController.doneTapped)) self.viewController?.diplayAddAccount(with: leftBarButton, rightBarButton: rightBarButton) } func presentLoadingIndicator() { let activityIndicator = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 20, height: 20)) activityIndicator.color = UIColor.black activityIndicator.startAnimating() let leftBarButton = UIBarButtonItem(title: "Cancel", style: .plain, target: viewController, action: #selector(AddAccountViewController.cancelTapped)) let rightBarButton = UIBarButtonItem(customView: activityIndicator) viewController?.diplayLoadingAccount(with: leftBarButton, rightBarButton: rightBarButton) } func presentUpdateAccount(account: String) { let leftBarButton = UIBarButtonItem() leftBarButton.title = "Account" let rightBarButton = UIBarButtonItem(title: "Done", style: .plain, target: viewController, action: #selector(AddAccountViewController.doneTapped)) viewController?.diplayUpdateAccount(with: account, leftBarButton: leftBarButton, rightBarButton: rightBarButton) } func presentReportsCompleted() { presentAddAccount() viewController?.diplayAlert(title: AppConfiguration.Messages.reportsCompletedTitle, message: AppConfiguration.Messages.reportsCompletedMessage) } // MARK: - Present Alert Controller func presentAlertController(title: String, message: String) { presentAddAccount() viewController?.diplayAlert(title: title, message: message) } } ================================================ FILE: insights-for-instagram/Account/AddAccountViewController.swift ================================================ import UIKit protocol AddAccountDisplayLogic: class { func diplayAddAccount(with leftBarButton: UIBarButtonItem, rightBarButton: UIBarButtonItem) func diplayLoadingAccount(with leftBarButton: UIBarButtonItem, rightBarButton: UIBarButtonItem) func diplayUpdateAccount(with accountName: String, leftBarButton: UIBarButtonItem, rightBarButton: UIBarButtonItem) func diplayAlert(title: String, message: String) } class AddAccountViewController: UIViewController, AddAccountDisplayLogic { // MARK: - Properties @IBOutlet weak var usernameTextfield: UITextField! var interactor: AddAccountInteractor? var presenter: AddAccountPresenter? // MARK: Object lifecycle required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } // MARK: Setup private func setup() { let viewController = self let interactor = AddAccountInteractor() let presenter = AddAccountPresenter() viewController.interactor = interactor viewController.presenter = presenter interactor.presenter = presenter presenter.viewController = viewController } // MARK: - View lifecycle override func viewDidLoad() { super.viewDidLoad() usernameTextfield.becomeFirstResponder() interactor?.loadAccount() } // MARK: - Actions @objc func doneTapped() { guard let username = usernameTextfield.text, !username.isEmpty else { return } usernameTextfield.resignFirstResponder() interactor?.validateAccount(with: username) } @objc func cancelTapped() { usernameTextfield.text = "" interactor?.deleteAccount() } // MARK: - AddAccountDisplayLogic func diplayAddAccount(with leftBarButton: UIBarButtonItem, rightBarButton: UIBarButtonItem) { usernameTextfield.isEnabled = true navigationItem.hidesBackButton = false navigationItem.setLeftBarButton(nil, animated: true) navigationItem.setRightBarButton(rightBarButton, animated: true) } func diplayLoadingAccount(with leftBarButton: UIBarButtonItem, rightBarButton: UIBarButtonItem) { usernameTextfield.isEnabled = false navigationItem.setLeftBarButton(leftBarButton, animated: true) navigationItem.setRightBarButton(rightBarButton, animated: true) } func diplayUpdateAccount(with accountName: String, leftBarButton: UIBarButtonItem, rightBarButton: UIBarButtonItem) { usernameTextfield.text = accountName usernameTextfield.isEnabled = true usernameTextfield.becomeFirstResponder() navigationItem.hidesBackButton = false navigationItem.setLeftBarButton(nil, animated: true) navigationItem.setRightBarButton(rightBarButton, animated: true) } func diplayAlert(title: String, message: String) { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let alertAction = UIAlertAction(title: AppConfiguration.Messages.okButton, style: .default, handler: nil) alertController.addAction(alertAction) present(alertController, animated: true, completion: nil) } } ================================================ FILE: insights-for-instagram/App/AppConfiguration.swift ================================================ import Foundation class AppConfiguration { // MARK: - AppConfiguration.TableViewSection struct TableViewSections { static let zero = NSLocalizedString("Best Engagement", comment: "") static let one = NSLocalizedString("Top 25 Most Commented", comment: "") static let two = NSLocalizedString("Recently Posted", comment: "") } // MARK: - AppConfiguration.TableViewCellIdentifiers struct TableViewCellIdentifiers { static let cell = "cellID" } struct DefaultsNotifications { static let reload = Notification.Name(rawValue: "reload") } // MARK: - AppConfiguration.Messages struct Messages { static let okButton = NSLocalizedString("OK", comment: "") static let doneButton = NSLocalizedString("Done", comment: "") static let cancelButton = NSLocalizedString("Cancel", comment: "") static let deleteAllButton = NSLocalizedString("Delete All", comment: "") static let somethingWrongMessage = NSLocalizedString("Something went wrong", comment: "") static let privateAccountMessage = NSLocalizedString("This account is private", comment: "") static let reportsCompletedTitle = NSLocalizedString("Reports Completed", comment: "") static let reportsCompletedMessage = NSLocalizedString("Please go back to the Insights screen", comment: "") static let deleteReportsTitle = NSLocalizedString("Delete Reports", comment: "") static let deleteReportsMessage = NSLocalizedString("Remove my account and delete all the reports", comment: "") } } ================================================ FILE: insights-for-instagram/App/AppDelegate.swift ================================================ import UIKit import RealmSwift @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { setupAppearance() return true } // MARK: Setup UIAppearance private func setupAppearance() { UINavigationBar.appearance().shadowImage = UIImage() UINavigationBar.appearance().setBackgroundImage(UIImage(), for: .default) UINavigationBar.appearance().tintColor = UIColor.red UITabBar.appearance().tintColor = UIColor.init(red: 233/255, green: 79/255, blue: 97/255, alpha: 1) } } ================================================ FILE: insights-for-instagram/App/AppExtensions.swift ================================================ import UIKit // MARK: - Formatter extension Formatter { static let withPoint: NumberFormatter = { let formatter = NumberFormatter() formatter.groupingSeparator = "." formatter.numberStyle = .decimal return formatter }() } // MARK: - Integer extension BinaryInteger { var formattedWithPoint: String { return Formatter.withPoint.string(for: self) ?? "" } } // MARK: - String extension String { /// -returns: A string without white spaces and new lines. func trim() -> String { return self.trimmingCharacters(in: .whitespacesAndNewlines) } } ================================================ FILE: insights-for-instagram/App/AppUserAccount.swift ================================================ import Foundation class AppUserAccount { enum DefaultsKeys: String { case UserAccountKey } // MARK: - Initializers let defaults: UserDefaults init(defaults: UserDefaults) { self.defaults = defaults } init() { self.defaults = UserDefaults.standard } // MARK: - Properties var name: String? { get { let key = defaults.string(forKey: DefaultsKeys.UserAccountKey.rawValue) return key } set(newName) { guard let name = newName else { defaults.removeObject(forKey: DefaultsKeys.UserAccountKey.rawValue) defaults.synchronize() return } defaults.set(name, forKey: DefaultsKeys.UserAccountKey.rawValue) defaults.synchronize() } } } ================================================ FILE: insights-for-instagram/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x-1.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "insights-29.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "insights-58.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "insights-87.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x-1.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "57x57", "idiom" : "iphone", "filename" : "Icon-App-57x57@1x.png", "scale" : "1x" }, { "size" : "57x57", "idiom" : "iphone", "filename" : "Icon-App-57x57@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x-1.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x-2.png", "scale" : "2x" }, { "size" : "50x50", "idiom" : "ipad", "filename" : "Icon-Small-50x50@1x.png", "scale" : "1x" }, { "size" : "50x50", "idiom" : "ipad", "filename" : "Icon-Small-50x50@2x.png", "scale" : "2x" }, { "size" : "72x72", "idiom" : "ipad", "filename" : "Icon-App-72x72@1x.png", "scale" : "1x" }, { "size" : "72x72", "idiom" : "ipad", "filename" : "Icon-App-72x72@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "iTunesArtwork@2x.png", "scale" : "1x" }, { "size" : "24x24", "idiom" : "watch", "scale" : "2x", "role" : "notificationCenter", "subtype" : "38mm" }, { "size" : "27.5x27.5", "idiom" : "watch", "scale" : "2x", "role" : "notificationCenter", "subtype" : "42mm" }, { "size" : "29x29", "idiom" : "watch", "filename" : "Icon-App-29x29@2x.png", "role" : "companionSettings", "scale" : "2x" }, { "size" : "29x29", "idiom" : "watch", "filename" : "Icon-App-29x29@3x.png", "role" : "companionSettings", "scale" : "3x" }, { "size" : "40x40", "idiom" : "watch", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x", "role" : "appLauncher", "subtype" : "38mm" }, { "size" : "86x86", "idiom" : "watch", "filename" : "Icon-86@2x.png", "scale" : "2x", "role" : "quickLook", "subtype" : "38mm" }, { "size" : "98x98", "idiom" : "watch", "filename" : "Icon-98@2x.png", "scale" : "2x", "role" : "quickLook", "subtype" : "42mm" }, { "idiom" : "watch-marketing", "size" : "1024x1024", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: insights-for-instagram/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: insights-for-instagram/Assets.xcassets/chevron_location_.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "chevron_location_@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "chevron_location_@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: insights-for-instagram/Assets.xcassets/icons8-Male User Filled_30.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icons8-Male User Filled_30.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "icons8-Male User Filled_60.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: insights-for-instagram/Assets.xcassets/line-chart-graph.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icons8-Line Chart_30.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "icons8-Line Chart_60.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: insights-for-instagram/Assets.xcassets/placeHolder.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "test.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: insights-for-instagram/Assets.xcassets/user-icon.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icons8-Male User Filled-50.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "icons8-Male User Filled-100.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: insights-for-instagram/Base.lproj/Main.storyboard ================================================ ================================================ FILE: insights-for-instagram/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName Insights CFBundlePackageType APPL CFBundleShortVersionString 1.0.7 CFBundleVersion 10 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UIRequiresFullScreen UIStatusBarTintParameters UINavigationBar Style UIBarStyleDefault Translucent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: insights-for-instagram/Insights/InsightsInteractor.swift ================================================ import UIKit class InsightsInteractor { // MARK: - Properties var presenter: InstagramMediaPresentation? // MARK: Object lifecycle init() { NotificationCenter.default.addObserver(self, selector: #selector(InsightsInteractor.loadMedia), name: AppConfiguration.DefaultsNotifications.reload, object: nil) } deinit { NotificationCenter.default.removeObserver(self) } // MARK: - Load media @objc func loadMedia() { guard let userName = AppUserAccount().name else { loadEmptyMedia() return } loadStoredMedia() DataService.media(for: userName) { (error) in if error == nil { self.loadStoredMedia() } } } func loadEmptyMedia() { presenter?.presentNoAccountSections() } func loadStoredMedia() { let bestEngagement = DataService.bestEngagement(with: 25) let lastWeeksPosted = DataService.lastWeeksPosted(weeks: 12) let topMostCommented = DataService.mostLiked(with: 25) let bestEngagementDictionary: [String: Any] = ["sectionTitle": AppConfiguration.TableViewSections.zero, "items": bestEngagement] let mostCommentedDictionary: [String: Any] = ["sectionTitle": AppConfiguration.TableViewSections.one, "items": topMostCommented] let lastWeeksPostedDictionary: [String: Any] = ["sectionTitle": AppConfiguration.TableViewSections.two, "items": lastWeeksPosted] presenter?.presentLoadedSections(with: [bestEngagementDictionary, mostCommentedDictionary, lastWeeksPostedDictionary]) } func loadFetchMediaFailureAlert(error: Error) { presenter?.presentAlertController(with: error.localizedDescription) } } ================================================ FILE: insights-for-instagram/Insights/InsightsPresenter.swift ================================================ import Foundation import UIKit struct InstagramMediaSection { let sectionTitle: String let instagramMediaViews: [InstagramMediaView] } struct InstagramMediaView { let likes: String let comments: String let imageURL: String } protocol InstagramMediaPresentation { func presentLoadedSections (with items: [[String: Any]]) func presentNoAccountSections () func presentAlertController(with message: String) } class InsightsPresenter: InstagramMediaPresentation { // MARK: - Properties weak var viewController: InsightsViewDisplayLogic? // MARK: - Present fetched media func presentLoadedSections(with items: [[String: Any]]) { var instagramMediaSections = [InstagramMediaSection]() for section in items { guard let sectionTitle = section["sectionTitle"] as? String, let instagramMedias = section["items"] as? [InstagramMedia] else { return } var instagramMediaViews = [InstagramMediaView]() for item in instagramMedias { let itemView = InstagramMediaView(likes: NSLocalizedString("Likes: ", comment: "")+"\(item.likesCount.formattedWithPoint)", comments: NSLocalizedString("Comments: ", comment: "")+"\(item.commentsCount.formattedWithPoint)", imageURL: item.imageUrl) instagramMediaViews.append(itemView) } let instagramMediaSection = InstagramMediaSection(sectionTitle: sectionTitle, instagramMediaViews: instagramMediaViews) instagramMediaSections.append(instagramMediaSection) } let weekday = DataService.weekday() viewController?.diplayFetchedMedia(instagramMediaSections: instagramMediaSections, weekday: weekday) } // MARK: - Present no account UI func presentNoAccountSections() { let instagramMediaSections = createPlaceHolderSection() viewController?.diplayFetchedMedia(instagramMediaSections: instagramMediaSections, weekday: NSLocalizedString("Weekday.", comment: "")) } private func createPlaceHolderSection() -> [InstagramMediaSection] { let item = InstagramMediaView(likes: NSLocalizedString("Likes", comment: ""), comments: NSLocalizedString("Comments", comment: ""), imageURL: "") let items = Array(repeating: item, count: 3) let instagramItemsSection_0 = InstagramMediaSection(sectionTitle: AppConfiguration.TableViewSections.zero, instagramMediaViews: items) let instagramItemsSection_1 = InstagramMediaSection(sectionTitle: AppConfiguration.TableViewSections.one, instagramMediaViews: items) let instagramItemsSection_2 = InstagramMediaSection(sectionTitle: AppConfiguration.TableViewSections.two, instagramMediaViews: items) return [instagramItemsSection_0, instagramItemsSection_1, instagramItemsSection_2] } // MARK: - Present Alert Controller func presentAlertController(with message: String) { viewController?.diplayFetchMediaFailureAlert(title: AppConfiguration.Messages.somethingWrongMessage, message: message) } } ================================================ FILE: insights-for-instagram/Insights/InsightsViewController.swift ================================================ import UIKit protocol InsightsViewDisplayLogic: class { func diplayFetchedMedia(instagramMediaSections: [InstagramMediaSection], weekday: String) func diplayFetchMediaFailureAlert(title: String, message: String) } class InsightsViewController: UITableViewController, InsightsViewDisplayLogic { // MARK: - Properties var interactor: InsightsInteractor? var presenter: InstagramMediaPresentation? var sections: [InstagramMediaSection]? var storedOffsets = [Int: CGFloat]() @IBOutlet weak var footerView: UIView? @IBOutlet weak var weekdayLabel: UILabel! @IBOutlet weak var refreshController: UIRefreshControl! // MARK: Object lifecycle required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } // MARK: Setup private func setup() { let viewController = self let interactor = InsightsInteractor() let presenter = InsightsPresenter() viewController.interactor = interactor viewController.presenter = presenter interactor.presenter = presenter presenter.viewController = viewController sections = [] } // MARK: - View lifecycle override func viewDidLoad() { super.viewDidLoad() setupUI() fetchMediaOnload() } // MARK: - SetupUI private func setupUI() { tableView.register(InstagramMediaSectionTableViewCell.self, forCellReuseIdentifier: AppConfiguration.TableViewCellIdentifiers.cell) tableView.rowHeight = UITableViewAutomaticDimension tableView.estimatedRowHeight = 200 tableView.separatorStyle = .none self.footerView?.isHidden = true } // MARK: - Fetch Media func fetchMediaOnload() { DispatchQueue.global(qos: .background).async { self.interactor?.loadMedia() } } // MARK: - Table view data source override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard let sections = sections else { return 0 } return sections.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: AppConfiguration.TableViewCellIdentifiers.cell, for: indexPath) as? InstagramMediaSectionTableViewCell else { return InstagramMediaSectionTableViewCell() } let instagramItemsSection = self.sections?[indexPath.row] cell.setCollectionViewDataSourceDelegate(self, forRow: indexPath.row) cell.collectionViewOffset = storedOffsets[indexPath.row] ?? 0 cell.sectionNameLabel.text = instagramItemsSection?.sectionTitle return cell } override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { guard let tableViewCell = cell as? InstagramMediaSectionTableViewCell else { return } storedOffsets[indexPath.row] = tableViewCell.collectionViewOffset } // MARK: - Actions @IBAction func refresh(_ sender: Any) { fetchMediaOnload() } @IBAction func displayAccount(_ sender: Any) { if let tabBarController = UIApplication.shared.keyWindow?.rootViewController as? UITabBarController { tabBarController.selectedIndex = 1 } } func diplayFetchedMedia(instagramMediaSections: [InstagramMediaSection], weekday: String) { self.sections = instagramMediaSections DispatchQueue.main.async { self.footerView?.isHidden = false self.weekdayLabel.text = weekday self.refreshController.endRefreshing() self.tableView.reloadData() } } func diplayFetchMediaFailureAlert(title: String, message: String) { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let alertAction = UIAlertAction(title: AppConfiguration.Messages.okButton, style: .default, handler: nil) alertController.addAction(alertAction) self.present(alertController, animated: true, completion: nil) } } extension InsightsViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { // MARK: - Collections view data source func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { let index = collectionView.tag let instagramMediaSection = self.sections?[index] return (instagramMediaSection?.instagramMediaViews.count) ?? 0 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let index = collectionView.tag let instagramMediaSection = self.sections?[index] let media = instagramMediaSection?.instagramMediaViews[indexPath.row] guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AppConfiguration.TableViewCellIdentifiers.cell, for: indexPath) as? InstagramMediaCollectionViewCell else { return InstagramMediaCollectionViewCell() } cell.mediaModelView = media return cell } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let h = collectionView.frame.height - 10 return CGSize(width: 324/2, height: h) } } ================================================ FILE: insights-for-instagram/Insights/Views/InstagramMediaCollectionViewCell.swift ================================================ import UIKit import Kingfisher class InstagramMediaCollectionViewCell: UICollectionViewCell { // MARK: - Properties var mediaModelView: InstagramMediaView? { didSet { updateViews() } } let imageView: UIImageView = { let image = UIImageView() image.contentMode = .scaleAspectFill image.layer.cornerRadius = 4 image.layer.masksToBounds = true image.image = UIImage(named: "placeHolder") return image }() let likesLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 14, weight: UIFont.Weight.regular) return label }() let commentsLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 14, weight: UIFont.Weight.regular) label.textColor = UIColor.lightGray return label }() // MARK: Object lifecycle override init(frame: CGRect) { super.init(frame: frame) setupViews() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setupViews() { addSubview(imageView) addSubview(likesLabel) addSubview(commentsLabel) imageView.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.width) likesLabel.frame = CGRect(x: 0, y: frame.width + 6, width: frame.width, height: 16) commentsLabel.frame = CGRect(x: 0, y: frame.width + 25, width: frame.width, height: 16) } func updateViews() { self.likesLabel.text = mediaModelView?.likes self.commentsLabel.text = mediaModelView?.comments if let imageUrl = mediaModelView?.imageURL, let url = URL(string: imageUrl) { self.imageView.kf.setImage(with: url) } } override func prepareForReuse() { imageView.image = UIImage(named: "placeHolder") } } ================================================ FILE: insights-for-instagram/Insights/Views/InstagramMediaSectionTableViewCell.swift ================================================ import Foundation import UIKit class InstagramMediaSectionTableViewCell: UITableViewCell { // MARK: Object lifecycle override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupView() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Properties let itemsCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.sectionInset = UIEdgeInsets(top: 0, left: 10, bottom: 10, right: 10) let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.backgroundColor = UIColor.clear collectionView.showsHorizontalScrollIndicator = false collectionView.translatesAutoresizingMaskIntoConstraints = false return collectionView }() let sectionNameLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 18, weight: UIFont.Weight.heavy) label.translatesAutoresizingMaskIntoConstraints = false return label }() let dividerLineView: UIView = { let view = UIView() view.backgroundColor = UIColor(white: 0.4, alpha: 0.4) view.translatesAutoresizingMaskIntoConstraints = false return view }() func setupView() { contentView.backgroundColor = UIColor.clear addSubview(itemsCollectionView) addSubview(dividerLineView) addSubview(sectionNameLabel) itemsCollectionView.addConstraint(NSLayoutConstraint(item: itemsCollectionView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: (420/2))) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-20-[sectionNameLabel]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["sectionNameLabel": sectionNameLabel])) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-20-[dividerLineView]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["dividerLineView": dividerLineView])) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-10-[itemsCollectionView]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["itemsCollectionView": itemsCollectionView])) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[dividerLineView(0.5)][sectionNameLabel(40)][itemsCollectionView]-20-|", options: NSLayoutFormatOptions(), metrics: nil, views: ["itemsCollectionView": itemsCollectionView, "dividerLineView": dividerLineView, "sectionNameLabel": sectionNameLabel])) itemsCollectionView.register(InstagramMediaCollectionViewCell.self, forCellWithReuseIdentifier: AppConfiguration.TableViewCellIdentifiers.cell) } } // MARK: UICollectionViewDataSource extension InstagramMediaSectionTableViewCell { //Following https://github.com/ashfurrow/ design func setCollectionViewDataSourceDelegate(_ dataSourceDelegate: D, forRow row: Int) { itemsCollectionView.delegate = dataSourceDelegate itemsCollectionView.dataSource = dataSourceDelegate itemsCollectionView.tag = row itemsCollectionView.setContentOffset(itemsCollectionView.contentOffset, animated: false) itemsCollectionView.reloadData() } var collectionViewOffset: CGFloat { set { itemsCollectionView.contentOffset.x = newValue } get { return itemsCollectionView.contentOffset.x } } } ================================================ FILE: insights-for-instagram/LaunchScreen.storyboard ================================================ ================================================ FILE: insights-for-instagram/Models/InstagramMedia.swift ================================================ import ObjectMapper import RealmSwift class InstagramMedia: Object, Mappable { @objc dynamic var id = "" @objc dynamic var code = "" @objc dynamic var type = "" @objc dynamic var imageUrl = "" @objc dynamic var createdTime = Date() @objc dynamic var weekday = 0 // https://developer.apple.com/documentation/foundation/nsdatecomponents/1410442-weekday @objc dynamic var likesCount = 0 @objc dynamic var commentsCount = 0 @objc dynamic var engagementCount = 0 // includes the total number of Instagram accounts that liked or commented a post override static func primaryKey() -> String? { return "id" } required convenience init?(map: Map) { self.init() } func mapping(map: Map) { id <- map["id"] code <- map["code"] imageUrl <- map["image_url"] createdTime <- (map["created_time"], DateTransform()) weekday = Calendar.current.component(.weekday, from: createdTime) likesCount <- map["likes_count"] commentsCount <- map["comments_count"] engagementCount <- map["engagement_count"] } } ================================================ FILE: insights-for-instagram/Networking/InstagramAPI.swift ================================================ import Foundation import Moya // MARK: - Provider setup struct Constant { static let baseURL = "https://insights-for-instagram.herokuapp.com/api/" } private func JSONResponseDataFormatter(_ data: Data) -> Data { do { let dataAsJSON = try JSONSerialization.jsonObject(with: data) let prettyData = try JSONSerialization.data(withJSONObject: dataAsJSON, options: .prettyPrinted) return prettyData } catch { return data // fallback to original data if it can't be serialized. } } let InstagramProvider = MoyaProvider(plugins: [NetworkLoggerPlugin(verbose: false, responseDataFormatter: JSONResponseDataFormatter)]) // MARK: - Provider support private extension String { var urlEscaped: String? { return self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) } } public enum Instagram { case userMedia(String) } extension Instagram: TargetType { public var baseURL: URL { return URL(string: Constant.baseURL)! } // swiftlint:disable:this force_unwrapping public var path: String { switch self { case .userMedia(let name): guard let name = name.urlEscaped else { return "" } return "users/\(name)/media" } } public var method: Moya.Method { return .get } public var parameterEncoding: ParameterEncoding { return URLEncoding.default } public var task: Task { return .requestPlain } public var validate: Bool { switch self { default: return false } } public var sampleData: Data { return Data() } public var headers: [String: String]? { return nil } } public func url(_ route: TargetType) -> String { return route.baseURL.appendingPathComponent(route.path).absoluteString } // MARK: - Response Handlers extension Moya.Response { func mapNSArray() throws -> [String: Any] { let any = try self.mapJSON() guard let array = any as? [String: Any] else { throw MoyaError.jsonMapping(self) } return array } } // MARK: - Provider support func stubbedResponse(_ filename: String) -> Data? { @objc class TestClass: NSObject { } let bundle = Bundle(for: TestClass.self) guard let path = bundle.path(forResource: filename, ofType: "json") else { return nil } return (try? Data(contentsOf: URL(fileURLWithPath: path))) } ================================================ FILE: insights-for-instagram/Services/DataService.swift ================================================ import RealmSwift enum Weekday: Int { case sunday = 1 case monday = 2 case tuesday = 3 case wednesday = 4 case thursday = 5 case friday = 6 case saturday = 7 } class DataService { static func media( for userName: String, completion: @escaping ( _ error: String?) -> Void) { InstagramProvider.request(.userMedia(userName)) { result in do { let response = try result.dematerialize() let value: [String: Any] = try response.mapNSArray() guard let items = value["data"] as? [[String: Any]], items.isEmpty == false else { completion(AppConfiguration.Messages.privateAccountMessage) return } AppUserAccount().name = userName importInstagramMedia(instagramMedia: items, completion: { completion(nil) }) } catch { completion(error.localizedDescription) } } } // Creates/updates media static func importInstagramMedia(instagramMedia: [[String: Any]], completion: @escaping () -> Void) { DispatchQueue.global().async { guard let realm = try? Realm() else { return } realm.beginWrite() for media in instagramMedia { guard let instagramMedia = InstagramMedia(JSON: media) else { continue } realm.add(instagramMedia, update: true) } try? realm.commitWrite() DispatchQueue.main.async { completion() } } } // Returns the top n most liked static func mostLiked (with limit: Int ) -> [InstagramMedia] { // paginating behavior isn’t necessary at all: https://realm.io/docs/swift/latest/#limiting-results guard let realm = try? Realm() else { return [] } let medias = realm.objects(InstagramMedia.self).sorted(byKeyPath: "commentsCount", ascending: false) if medias.count > limit { return Array(medias[0...limit]) } return Array(medias) } // Returns last (n) weeks posted media static func lastWeeksPosted (weeks: Int) -> [InstagramMedia] { guard let realm = try? Realm() else { return [] } guard let fromDate = Calendar.current.date(byAdding: .day, value: -(7 * weeks), to: Date()) else { return [] } let predicate = NSPredicate(format: "createdTime > %@", fromDate as NSDate) let medias = realm.objects(InstagramMedia.self).sorted(byKeyPath: "createdTime", ascending: false).filter(predicate) return Array(medias) } // Returns best Engagement static func bestEngagement (with limit: Int) -> [InstagramMedia] { guard let realm = try? Realm() else { return [] } let medias = realm.objects(InstagramMedia.self).sorted(byKeyPath: "engagementCount", ascending: false) if medias.count > limit { return Array(medias[0...limit]) } return Array(medias) } // Returns the oldest media stored locally static func instagramMediaIndex() -> (offset: String?, count: Int) { guard let realm = try? Realm() else { return (nil, 0) } let entries = realm.objects(InstagramMedia.self).sorted(byKeyPath: "createdTime", ascending: true) if entries.isEmpty == false { guard let offset = entries[0]["id"] as? String else { return (nil, 0) } let count = entries.count return (offset, count) } else { return (nil, 0) } } // Weekday // swiftlint:disable:next cyclomatic_complexity static func weekday() -> String { guard let realm = try? Realm() else { return "" } var predicateArray = [NSPredicate]() var groupbyArray = [[InstagramMedia]]() var summedEngagementArray = [(Int, Int)]() for weekday in 1...7 { let predicate = NSPredicate(format: "weekday == %d", weekday) predicateArray.append(predicate) } for weekday in predicateArray.indices { let medias = Array(realm.objects(InstagramMedia.self).filter(predicateArray[weekday])) groupbyArray.append(medias) } for weekday in groupbyArray.indices { let array = groupbyArray[weekday] summedEngagementArray.append(DataService.sumEngagement(for: array, day: weekday)) } summedEngagementArray = summedEngagementArray.sorted(by: { $0.0 > $1.0 }) switch summedEngagementArray[0].1 { case Weekday.sunday.rawValue: return NSLocalizedString("Sunday.", comment: "") case Weekday.monday.rawValue: return NSLocalizedString("Monday.", comment: "") case Weekday.tuesday.rawValue: return NSLocalizedString("Tuesday.", comment: "") case Weekday.wednesday.rawValue: return NSLocalizedString("Wednesday.", comment: "") case Weekday.thursday.rawValue: return NSLocalizedString("Thursday.", comment: "") case Weekday.friday.rawValue: return NSLocalizedString("Friday.", comment: "") case Weekday.saturday.rawValue: return NSLocalizedString("Saturday.", comment: "") default: return NSLocalizedString("Weekday.", comment: "") } } static func sumEngagement(for array: [InstagramMedia], day: Int) -> (Int, Int) { var engagmentSum = 0 for media in array { engagmentSum += media.engagementCount } return (engagmentSum, day + 1) } static func deleteAll() { AppUserAccount().name = nil guard let realm = try? Realm() else { return } try? realm.write { realm.deleteAll() } NotificationCenter.default.post(name: AppConfiguration.DefaultsNotifications.reload, object: nil) } } ================================================ FILE: insights-for-instagram.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 826DAD622CE2DE3A20EE8817 /* Pods_insights_for_instagramTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09F2374247510737687CC66A /* Pods_insights_for_instagramTests.framework */; }; 970EC5CA084F08EC732835A5 /* Pods_insights_for_instagram.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 802F0FF4317368326E258C85 /* Pods_insights_for_instagram.framework */; }; AD4142E31F6FD78E00A83795 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD4142E21F6FD78E00A83795 /* LaunchScreen.storyboard */; }; AD4142E41F6FD78E00A83795 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD4142E21F6FD78E00A83795 /* LaunchScreen.storyboard */; }; AD61BABD1F6BDBF900307E92 /* AddAccountPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD61BABC1F6BDBF900307E92 /* AddAccountPresenter.swift */; }; AD6C69081F390A4E00039B66 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD6C69061F390A4E00039B66 /* Main.storyboard */; }; AD6C690A1F390A4E00039B66 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AD6C69091F390A4E00039B66 /* Assets.xcassets */; }; AD7D8C371F73CCC60094697E /* InstagramAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7D8C361F73CCC60094697E /* InstagramAPITests.swift */; }; ADB36EDB1F7306DA002844FD /* InstagramMediaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB36EDA1F7306DA002844FD /* InstagramMediaTests.swift */; }; ADC34C1B20261413008E7875 /* InstagramMediaCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADC34C1A20261413008E7875 /* InstagramMediaCollectionViewCell.swift */; }; ADD71BCF1F6AE47800251445 /* InstagramAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71BCE1F6AE47800251445 /* InstagramAPI.swift */; }; ADD71BD31F6AE48500251445 /* InstagramMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71BD21F6AE48500251445 /* InstagramMedia.swift */; }; ADD71BDA1F6AE49B00251445 /* InsightsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71BD61F6AE49B00251445 /* InsightsInteractor.swift */; }; ADD71BDD1F6AE49B00251445 /* InsightsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71BD71F6AE49B00251445 /* InsightsPresenter.swift */; }; ADD71BE01F6AE49B00251445 /* InsightsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71BD81F6AE49B00251445 /* InsightsViewController.swift */; }; ADD71BE31F6AE49B00251445 /* InstagramMediaSectionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71BD91F6AE49B00251445 /* InstagramMediaSectionTableViewCell.swift */; }; ADD71BEC1F6AE4AC00251445 /* AppUserAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71BE61F6AE4AC00251445 /* AppUserAccount.swift */; }; ADD71BF21F6AE4AC00251445 /* DataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71BE81F6AE4AC00251445 /* DataService.swift */; }; ADD71BF51F6AE4AC00251445 /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71BE91F6AE4AC00251445 /* AppConfiguration.swift */; }; ADD71BF81F6AE4AC00251445 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71BEA1F6AE4AC00251445 /* AppDelegate.swift */; }; ADD71BFB1F6AE4AC00251445 /* AppExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71BEB1F6AE4AC00251445 /* AppExtensions.swift */; }; ADD71C021F6AE4C100251445 /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71BFE1F6AE4C100251445 /* AccountViewController.swift */; }; ADD71C081F6AE4C100251445 /* AddAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71C001F6AE4C100251445 /* AddAccountViewController.swift */; }; ADD71C0B1F6AE4C100251445 /* AddAccountInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD71C011F6AE4C100251445 /* AddAccountInteractor.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ AD6C69141F390A4F00039B66 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AD6C68F51F390A4E00039B66 /* Project object */; proxyType = 1; remoteGlobalIDString = AD6C68FC1F390A4E00039B66; remoteInfo = "insights-for-instagram"; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 09F2374247510737687CC66A /* Pods_insights_for_instagramTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_insights_for_instagramTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1879247EFBB7E37B2DF8A95E /* Pods-insights-for-instagramTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-insights-for-instagramTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-insights-for-instagramTests/Pods-insights-for-instagramTests.release.xcconfig"; sourceTree = ""; }; 25CF554A7A2BE8E283AEC682 /* Pods-insights-for-instagram.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-insights-for-instagram.debug.xcconfig"; path = "Pods/Target Support Files/Pods-insights-for-instagram/Pods-insights-for-instagram.debug.xcconfig"; sourceTree = ""; }; 7FADBF9E959DB8F02D5624CA /* Pods-insights-for-instagramTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-insights-for-instagramTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-insights-for-instagramTests/Pods-insights-for-instagramTests.debug.xcconfig"; sourceTree = ""; }; 802F0FF4317368326E258C85 /* Pods_insights_for_instagram.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_insights_for_instagram.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A7CD320FF647674A7285FBC0 /* Pods-insights-for-instagram.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-insights-for-instagram.release.xcconfig"; path = "Pods/Target Support Files/Pods-insights-for-instagram/Pods-insights-for-instagram.release.xcconfig"; sourceTree = ""; }; AD4142E21F6FD78E00A83795 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; AD61BABC1F6BDBF900307E92 /* AddAccountPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddAccountPresenter.swift; sourceTree = ""; }; AD6C68FD1F390A4E00039B66 /* insights-for-instagram.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "insights-for-instagram.app"; sourceTree = BUILT_PRODUCTS_DIR; }; AD6C69071F390A4E00039B66 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; AD6C69091F390A4E00039B66 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; AD6C690E1F390A4E00039B66 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AD6C69131F390A4F00039B66 /* insights-for-instagramTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "insights-for-instagramTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; AD6C69191F390A4F00039B66 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AD7D8C361F73CCC60094697E /* InstagramAPITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstagramAPITests.swift; sourceTree = ""; }; ADB36EDA1F7306DA002844FD /* InstagramMediaTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstagramMediaTests.swift; sourceTree = ""; }; ADC34C1A20261413008E7875 /* InstagramMediaCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstagramMediaCollectionViewCell.swift; sourceTree = ""; }; ADD71BCE1F6AE47800251445 /* InstagramAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstagramAPI.swift; sourceTree = ""; }; ADD71BD21F6AE48500251445 /* InstagramMedia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstagramMedia.swift; sourceTree = ""; }; ADD71BD61F6AE49B00251445 /* InsightsInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsightsInteractor.swift; sourceTree = ""; }; ADD71BD71F6AE49B00251445 /* InsightsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsightsPresenter.swift; sourceTree = ""; }; ADD71BD81F6AE49B00251445 /* InsightsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsightsViewController.swift; sourceTree = ""; }; ADD71BD91F6AE49B00251445 /* InstagramMediaSectionTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstagramMediaSectionTableViewCell.swift; sourceTree = ""; }; ADD71BE61F6AE4AC00251445 /* AppUserAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppUserAccount.swift; sourceTree = ""; }; ADD71BE81F6AE4AC00251445 /* DataService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataService.swift; sourceTree = ""; }; ADD71BE91F6AE4AC00251445 /* AppConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppConfiguration.swift; sourceTree = ""; }; ADD71BEA1F6AE4AC00251445 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; ADD71BEB1F6AE4AC00251445 /* AppExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppExtensions.swift; sourceTree = ""; }; ADD71BFE1F6AE4C100251445 /* AccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = ""; }; ADD71C001F6AE4C100251445 /* AddAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddAccountViewController.swift; sourceTree = ""; }; ADD71C011F6AE4C100251445 /* AddAccountInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddAccountInteractor.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ AD6C68FA1F390A4E00039B66 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 970EC5CA084F08EC732835A5 /* Pods_insights_for_instagram.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; AD6C69101F390A4F00039B66 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 826DAD622CE2DE3A20EE8817 /* Pods_insights_for_instagramTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ AD6C68F41F390A4E00039B66 = { isa = PBXGroup; children = ( AD6C68FE1F390A4E00039B66 /* Products */, AD6C68FF1F390A4E00039B66 /* insights-for-instagram */, AD6C69161F390A4F00039B66 /* insights-for-instagramTests */, FAD78C34808A08430765163F /* Pods */, F3AD435598555D6DC2ACAD81 /* Frameworks */, ); sourceTree = ""; }; AD6C68FE1F390A4E00039B66 /* Products */ = { isa = PBXGroup; children = ( AD6C68FD1F390A4E00039B66 /* insights-for-instagram.app */, AD6C69131F390A4F00039B66 /* insights-for-instagramTests.xctest */, ); name = Products; sourceTree = ""; }; AD6C68FF1F390A4E00039B66 /* insights-for-instagram */ = { isa = PBXGroup; children = ( ADD71B8A1F6AE36500251445 /* Account */, ADD71B891F6AE32A00251445 /* App */, ADD71B8B1F6AE36F00251445 /* Insights */, ADD71B8C1F6AE37C00251445 /* Models */, ADD71B8D1F6AE38500251445 /* Networking */, ADC34C192025FAD5008E7875 /* Services */, AD6C69091F390A4E00039B66 /* Assets.xcassets */, AD6C690E1F390A4E00039B66 /* Info.plist */, AD4142E21F6FD78E00A83795 /* LaunchScreen.storyboard */, AD6C69061F390A4E00039B66 /* Main.storyboard */, ); path = "insights-for-instagram"; sourceTree = ""; }; AD6C69161F390A4F00039B66 /* insights-for-instagramTests */ = { isa = PBXGroup; children = ( AD6C69191F390A4F00039B66 /* Info.plist */, ADB36EDA1F7306DA002844FD /* InstagramMediaTests.swift */, AD7D8C361F73CCC60094697E /* InstagramAPITests.swift */, ); path = "insights-for-instagramTests"; sourceTree = ""; }; ADC34C192025FAD5008E7875 /* Services */ = { isa = PBXGroup; children = ( ADD71BE81F6AE4AC00251445 /* DataService.swift */, ); path = Services; sourceTree = ""; }; ADC34C1C202615FF008E7875 /* Views */ = { isa = PBXGroup; children = ( ADD71BD91F6AE49B00251445 /* InstagramMediaSectionTableViewCell.swift */, ADC34C1A20261413008E7875 /* InstagramMediaCollectionViewCell.swift */, ); path = Views; sourceTree = ""; }; ADD71B891F6AE32A00251445 /* App */ = { isa = PBXGroup; children = ( ADD71BE91F6AE4AC00251445 /* AppConfiguration.swift */, ADD71BEA1F6AE4AC00251445 /* AppDelegate.swift */, ADD71BEB1F6AE4AC00251445 /* AppExtensions.swift */, ADD71BE61F6AE4AC00251445 /* AppUserAccount.swift */, ); path = App; sourceTree = ""; }; ADD71B8A1F6AE36500251445 /* Account */ = { isa = PBXGroup; children = ( ADD71BFE1F6AE4C100251445 /* AccountViewController.swift */, ADD71C011F6AE4C100251445 /* AddAccountInteractor.swift */, AD61BABC1F6BDBF900307E92 /* AddAccountPresenter.swift */, ADD71C001F6AE4C100251445 /* AddAccountViewController.swift */, ); path = Account; sourceTree = ""; }; ADD71B8B1F6AE36F00251445 /* Insights */ = { isa = PBXGroup; children = ( ADC34C1C202615FF008E7875 /* Views */, ADD71BD61F6AE49B00251445 /* InsightsInteractor.swift */, ADD71BD71F6AE49B00251445 /* InsightsPresenter.swift */, ADD71BD81F6AE49B00251445 /* InsightsViewController.swift */, ); path = Insights; sourceTree = ""; }; ADD71B8C1F6AE37C00251445 /* Models */ = { isa = PBXGroup; children = ( ADD71BD21F6AE48500251445 /* InstagramMedia.swift */, ); path = Models; sourceTree = ""; }; ADD71B8D1F6AE38500251445 /* Networking */ = { isa = PBXGroup; children = ( ADD71BCE1F6AE47800251445 /* InstagramAPI.swift */, ); path = Networking; sourceTree = ""; }; F3AD435598555D6DC2ACAD81 /* Frameworks */ = { isa = PBXGroup; children = ( 802F0FF4317368326E258C85 /* Pods_insights_for_instagram.framework */, 09F2374247510737687CC66A /* Pods_insights_for_instagramTests.framework */, ); name = Frameworks; sourceTree = ""; }; FAD78C34808A08430765163F /* Pods */ = { isa = PBXGroup; children = ( 25CF554A7A2BE8E283AEC682 /* Pods-insights-for-instagram.debug.xcconfig */, A7CD320FF647674A7285FBC0 /* Pods-insights-for-instagram.release.xcconfig */, 7FADBF9E959DB8F02D5624CA /* Pods-insights-for-instagramTests.debug.xcconfig */, 1879247EFBB7E37B2DF8A95E /* Pods-insights-for-instagramTests.release.xcconfig */, ); name = Pods; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ AD6C68FC1F390A4E00039B66 /* insights-for-instagram */ = { isa = PBXNativeTarget; buildConfigurationList = AD6C69271F390A4F00039B66 /* Build configuration list for PBXNativeTarget "insights-for-instagram" */; buildPhases = ( A841561F091F6E1A316F0B6D /* [CP] Check Pods Manifest.lock */, AD6C68F91F390A4E00039B66 /* Sources */, AD6C68FA1F390A4E00039B66 /* Frameworks */, AD6C68FB1F390A4E00039B66 /* Resources */, B4A905D47AD41D1CAD9E0B84 /* [CP] Embed Pods Frameworks */, E0E93F41DF7DC8CABFA0959C /* [CP] Copy Pods Resources */, ADBDCEB92022681300183499 /* ShellScript */, ); buildRules = ( ); dependencies = ( ); name = "insights-for-instagram"; productName = "insights-for-instagram"; productReference = AD6C68FD1F390A4E00039B66 /* insights-for-instagram.app */; productType = "com.apple.product-type.application"; }; AD6C69121F390A4F00039B66 /* insights-for-instagramTests */ = { isa = PBXNativeTarget; buildConfigurationList = AD6C692A1F390A4F00039B66 /* Build configuration list for PBXNativeTarget "insights-for-instagramTests" */; buildPhases = ( 5C69263025FD218D8650F06A /* [CP] Check Pods Manifest.lock */, AD6C690F1F390A4F00039B66 /* Sources */, AD6C69101F390A4F00039B66 /* Frameworks */, AD6C69111F390A4F00039B66 /* Resources */, 480D1B7517B89E5E86BC99CC /* [CP] Embed Pods Frameworks */, 71875759ADCF7CD1A3B52512 /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( AD6C69151F390A4F00039B66 /* PBXTargetDependency */, ); name = "insights-for-instagramTests"; productName = "insights-for-instagramTests"; productReference = AD6C69131F390A4F00039B66 /* insights-for-instagramTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ AD6C68F51F390A4E00039B66 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0830; LastUpgradeCheck = 0900; ORGANIZATIONNAME = "Alex Di Mango"; TargetAttributes = { AD6C68FC1F390A4E00039B66 = { CreatedOnToolsVersion = 8.3.3; DevelopmentTeam = 46BS3CW9YT; LastSwiftMigration = 0900; ProvisioningStyle = Automatic; }; AD6C69121F390A4F00039B66 = { CreatedOnToolsVersion = 8.3.3; DevelopmentTeam = 46BS3CW9YT; LastSwiftMigration = 0920; ProvisioningStyle = Automatic; TestTargetID = AD6C68FC1F390A4E00039B66; }; }; }; buildConfigurationList = AD6C68F81F390A4E00039B66 /* Build configuration list for PBXProject "insights-for-instagram" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = AD6C68F41F390A4E00039B66; productRefGroup = AD6C68FE1F390A4E00039B66 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( AD6C68FC1F390A4E00039B66 /* insights-for-instagram */, AD6C69121F390A4F00039B66 /* insights-for-instagramTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ AD6C68FB1F390A4E00039B66 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( AD4142E31F6FD78E00A83795 /* LaunchScreen.storyboard in Resources */, AD6C690A1F390A4E00039B66 /* Assets.xcassets in Resources */, AD6C69081F390A4E00039B66 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; AD6C69111F390A4F00039B66 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( AD4142E41F6FD78E00A83795 /* LaunchScreen.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 480D1B7517B89E5E86BC99CC /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-insights-for-instagramTests/Pods-insights-for-instagramTests-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Nimble/Nimble.framework", "${BUILT_PRODUCTS_DIR}/Quick/Quick.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nimble.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Quick.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-insights-for-instagramTests/Pods-insights-for-instagramTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 5C69263025FD218D8650F06A /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-insights-for-instagramTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 71875759ADCF7CD1A3B52512 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "[CP] Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-insights-for-instagramTests/Pods-insights-for-instagramTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; A841561F091F6E1A316F0B6D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-insights-for-instagram-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; ADBDCEB92022681300183499 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; }; B4A905D47AD41D1CAD9E0B84 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-insights-for-instagram/Pods-insights-for-instagram-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework", "${BUILT_PRODUCTS_DIR}/Moya/Moya.framework", "${BUILT_PRODUCTS_DIR}/ObjectMapper/ObjectMapper.framework", "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", "${BUILT_PRODUCTS_DIR}/RealmSwift/RealmSwift.framework", "${BUILT_PRODUCTS_DIR}/Result/Result.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Moya.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ObjectMapper.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RealmSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Result.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-insights-for-instagram/Pods-insights-for-instagram-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; E0E93F41DF7DC8CABFA0959C /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "[CP] Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-insights-for-instagram/Pods-insights-for-instagram-resources.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ AD6C68F91F390A4E00039B66 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ADD71BDD1F6AE49B00251445 /* InsightsPresenter.swift in Sources */, ADD71BF21F6AE4AC00251445 /* DataService.swift in Sources */, ADD71BE01F6AE49B00251445 /* InsightsViewController.swift in Sources */, ADD71BEC1F6AE4AC00251445 /* AppUserAccount.swift in Sources */, ADD71C021F6AE4C100251445 /* AccountViewController.swift in Sources */, ADD71C0B1F6AE4C100251445 /* AddAccountInteractor.swift in Sources */, ADD71BF51F6AE4AC00251445 /* AppConfiguration.swift in Sources */, ADD71C081F6AE4C100251445 /* AddAccountViewController.swift in Sources */, ADD71BF81F6AE4AC00251445 /* AppDelegate.swift in Sources */, ADD71BCF1F6AE47800251445 /* InstagramAPI.swift in Sources */, ADD71BE31F6AE49B00251445 /* InstagramMediaSectionTableViewCell.swift in Sources */, AD61BABD1F6BDBF900307E92 /* AddAccountPresenter.swift in Sources */, ADD71BDA1F6AE49B00251445 /* InsightsInteractor.swift in Sources */, ADD71BD31F6AE48500251445 /* InstagramMedia.swift in Sources */, ADC34C1B20261413008E7875 /* InstagramMediaCollectionViewCell.swift in Sources */, ADD71BFB1F6AE4AC00251445 /* AppExtensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; AD6C690F1F390A4F00039B66 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ADB36EDB1F7306DA002844FD /* InstagramMediaTests.swift in Sources */, AD7D8C371F73CCC60094697E /* InstagramAPITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ AD6C69151F390A4F00039B66 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = AD6C68FC1F390A4E00039B66 /* insights-for-instagram */; targetProxy = AD6C69141F390A4F00039B66 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ AD6C69061F390A4E00039B66 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( AD6C69071F390A4E00039B66 /* Base */, ); name = Main.storyboard; path = .; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ AD6C69251F390A4F00039B66 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.3; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; AD6C69261F390A4F00039B66 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.3; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; AD6C69281F390A4F00039B66 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 25CF554A7A2BE8E283AEC682 /* Pods-insights-for-instagram.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 46BS3CW9YT; INFOPLIST_FILE = "insights-for-instagram/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "me.alexdimango.insights-for-instagram"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; AD6C69291F390A4F00039B66 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = A7CD320FF647674A7285FBC0 /* Pods-insights-for-instagram.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 46BS3CW9YT; INFOPLIST_FILE = "insights-for-instagram/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "me.alexdimango.insights-for-instagram"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; AD6C692B1F390A4F00039B66 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7FADBF9E959DB8F02D5624CA /* Pods-insights-for-instagramTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; BUNDLE_LOADER = "$(TEST_HOST)"; DEVELOPMENT_TEAM = 46BS3CW9YT; INFOPLIST_FILE = "insights-for-instagramTests/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "me.alexdimango.insights-for-instagramTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 3.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/insights-for-instagram.app/insights-for-instagram"; }; name = Debug; }; AD6C692C1F390A4F00039B66 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 1879247EFBB7E37B2DF8A95E /* Pods-insights-for-instagramTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; BUNDLE_LOADER = "$(TEST_HOST)"; DEVELOPMENT_TEAM = 46BS3CW9YT; INFOPLIST_FILE = "insights-for-instagramTests/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "me.alexdimango.insights-for-instagramTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 3.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/insights-for-instagram.app/insights-for-instagram"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ AD6C68F81F390A4E00039B66 /* Build configuration list for PBXProject "insights-for-instagram" */ = { isa = XCConfigurationList; buildConfigurations = ( AD6C69251F390A4F00039B66 /* Debug */, AD6C69261F390A4F00039B66 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; AD6C69271F390A4F00039B66 /* Build configuration list for PBXNativeTarget "insights-for-instagram" */ = { isa = XCConfigurationList; buildConfigurations = ( AD6C69281F390A4F00039B66 /* Debug */, AD6C69291F390A4F00039B66 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; AD6C692A1F390A4F00039B66 /* Build configuration list for PBXNativeTarget "insights-for-instagramTests" */ = { isa = XCConfigurationList; buildConfigurations = ( AD6C692B1F390A4F00039B66 /* Debug */, AD6C692C1F390A4F00039B66 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = AD6C68F51F390A4E00039B66 /* Project object */; } ================================================ FILE: insights-for-instagram.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: insights-for-instagram.xcodeproj/xcshareddata/xcschemes/insights-for-instagram.xcscheme ================================================ ================================================ FILE: insights-for-instagram.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: insights-for-instagramTests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0.7 CFBundleVersion 10 ================================================ FILE: insights-for-instagramTests/InstagramAPITests.swift ================================================ import Quick import Nimble import Alamofire @testable import insights_for_instagram import Moya class InstagramAPITests: QuickSpec { override func spec() { var provider: MoyaProvider! beforeEach { provider = MoyaProvider(stubClosure: MoyaProvider.immediatelyStub) } it("returns stubbed data for user media request") { var json: String? let target: Instagram = .userMedia("") provider.request(target) { result in if case let .success(response) = result { json = String(data: response.data, encoding: .utf8) } } let sampleData = String(data: target.sampleData, encoding: .utf8) expect(json).to(equal(sampleData)) } } } ================================================ FILE: insights-for-instagramTests/InstagramMediaTests.swift ================================================ import Quick import Nimble import Result import Alamofire @testable import insights_for_instagram import ObjectMapper class InstagramMediaTests: QuickSpec { override func spec() { it("create from JSON") { let id = "id-xyz" let code = "xyx" let imageUrl = "" let createdTime = Date() let likesCount = 4 let commentsCount = 3 let engagementCount = 7 let json = ["id": id, "code": code, "image_url": imageUrl, "created_time": createdTime, "likes_count": likesCount, "comments_count": commentsCount, "engagement_count": engagementCount] as [String: Any] let media = InstagramMedia(JSON: json) expect(media?.id) == id expect(media?.code) == code expect(media?.likesCount) == 4 expect(media?.commentsCount) == 3 expect(media?.engagementCount) == 4 + 3 } }}