diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..171d573 --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +Compiled source + +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +Packages + +############ + +it's better to unpack these files and commit the raw source + +git has its own built in compression methods + +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +Logs and databases + +###################### +*.log +*.sql +*.sqlite + +OS generated files + +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +xcschememanagement.plist + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +*.xcuserstate +project.xcworkspace/ +xcuserdata/ + +## Build generated +build/ +DerivedData/ +UserInterfaceState.xcuserstate +/mobile-coding-challenge.xcodeproj/project.xcworkspace/xcuserdata/Talha.xcuserdatad/UserInterfaceState.xcuserstate + +# Carthage + +Carthage/Checkouts +Carthage/Build + +# Fastlane Generated Reports + +fastlane/test_output/report.html + +# Ruby Mine Workspace + +features/.idea/workspace.xml + diff --git a/Cartfile b/Cartfile new file mode 100644 index 0000000..748de6f --- /dev/null +++ b/Cartfile @@ -0,0 +1,6 @@ +#networking +github "Alamofire/Alamofire" +github "Alamofire/AlamofireImage" ~> 3.5 + +#json mapping +github "bignerdranch/Freddy" ~> 3.0 \ No newline at end of file diff --git a/Cartfile.resolved b/Cartfile.resolved new file mode 100644 index 0000000..21ca4f4 --- /dev/null +++ b/Cartfile.resolved @@ -0,0 +1,3 @@ +github "Alamofire/Alamofire" "4.8.2" +github "Alamofire/AlamofireImage" "3.5.2" +github "bignerdranch/Freddy" "3.0.3" diff --git a/README.md b/README.md index f83520b..75cd39b 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,16 @@ # Mobile Coding Challenge -## Idea of the App -The task is to implement a small app that will list the most starred Github repos that were created in the last 30 days. -You'll be fetching the sorted JSON data directly from the Github API (Github API explained down below). +In Order to Run the Project -## Features -* As a User I should be able to list the most starred Github repos that were created in the last 30 days. -* As a User I should see the results as a list. One repository per row. -* As a User I should be able to see for each repo/row the following details : - * Repository name - * Repository description - * Numbers of stars for the repo. - * Username and avatar of the owner. -* [BONUS] As a User I should be able to keep scrolling and new results should appear (pagination). +1. Install Carthage via [Homebrew][] + ```bash + $ brew update + $ brew install carthage -## Things to keep in mind 🚨 -* Features are less important than code quality. Put more focus on code quality and less on speed and number of features implemented. -* Your code will be evaluated based on: code structure, programming best practices, legibility (and not number of features implemented or speed). -* The git commit history (and git commit messages) will be also evaluated. -* Do not forget to include few details about the project in the README (e.g explain choice of libraries, how to run it ...) +2. Run `carthage update --platform iOS --no-use-binaries`. -## How to get the data from Github -To get the most starred Github repos created in the last 30 days (relative to 2017-11-22), you'll need to call the following endpoint : -`https://api.github.com/search/repositories?q=created:>2017-10-22&sort=stars&order=desc` - -The JSON data from Github will be paginated (you'll receive around 100 repos per JSON page). - -To get the 2nd page, you add `&page=2` to the end of your API request : - -`https://api.github.com/search/repositories?q=created:>2017-10-22&sort=stars&order=desc&page=2` - -To get the 3rd page, you add `&page=3` ... etc - -You can read more about the Github API over [here](https://developer.github.com/v3/search/#search-repositories -). - -## Mockups -![alt text](https://raw.githubusercontent.com/hiddenfounders/mobile-coding-challenge/master/mockup.png) - -Here's what each element represents : - -![alt text](https://raw.githubusercontent.com/hiddenfounders/mobile-coding-challenge/master/row-explained.png) - - -## Technologies to use -Choose whatever mobile platform you're most familiar with. - -* For iOS, feel free to use Swift or Objective-C. -* For Android, feel free to use Kotlin or Java. +## Technology used +* iOS, Swift. diff --git a/mobile-coding-challenge.xcodeproj/project.pbxproj b/mobile-coding-challenge.xcodeproj/project.pbxproj new file mode 100644 index 0000000..16c3c7d --- /dev/null +++ b/mobile-coding-challenge.xcodeproj/project.pbxproj @@ -0,0 +1,411 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + BCA085B622F52D8700AD4CE2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCA085B522F52D8700AD4CE2 /* AppDelegate.swift */; }; + BCA085B822F52D8700AD4CE2 /* TrendingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCA085B722F52D8700AD4CE2 /* TrendingViewController.swift */; }; + BCA085BB22F52D8700AD4CE2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BCA085B922F52D8700AD4CE2 /* Main.storyboard */; }; + BCA085BD22F52D8800AD4CE2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BCA085BC22F52D8800AD4CE2 /* Assets.xcassets */; }; + BCA085C022F52D8800AD4CE2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BCA085BE22F52D8800AD4CE2 /* LaunchScreen.storyboard */; }; + BCA085CF22F7A8B200AD4CE2 /* Freddy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BCA085CD22F7A8B200AD4CE2 /* Freddy.framework */; }; + BCA085D022F7A8B200AD4CE2 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BCA085CE22F7A8B200AD4CE2 /* Alamofire.framework */; }; + BCA085D522F7A95900AD4CE2 /* Entity.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCA085D422F7A95900AD4CE2 /* Entity.swift */; }; + BCA085D722F7ACAD00AD4CE2 /* WebService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCA085D622F7ACAD00AD4CE2 /* WebService.swift */; }; + BCA085E722F7DC8F00AD4CE2 /* AlamofireImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BCA085E622F7DC8F00AD4CE2 /* AlamofireImage.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + BCA085B222F52D8700AD4CE2 /* mobile-coding-challenge.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "mobile-coding-challenge.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + BCA085B522F52D8700AD4CE2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + BCA085B722F52D8700AD4CE2 /* TrendingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingViewController.swift; sourceTree = ""; }; + BCA085BA22F52D8700AD4CE2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + BCA085BC22F52D8800AD4CE2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + BCA085BF22F52D8800AD4CE2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + BCA085C122F52D8800AD4CE2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BCA085CD22F7A8B200AD4CE2 /* Freddy.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Freddy.framework; path = Carthage/Build/iOS/Freddy.framework; sourceTree = ""; }; + BCA085CE22F7A8B200AD4CE2 /* Alamofire.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Alamofire.framework; path = Carthage/Build/iOS/Alamofire.framework; sourceTree = ""; }; + BCA085D422F7A95900AD4CE2 /* Entity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Entity.swift; sourceTree = ""; }; + BCA085D622F7ACAD00AD4CE2 /* WebService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebService.swift; sourceTree = ""; }; + BCA085E622F7DC8F00AD4CE2 /* AlamofireImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AlamofireImage.framework; path = Carthage/Build/iOS/AlamofireImage.framework; sourceTree = ""; }; + BCA085E822F8760000AD4CE2 /* Reachability.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Reachability.framework; path = Carthage/Build/iOS/Reachability.framework; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + BCA085AF22F52D8700AD4CE2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BCA085E722F7DC8F00AD4CE2 /* AlamofireImage.framework in Frameworks */, + BCA085CF22F7A8B200AD4CE2 /* Freddy.framework in Frameworks */, + BCA085D022F7A8B200AD4CE2 /* Alamofire.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + BCA085A922F52D8700AD4CE2 = { + isa = PBXGroup; + children = ( + BCA085B422F52D8700AD4CE2 /* mobile-coding-challenge */, + BCA085B322F52D8700AD4CE2 /* Products */, + BCA085CC22F7A8B200AD4CE2 /* Frameworks */, + ); + sourceTree = ""; + }; + BCA085B322F52D8700AD4CE2 /* Products */ = { + isa = PBXGroup; + children = ( + BCA085B222F52D8700AD4CE2 /* mobile-coding-challenge.app */, + ); + name = Products; + sourceTree = ""; + }; + BCA085B422F52D8700AD4CE2 /* mobile-coding-challenge */ = { + isa = PBXGroup; + children = ( + BCA085D122F7A8EB00AD4CE2 /* Models */, + BCA085B522F52D8700AD4CE2 /* AppDelegate.swift */, + BCA085B722F52D8700AD4CE2 /* TrendingViewController.swift */, + BCA085B922F52D8700AD4CE2 /* Main.storyboard */, + BCA085BC22F52D8800AD4CE2 /* Assets.xcassets */, + BCA085BE22F52D8800AD4CE2 /* LaunchScreen.storyboard */, + BCA085C122F52D8800AD4CE2 /* Info.plist */, + ); + path = "mobile-coding-challenge"; + sourceTree = ""; + }; + BCA085CC22F7A8B200AD4CE2 /* Frameworks */ = { + isa = PBXGroup; + children = ( + BCA085E822F8760000AD4CE2 /* Reachability.framework */, + BCA085E622F7DC8F00AD4CE2 /* AlamofireImage.framework */, + BCA085CE22F7A8B200AD4CE2 /* Alamofire.framework */, + BCA085CD22F7A8B200AD4CE2 /* Freddy.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + BCA085D122F7A8EB00AD4CE2 /* Models */ = { + isa = PBXGroup; + children = ( + BCA085D422F7A95900AD4CE2 /* Entity.swift */, + BCA085D622F7ACAD00AD4CE2 /* WebService.swift */, + ); + path = Models; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + BCA085B122F52D8700AD4CE2 /* mobile-coding-challenge */ = { + isa = PBXNativeTarget; + buildConfigurationList = BCA085C422F52D8800AD4CE2 /* Build configuration list for PBXNativeTarget "mobile-coding-challenge" */; + buildPhases = ( + BCA085AE22F52D8700AD4CE2 /* Sources */, + BCA085AF22F52D8700AD4CE2 /* Frameworks */, + BCA085B022F52D8700AD4CE2 /* Resources */, + BCA085CB22F7A81400AD4CE2 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "mobile-coding-challenge"; + productName = "mobile-coding-challenge"; + productReference = BCA085B222F52D8700AD4CE2 /* mobile-coding-challenge.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BCA085AA22F52D8700AD4CE2 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1010; + LastUpgradeCheck = 1010; + ORGANIZATIONNAME = Talha; + TargetAttributes = { + BCA085B122F52D8700AD4CE2 = { + CreatedOnToolsVersion = 10.1; + }; + }; + }; + buildConfigurationList = BCA085AD22F52D8700AD4CE2 /* Build configuration list for PBXProject "mobile-coding-challenge" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = BCA085A922F52D8700AD4CE2; + productRefGroup = BCA085B322F52D8700AD4CE2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + BCA085B122F52D8700AD4CE2 /* mobile-coding-challenge */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + BCA085B022F52D8700AD4CE2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BCA085C022F52D8800AD4CE2 /* LaunchScreen.storyboard in Resources */, + BCA085BD22F52D8800AD4CE2 /* Assets.xcassets in Resources */, + BCA085BB22F52D8700AD4CE2 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + BCA085CB22F7A81400AD4CE2 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/Carthage/Build/iOS/Alamofire.framework", + "$(SRCROOT)/Carthage/Build/iOS/Freddy.framework", + "$(SRCROOT)/Carthage/Build/iOS/AlamofireImage.framework", + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/usr/local/bin/carthage copy-frameworks\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + BCA085AE22F52D8700AD4CE2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BCA085B822F52D8700AD4CE2 /* TrendingViewController.swift in Sources */, + BCA085D522F7A95900AD4CE2 /* Entity.swift in Sources */, + BCA085D722F7ACAD00AD4CE2 /* WebService.swift in Sources */, + BCA085B622F52D8700AD4CE2 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + BCA085B922F52D8700AD4CE2 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BCA085BA22F52D8700AD4CE2 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + BCA085BE22F52D8800AD4CE2 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BCA085BF22F52D8800AD4CE2 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + BCA085C222F52D8800AD4CE2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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 = 12.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + BCA085C322F52D8800AD4CE2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "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 = gnu11; + 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 = 12.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + BCA085C522F52D8800AD4CE2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5BBUDM5XDN; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); + INFOPLIST_FILE = "mobile-coding-challenge/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "talha.mobile-coding-challenge"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + BCA085C622F52D8800AD4CE2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5BBUDM5XDN; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); + INFOPLIST_FILE = "mobile-coding-challenge/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "talha.mobile-coding-challenge"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + BCA085AD22F52D8700AD4CE2 /* Build configuration list for PBXProject "mobile-coding-challenge" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BCA085C222F52D8800AD4CE2 /* Debug */, + BCA085C322F52D8800AD4CE2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BCA085C422F52D8800AD4CE2 /* Build configuration list for PBXNativeTarget "mobile-coding-challenge" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BCA085C522F52D8800AD4CE2 /* Debug */, + BCA085C622F52D8800AD4CE2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BCA085AA22F52D8700AD4CE2 /* Project object */; +} diff --git a/mobile-coding-challenge/AppDelegate.swift b/mobile-coding-challenge/AppDelegate.swift new file mode 100644 index 0000000..26bc2f5 --- /dev/null +++ b/mobile-coding-challenge/AppDelegate.swift @@ -0,0 +1,46 @@ +// +// AppDelegate.swift +// mobile-coding-challenge +// +// Created by Talha on 03/08/2019. +// Copyright © 2019 Talha. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // 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. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // 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. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // 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. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/1024.png b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..194b327 Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/120-1.png b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/120-1.png new file mode 100644 index 0000000..fc6926d Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/120-1.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/120.png b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000..fc6926d Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/180.png b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000..cc17f27 Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/40.png b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..a8a0f24 Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/58.png b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000..0d74bca Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/60.png b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000..a906233 Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/80.png b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000..fcd952d Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/87.png b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000..4721312 Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/Contents.json b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..b0a1a07 --- /dev/null +++ b/mobile-coding-challenge/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,62 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "40.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "60.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "58.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "87.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "80.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "120.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "120-1.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "180.png", + "scale" : "3x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "1024.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile-coding-challenge/Assets.xcassets/Contents.json b/mobile-coding-challenge/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/mobile-coding-challenge/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile-coding-challenge/Assets.xcassets/star.imageset/Contents.json b/mobile-coding-challenge/Assets.xcassets/star.imageset/Contents.json new file mode 100644 index 0000000..14cf9e0 --- /dev/null +++ b/mobile-coding-challenge/Assets.xcassets/star.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "star.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "star@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "star@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile-coding-challenge/Assets.xcassets/star.imageset/star.png b/mobile-coding-challenge/Assets.xcassets/star.imageset/star.png new file mode 100644 index 0000000..106ddf8 Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/star.imageset/star.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/star.imageset/star@2x.png b/mobile-coding-challenge/Assets.xcassets/star.imageset/star@2x.png new file mode 100644 index 0000000..60c5c82 Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/star.imageset/star@2x.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/star.imageset/star@3x.png b/mobile-coding-challenge/Assets.xcassets/star.imageset/star@3x.png new file mode 100644 index 0000000..70e374b Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/star.imageset/star@3x.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/uilogo.imageset/Contents.json b/mobile-coding-challenge/Assets.xcassets/uilogo.imageset/Contents.json new file mode 100644 index 0000000..64b297f --- /dev/null +++ b/mobile-coding-challenge/Assets.xcassets/uilogo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "uilogo.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "uilogo@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "uilogo@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile-coding-challenge/Assets.xcassets/uilogo.imageset/uilogo.png b/mobile-coding-challenge/Assets.xcassets/uilogo.imageset/uilogo.png new file mode 100644 index 0000000..66ae694 Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/uilogo.imageset/uilogo.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/uilogo.imageset/uilogo@2x.png b/mobile-coding-challenge/Assets.xcassets/uilogo.imageset/uilogo@2x.png new file mode 100644 index 0000000..eed3a48 Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/uilogo.imageset/uilogo@2x.png differ diff --git a/mobile-coding-challenge/Assets.xcassets/uilogo.imageset/uilogo@3x.png b/mobile-coding-challenge/Assets.xcassets/uilogo.imageset/uilogo@3x.png new file mode 100644 index 0000000..0f5b6fe Binary files /dev/null and b/mobile-coding-challenge/Assets.xcassets/uilogo.imageset/uilogo@3x.png differ diff --git a/mobile-coding-challenge/Base.lproj/LaunchScreen.storyboard b/mobile-coding-challenge/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..4c86382 --- /dev/null +++ b/mobile-coding-challenge/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-coding-challenge/Base.lproj/Main.storyboard b/mobile-coding-challenge/Base.lproj/Main.storyboard new file mode 100644 index 0000000..83f621f --- /dev/null +++ b/mobile-coding-challenge/Base.lproj/Main.storyboard @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-coding-challenge/Info.plist b/mobile-coding-challenge/Info.plist new file mode 100644 index 0000000..16be3b6 --- /dev/null +++ b/mobile-coding-challenge/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/mobile-coding-challenge/Models/Entity.swift b/mobile-coding-challenge/Models/Entity.swift new file mode 100644 index 0000000..6a35d1f --- /dev/null +++ b/mobile-coding-challenge/Models/Entity.swift @@ -0,0 +1,38 @@ +// +// Entity.swift +// mobile-coding-challenge +// +// Created by Talha on 05/08/2019. +// Copyright © 2019 Talha. All rights reserved. +// + +import Foundation +import Freddy + +struct GitHubRepo { + + var repoId: Int + var repoName: String + var repoDescription: String + var repoOwner: String + var repoAvatar: String + var repoStars: Int + +} + +extension GitHubRepo: JSONDecodable { + + init(json: JSON) throws { + repoId = try json.getInt(at: "id") + repoName = try json.getString(at: "name") + if json["description"] != nil && json["description"] != JSON.null { + repoDescription = try json.getString(at: "description") + } else { + repoDescription = "" + } + repoOwner = try json.getString(at: "owner", "login") + repoAvatar = try json.getString(at: "owner", "avatar_url") + repoStars = try json.getInt(at: "stargazers_count") + } + +} diff --git a/mobile-coding-challenge/Models/WebService.swift b/mobile-coding-challenge/Models/WebService.swift new file mode 100644 index 0000000..69d1ad6 --- /dev/null +++ b/mobile-coding-challenge/Models/WebService.swift @@ -0,0 +1,48 @@ +// +// WebService.swift +// mobile-coding-challenge +// +// Created by Talha on 05/08/2019. +// Copyright © 2019 Talha. All rights reserved. +// + +import Foundation +import Alamofire +import Freddy + +class WebService { + + //MARK: - callService + class func callService(queryString: String, success: @escaping ([GitHubRepo]) -> Void, failure: @escaping (String) -> Void) { + + let gitHubURL = "https://api.github.com/search/repositories?" + queryString + + Alamofire.request(gitHubURL).responseJSON { (response) in + if response.result.isSuccess { + do { + let trueJson = try JSON(data: response.data!) + do { + let totalCount = try trueJson.getInt(at: "total_count") + if totalCount == 0 { + failure("No Records Available") + return + } + } catch { + failure("No Records Available") + return + } + let responseArray = try trueJson.getArray(at: "items") + let gitRepoResponseArray = try responseArray.map { (json) in + return try GitHubRepo(json: json) + } + success(gitRepoResponseArray) + } catch let error { + failure(error.localizedDescription) + } + } else { + failure(response.result.error!.localizedDescription) + } + } + } + +} diff --git a/mobile-coding-challenge/TrendingViewController.swift b/mobile-coding-challenge/TrendingViewController.swift new file mode 100644 index 0000000..a40f73a --- /dev/null +++ b/mobile-coding-challenge/TrendingViewController.swift @@ -0,0 +1,194 @@ +// +// TrendingViewController.swift +// mobile-coding-challenge +// +// Created by Talha on 03/08/2019. +// Copyright © 2019 Talha. All rights reserved. +// + +import UIKit +import Alamofire +import AlamofireImage + +class TrendingViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate { + + @IBOutlet weak var activityIndicator: UIActivityIndicatorView! + @IBOutlet weak var trendingTableView: UITableView! + + var trendingData: [GitHubRepo]? + var isScrollViewBegin = true + var isServiceRunning = false + var page = 1 + var noRecords = false + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.noRecords = false + + self.trendingTableView.estimatedRowHeight = 150 + self.trendingTableView.rowHeight = 150 + + self.loadData(queryString: self.getQueryString()) + } + + + // MARK : UITableView Delegate Methods + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let cell = tableView.dequeueReusableCell(withIdentifier: "trandingCell", for: indexPath) + guard let trending = self.trendingData else { return cell } + + let repository = trending[indexPath.row] + + if let repoName:UILabel = cell.viewWithTag(1) as? UILabel { + repoName.text = repository.repoName + } + if let repoDescription:UILabel = cell.viewWithTag(2) as? UILabel { + repoDescription.text = repository.repoDescription + } + if let repoAvatar:UIImageView = cell.viewWithTag(3) as? UIImageView { + let imageUrl:URL = URL(string: repository.repoAvatar)! + repoAvatar.af_setImage(withURL: imageUrl, placeholderImage: nil, imageTransition: .crossDissolve(0.5), runImageTransitionIfCached: true, completion: nil) + } + if let repoOwner:UILabel = cell.viewWithTag(4) as? UILabel { + repoOwner.text = repository.repoOwner + } + if let repoStars:UILabel = cell.viewWithTag(5) as? UILabel { + repoStars.text = formatPoints(num: Double(repository.repoStars)) + } + return cell + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + guard let tredingDataCount = self.trendingData?.count else { return 0 } + return tredingDataCount + } + + //MARK : Class Methods + + @objc func getQueryString(page: Int = 1) -> String { + let today = Date() + let thirtyDaysBeforeToday = Calendar.current.date(byAdding: .day, value: -30, to: today)! + + let dateFormatterGet = DateFormatter() + dateFormatterGet.dateFormat = "yyyy-MM-dd" + + let thirtyDaysBeforeTodayInString = dateFormatterGet.string(from: thirtyDaysBeforeToday) + + var queryString = "q=created:%3E" + thirtyDaysBeforeTodayInString + "&sort=stars&order=desc" + if page > 1 { + queryString = queryString + "&page=" + String(page) + } + return queryString + } + + @objc func loadData(queryString: String) { + if self.noRecords == false { + activityIndicator.startAnimating() + isServiceRunning = true + self.trendingTableView.isUserInteractionEnabled = false + WebService.callService(queryString: queryString, success: {(result) in + self.isServiceRunning = false + if self.trendingData == nil { + self.trendingData = result + } else { + self.trendingData?.append(contentsOf: result) + } + self.trendingTableView.reloadData() + self.activityIndicator.stopAnimating() + self.isScrollViewBegin = false + self.trendingTableView.isUserInteractionEnabled = true + }, failure: { (error) in + if error == "No Records Available" { + self.noRecords = true + } else { + self.showErrorAlert(message: error) + } + self.activityIndicator.stopAnimating() + self.isScrollViewBegin = false + self.trendingTableView.isUserInteractionEnabled = true + }) + } + } + + //MARK : Helper Method + + func formatPoints(num: Double) ->String{ + var thousandNum = num/1000 + var millionNum = num/1000000 + if num >= 1000 && num < 1000000{ + if(floor(thousandNum) == thousandNum){ + return("\(Int(thousandNum))k") + } + return("\(thousandNum.roundToPlaces(places: 1))k") + } + if num > 1000000{ + if(floor(millionNum) == millionNum){ + return("\(Int(thousandNum))k") + } + return ("\(millionNum.roundToPlaces(places: 1))M") + } + else{ + if(floor(num) == num){ + return ("\(Int(num))") + } + return ("\(num)") + } + } + + func loadMore(scrollView: UIScrollView, isServiceRunning: Bool, isScrollViewBegin: Bool, completion: @escaping () -> Void) { + + let offset_: CGPoint = scrollView.contentOffset + let bounds: CGRect = scrollView.bounds + let size: CGSize = scrollView.contentSize + let inset: UIEdgeInsets = scrollView.contentInset + let y: CGFloat = offset_.y + bounds.size.height - inset.bottom + let h: CGFloat = size.height + let reload_distance: CGFloat = 12 + + if y > h + reload_distance && !isServiceRunning && isScrollViewBegin { + completion() + } + } + + func showErrorAlert(message: String) { + let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) + + let okAction = UIAlertAction(title: "OK", style: .default) { (_: UIAlertAction) -> Void in + print("OK pressed on error alert") + } + alertController.addAction(okAction) + self.present(alertController, animated: true, completion: nil) + } + + // MARK : Scroll View Delegate Methods + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.loadMore(scrollView: scrollView, isServiceRunning: self.isServiceRunning, isScrollViewBegin: self.isScrollViewBegin, completion: { + self.page = self.page + 1 + self.loadData(queryString: self.getQueryString(page: self.page)) + }) + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + isScrollViewBegin = true + } + +} + +// MARK : Double Extension + +extension Double { + /// Rounds the double to decimal places value + mutating func roundToPlaces(places:Int) -> Double { + let divisor = pow(10.0, Double(places)) + return Darwin.round(self * divisor) / divisor + } +} +