├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .travis.yml ├── Example ├── .swift-version ├── Podfile ├── StravaSwift.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── StravaSwift-Example.xcscheme ├── StravaSwift.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── StravaSwift │ ├── ActivitiesViewController.swift │ ├── ActivityDetailVC.swift │ ├── AppDelegate.swift │ ├── AthleteViewController.swift │ ├── ConnectViewController.swift │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── Strava.imageset │ │ │ ├── ConnectWithStrava.png │ │ │ ├── ConnectWithStrava@2x.png │ │ │ └── Contents.json │ ├── Info.plist │ ├── LaunchScreen.storyboard │ ├── Main.storyboard │ └── test2.gpx └── Tests │ ├── Info.plist │ └── Tests.swift ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── StravaSwift │ ├── Achievement.swift │ ├── Activity.swift │ ├── AlamofireRequest.swift │ ├── Athlete.swift │ ├── AthleteStats.swift │ ├── Club.swift │ ├── Comment.swift │ ├── DefaultTokenDelegate.swift │ ├── Effort.swift │ ├── Event.swift │ ├── GearBikeShoe.swift │ ├── GeneralExtensions.swift │ ├── Location.swift │ ├── Map.swift │ ├── ModelEnums.swift │ ├── NSURLExtensions.swift │ ├── OAuthToken.swift │ ├── Photo.swift │ ├── Route.swift │ ├── Router.swift │ ├── Segment.swift │ ├── Split.swift │ ├── StravaClient.swift │ ├── StravaClientError.swift │ ├── StravaConfig.swift │ ├── StravaProtocols.swift │ ├── Stream.swift │ ├── StringExtensions.swift │ ├── SwiftyJSON.swift │ ├── TokenDelegate.swift │ └── Upload.swift ├── StravaSwift.podspec └── Tests ├── LinuxMain.swift └── StravaSwiftTests ├── RouterTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | docs 2 | 3 | # OS X 4 | .DS_Store 5 | 6 | # Xcode 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata/ 17 | *.xccheckout 18 | profile 19 | *.moved-aside 20 | DerivedData 21 | *.hmap 22 | *.ipa 23 | 24 | # Bundler 25 | .bundle 26 | 27 | Carthage 28 | # We recommend against adding the Pods directory to your .gitignore. However 29 | # you should judge for yourself, the pros and cons are mentioned at: 30 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 31 | # 32 | # Note: if you ignore the Pods directory, make sure to uncomment 33 | # `pod install` in .travis.yml 34 | # 35 | Pods/ 36 | Podfile.lock 37 | 38 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * http://www.objc.io/issue-6/travis-ci.html 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | language: swift 6 | podfile: Example/Podfile 7 | osx_image: xcode13.1 8 | 9 | before_install: 10 | # - gem install xcpretty cocoapods --pre # Since Travis is not always on latest version 11 | - pod repo update 12 | - pod install --project-directory=Example 13 | script: 14 | - set -o pipefail && xcodebuild -workspace Example/StravaSwift.xcworkspace -scheme StravaSwift-Example -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO | xcpretty 15 | - pod lib lint --allow-warnings 16 | -------------------------------------------------------------------------------- /Example/.swift-version: -------------------------------------------------------------------------------- 1 | 4.0 2 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | platform :ios, '13.0' 3 | 4 | target 'StravaSwift_Example' do 5 | pod 'StravaSwift', :path => '../' 6 | 7 | target 'StravaSwift_Tests' do 8 | inherit! :search_paths 9 | end 10 | end 11 | 12 | post_install do |installer| 13 | installer.pods_project.build_configurations.each do |config| 14 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' 15 | end 16 | installer.pods_project.targets.each do |target| 17 | target.build_configurations.each do |config| 18 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' 19 | end 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /Example/StravaSwift.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 042240876C1C98B43960AA19 /* Pods_StravaSwift_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ACA61AA5A9D07B6AA6D80544 /* Pods_StravaSwift_Tests.framework */; }; 11 | 5A2F0F7B275E9545006CBD76 /* ActivityDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2F0F7A275E9545006CBD76 /* ActivityDetailVC.swift */; }; 12 | 5A2F0F7D275EBEFD006CBD76 /* test2.gpx in Resources */ = {isa = PBXBuildFile; fileRef = 5A2F0F7C275EBEFD006CBD76 /* test2.gpx */; }; 13 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 14 | 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* Tests.swift */; }; 15 | A70657D966A103F32858A0B8 /* Pods_StravaSwift_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 31A74C66DF27481B43059A3C /* Pods_StravaSwift_Example.framework */; }; 16 | C9E97C411CF3D522001C8BDD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C9E97C3D1CF3D522001C8BDD /* LaunchScreen.storyboard */; }; 17 | C9F475431CF3E19400B730EF /* ActivitiesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F4753F1CF3E19400B730EF /* ActivitiesViewController.swift */; }; 18 | C9F475441CF3E19400B730EF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F475401CF3E19400B730EF /* AppDelegate.swift */; }; 19 | C9F475451CF3E19400B730EF /* AthleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F475411CF3E19400B730EF /* AthleteViewController.swift */; }; 20 | C9F475461CF3E19400B730EF /* ConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F475421CF3E19400B730EF /* ConnectViewController.swift */; }; 21 | C9F475481CF3E1B800B730EF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C9F475471CF3E1B800B730EF /* Main.storyboard */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXContainerItemProxy section */ 25 | 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = { 26 | isa = PBXContainerItemProxy; 27 | containerPortal = 607FACC81AFB9204008FA782 /* Project object */; 28 | proxyType = 1; 29 | remoteGlobalIDString = 607FACCF1AFB9204008FA782; 30 | remoteInfo = StravaSwift; 31 | }; 32 | /* End PBXContainerItemProxy section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | 22D8DE0EB6934B7E51CB342F /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 36 | 28B11C57DFEF6CF47F7E1A14 /* Pods-StravaSwift_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StravaSwift_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-StravaSwift_Tests/Pods-StravaSwift_Tests.release.xcconfig"; sourceTree = ""; }; 37 | 2E43E18C32F4437137707402 /* Pods-StravaSwift_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StravaSwift_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-StravaSwift_Tests/Pods-StravaSwift_Tests.debug.xcconfig"; sourceTree = ""; }; 38 | 31A74C66DF27481B43059A3C /* Pods_StravaSwift_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_StravaSwift_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | 357100604A445BFCF095B848 /* StravaSwift.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = StravaSwift.podspec; path = ../StravaSwift.podspec; sourceTree = ""; }; 40 | 50961F78A3E9092861111DD5 /* Pods-StravaSwift_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StravaSwift_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-StravaSwift_Example/Pods-StravaSwift_Example.release.xcconfig"; sourceTree = ""; }; 41 | 54225B8273D655E5B950E17C /* Pods-StravaSwift_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StravaSwift_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-StravaSwift_Example/Pods-StravaSwift_Example.debug.xcconfig"; sourceTree = ""; }; 42 | 5A2F0F7A275E9545006CBD76 /* ActivityDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityDetailVC.swift; sourceTree = ""; }; 43 | 5A2F0F7C275EBEFD006CBD76 /* test2.gpx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = test2.gpx; sourceTree = ""; }; 44 | 607FACD01AFB9204008FA782 /* StravaSwift_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StravaSwift_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 46 | 607FACE51AFB9204008FA782 /* StravaSwift_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StravaSwift_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 607FACEB1AFB9204008FA782 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; 49 | ACA61AA5A9D07B6AA6D80544 /* Pods_StravaSwift_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_StravaSwift_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | C9E97C3A1CF3D505001C8BDD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | C9E97C3D1CF3D522001C8BDD /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 52 | C9F4753F1CF3E19400B730EF /* ActivitiesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivitiesViewController.swift; sourceTree = ""; }; 53 | C9F475401CF3E19400B730EF /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 54 | C9F475411CF3E19400B730EF /* AthleteViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AthleteViewController.swift; sourceTree = ""; }; 55 | C9F475421CF3E19400B730EF /* ConnectViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectViewController.swift; sourceTree = ""; }; 56 | C9F475471CF3E1B800B730EF /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 57 | E205A84E7E90DD2DF36F8489 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | A70657D966A103F32858A0B8 /* Pods_StravaSwift_Example.framework in Frameworks */, 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | 607FACE21AFB9204008FA782 /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | 042240876C1C98B43960AA19 /* Pods_StravaSwift_Tests.framework in Frameworks */, 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXFrameworksBuildPhase section */ 78 | 79 | /* Begin PBXGroup section */ 80 | 607FACC71AFB9204008FA782 = { 81 | isa = PBXGroup; 82 | children = ( 83 | 607FACF51AFB993E008FA782 /* Podspec Metadata */, 84 | 607FACD21AFB9204008FA782 /* Example for StravaSwift */, 85 | 607FACE81AFB9204008FA782 /* Tests */, 86 | 607FACD11AFB9204008FA782 /* Products */, 87 | FCFF2F7A18414445CF6AC169 /* Pods */, 88 | FBD232F6176D5FB32046FF1C /* Frameworks */, 89 | ); 90 | sourceTree = ""; 91 | }; 92 | 607FACD11AFB9204008FA782 /* Products */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 607FACD01AFB9204008FA782 /* StravaSwift_Example.app */, 96 | 607FACE51AFB9204008FA782 /* StravaSwift_Tests.xctest */, 97 | ); 98 | name = Products; 99 | sourceTree = ""; 100 | }; 101 | 607FACD21AFB9204008FA782 /* Example for StravaSwift */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | C9F475401CF3E19400B730EF /* AppDelegate.swift */, 105 | C9F4753F1CF3E19400B730EF /* ActivitiesViewController.swift */, 106 | 5A2F0F7A275E9545006CBD76 /* ActivityDetailVC.swift */, 107 | C9F475411CF3E19400B730EF /* AthleteViewController.swift */, 108 | C9F475421CF3E19400B730EF /* ConnectViewController.swift */, 109 | 607FACD31AFB9204008FA782 /* Supporting Files */, 110 | ); 111 | name = "Example for StravaSwift"; 112 | path = StravaSwift; 113 | sourceTree = ""; 114 | }; 115 | 607FACD31AFB9204008FA782 /* Supporting Files */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 5A2F0F7C275EBEFD006CBD76 /* test2.gpx */, 119 | C9F475471CF3E1B800B730EF /* Main.storyboard */, 120 | C9E97C3D1CF3D522001C8BDD /* LaunchScreen.storyboard */, 121 | 607FACDC1AFB9204008FA782 /* Images.xcassets */, 122 | C9E97C3A1CF3D505001C8BDD /* Info.plist */, 123 | ); 124 | name = "Supporting Files"; 125 | sourceTree = ""; 126 | }; 127 | 607FACE81AFB9204008FA782 /* Tests */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 607FACEB1AFB9204008FA782 /* Tests.swift */, 131 | 607FACE91AFB9204008FA782 /* Supporting Files */, 132 | ); 133 | path = Tests; 134 | sourceTree = ""; 135 | }; 136 | 607FACE91AFB9204008FA782 /* Supporting Files */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 607FACEA1AFB9204008FA782 /* Info.plist */, 140 | ); 141 | name = "Supporting Files"; 142 | sourceTree = ""; 143 | }; 144 | 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 357100604A445BFCF095B848 /* StravaSwift.podspec */, 148 | E205A84E7E90DD2DF36F8489 /* README.md */, 149 | 22D8DE0EB6934B7E51CB342F /* LICENSE */, 150 | ); 151 | name = "Podspec Metadata"; 152 | sourceTree = ""; 153 | }; 154 | FBD232F6176D5FB32046FF1C /* Frameworks */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 31A74C66DF27481B43059A3C /* Pods_StravaSwift_Example.framework */, 158 | ACA61AA5A9D07B6AA6D80544 /* Pods_StravaSwift_Tests.framework */, 159 | ); 160 | name = Frameworks; 161 | sourceTree = ""; 162 | }; 163 | FCFF2F7A18414445CF6AC169 /* Pods */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | 54225B8273D655E5B950E17C /* Pods-StravaSwift_Example.debug.xcconfig */, 167 | 50961F78A3E9092861111DD5 /* Pods-StravaSwift_Example.release.xcconfig */, 168 | 2E43E18C32F4437137707402 /* Pods-StravaSwift_Tests.debug.xcconfig */, 169 | 28B11C57DFEF6CF47F7E1A14 /* Pods-StravaSwift_Tests.release.xcconfig */, 170 | ); 171 | name = Pods; 172 | sourceTree = ""; 173 | }; 174 | /* End PBXGroup section */ 175 | 176 | /* Begin PBXNativeTarget section */ 177 | 607FACCF1AFB9204008FA782 /* StravaSwift_Example */ = { 178 | isa = PBXNativeTarget; 179 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "StravaSwift_Example" */; 180 | buildPhases = ( 181 | 806B908665F416C34816EADA /* [CP] Check Pods Manifest.lock */, 182 | 607FACCC1AFB9204008FA782 /* Sources */, 183 | 607FACCD1AFB9204008FA782 /* Frameworks */, 184 | 607FACCE1AFB9204008FA782 /* Resources */, 185 | 469703632F4658D264C59B94 /* [CP] Embed Pods Frameworks */, 186 | ); 187 | buildRules = ( 188 | ); 189 | dependencies = ( 190 | ); 191 | name = StravaSwift_Example; 192 | productName = StravaSwift; 193 | productReference = 607FACD01AFB9204008FA782 /* StravaSwift_Example.app */; 194 | productType = "com.apple.product-type.application"; 195 | }; 196 | 607FACE41AFB9204008FA782 /* StravaSwift_Tests */ = { 197 | isa = PBXNativeTarget; 198 | buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "StravaSwift_Tests" */; 199 | buildPhases = ( 200 | 92EDE072465CFE848C31F400 /* [CP] Check Pods Manifest.lock */, 201 | 607FACE11AFB9204008FA782 /* Sources */, 202 | 607FACE21AFB9204008FA782 /* Frameworks */, 203 | 607FACE31AFB9204008FA782 /* Resources */, 204 | ); 205 | buildRules = ( 206 | ); 207 | dependencies = ( 208 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */, 209 | ); 210 | name = StravaSwift_Tests; 211 | productName = Tests; 212 | productReference = 607FACE51AFB9204008FA782 /* StravaSwift_Tests.xctest */; 213 | productType = "com.apple.product-type.bundle.unit-test"; 214 | }; 215 | /* End PBXNativeTarget section */ 216 | 217 | /* Begin PBXProject section */ 218 | 607FACC81AFB9204008FA782 /* Project object */ = { 219 | isa = PBXProject; 220 | attributes = { 221 | LastSwiftUpdateCheck = 0730; 222 | LastUpgradeCheck = 1310; 223 | ORGANIZATIONNAME = CocoaPods; 224 | TargetAttributes = { 225 | 607FACCF1AFB9204008FA782 = { 226 | CreatedOnToolsVersion = 6.3.1; 227 | LastSwiftMigration = 1110; 228 | }; 229 | 607FACE41AFB9204008FA782 = { 230 | CreatedOnToolsVersion = 6.3.1; 231 | LastSwiftMigration = 1110; 232 | TestTargetID = 607FACCF1AFB9204008FA782; 233 | }; 234 | }; 235 | }; 236 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "StravaSwift" */; 237 | compatibilityVersion = "Xcode 3.2"; 238 | developmentRegion = en; 239 | hasScannedForEncodings = 0; 240 | knownRegions = ( 241 | en, 242 | Base, 243 | ); 244 | mainGroup = 607FACC71AFB9204008FA782; 245 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 246 | projectDirPath = ""; 247 | projectRoot = ""; 248 | targets = ( 249 | 607FACCF1AFB9204008FA782 /* StravaSwift_Example */, 250 | 607FACE41AFB9204008FA782 /* StravaSwift_Tests */, 251 | ); 252 | }; 253 | /* End PBXProject section */ 254 | 255 | /* Begin PBXResourcesBuildPhase section */ 256 | 607FACCE1AFB9204008FA782 /* Resources */ = { 257 | isa = PBXResourcesBuildPhase; 258 | buildActionMask = 2147483647; 259 | files = ( 260 | C9F475481CF3E1B800B730EF /* Main.storyboard in Resources */, 261 | 5A2F0F7D275EBEFD006CBD76 /* test2.gpx in Resources */, 262 | C9E97C411CF3D522001C8BDD /* LaunchScreen.storyboard in Resources */, 263 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | }; 267 | 607FACE31AFB9204008FA782 /* Resources */ = { 268 | isa = PBXResourcesBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | ); 272 | runOnlyForDeploymentPostprocessing = 0; 273 | }; 274 | /* End PBXResourcesBuildPhase section */ 275 | 276 | /* Begin PBXShellScriptBuildPhase section */ 277 | 469703632F4658D264C59B94 /* [CP] Embed Pods Frameworks */ = { 278 | isa = PBXShellScriptBuildPhase; 279 | buildActionMask = 2147483647; 280 | files = ( 281 | ); 282 | inputPaths = ( 283 | "${PODS_ROOT}/Target Support Files/Pods-StravaSwift_Example/Pods-StravaSwift_Example-frameworks.sh", 284 | "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", 285 | "${BUILT_PRODUCTS_DIR}/StravaSwift/StravaSwift.framework", 286 | "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", 287 | ); 288 | name = "[CP] Embed Pods Frameworks"; 289 | outputPaths = ( 290 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", 291 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/StravaSwift.framework", 292 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", 293 | ); 294 | runOnlyForDeploymentPostprocessing = 0; 295 | shellPath = /bin/sh; 296 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-StravaSwift_Example/Pods-StravaSwift_Example-frameworks.sh\"\n"; 297 | showEnvVarsInLog = 0; 298 | }; 299 | 806B908665F416C34816EADA /* [CP] Check Pods Manifest.lock */ = { 300 | isa = PBXShellScriptBuildPhase; 301 | buildActionMask = 2147483647; 302 | files = ( 303 | ); 304 | inputPaths = ( 305 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 306 | "${PODS_ROOT}/Manifest.lock", 307 | ); 308 | name = "[CP] Check Pods Manifest.lock"; 309 | outputPaths = ( 310 | "$(DERIVED_FILE_DIR)/Pods-StravaSwift_Example-checkManifestLockResult.txt", 311 | ); 312 | runOnlyForDeploymentPostprocessing = 0; 313 | shellPath = /bin/sh; 314 | 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"; 315 | showEnvVarsInLog = 0; 316 | }; 317 | 92EDE072465CFE848C31F400 /* [CP] Check Pods Manifest.lock */ = { 318 | isa = PBXShellScriptBuildPhase; 319 | buildActionMask = 2147483647; 320 | files = ( 321 | ); 322 | inputPaths = ( 323 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 324 | "${PODS_ROOT}/Manifest.lock", 325 | ); 326 | name = "[CP] Check Pods Manifest.lock"; 327 | outputPaths = ( 328 | "$(DERIVED_FILE_DIR)/Pods-StravaSwift_Tests-checkManifestLockResult.txt", 329 | ); 330 | runOnlyForDeploymentPostprocessing = 0; 331 | shellPath = /bin/sh; 332 | 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"; 333 | showEnvVarsInLog = 0; 334 | }; 335 | /* End PBXShellScriptBuildPhase section */ 336 | 337 | /* Begin PBXSourcesBuildPhase section */ 338 | 607FACCC1AFB9204008FA782 /* Sources */ = { 339 | isa = PBXSourcesBuildPhase; 340 | buildActionMask = 2147483647; 341 | files = ( 342 | C9F475451CF3E19400B730EF /* AthleteViewController.swift in Sources */, 343 | 5A2F0F7B275E9545006CBD76 /* ActivityDetailVC.swift in Sources */, 344 | C9F475441CF3E19400B730EF /* AppDelegate.swift in Sources */, 345 | C9F475431CF3E19400B730EF /* ActivitiesViewController.swift in Sources */, 346 | C9F475461CF3E19400B730EF /* ConnectViewController.swift in Sources */, 347 | ); 348 | runOnlyForDeploymentPostprocessing = 0; 349 | }; 350 | 607FACE11AFB9204008FA782 /* Sources */ = { 351 | isa = PBXSourcesBuildPhase; 352 | buildActionMask = 2147483647; 353 | files = ( 354 | 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */, 355 | ); 356 | runOnlyForDeploymentPostprocessing = 0; 357 | }; 358 | /* End PBXSourcesBuildPhase section */ 359 | 360 | /* Begin PBXTargetDependency section */ 361 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */ = { 362 | isa = PBXTargetDependency; 363 | target = 607FACCF1AFB9204008FA782 /* StravaSwift_Example */; 364 | targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */; 365 | }; 366 | /* End PBXTargetDependency section */ 367 | 368 | /* Begin XCBuildConfiguration section */ 369 | 607FACED1AFB9204008FA782 /* Debug */ = { 370 | isa = XCBuildConfiguration; 371 | buildSettings = { 372 | ALWAYS_SEARCH_USER_PATHS = NO; 373 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 374 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 375 | CLANG_CXX_LIBRARY = "libc++"; 376 | CLANG_ENABLE_MODULES = YES; 377 | CLANG_ENABLE_OBJC_ARC = YES; 378 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 379 | CLANG_WARN_BOOL_CONVERSION = YES; 380 | CLANG_WARN_COMMA = YES; 381 | CLANG_WARN_CONSTANT_CONVERSION = YES; 382 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 383 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 384 | CLANG_WARN_EMPTY_BODY = YES; 385 | CLANG_WARN_ENUM_CONVERSION = YES; 386 | CLANG_WARN_INFINITE_RECURSION = YES; 387 | CLANG_WARN_INT_CONVERSION = YES; 388 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 389 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 390 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 391 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 392 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 393 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 394 | CLANG_WARN_STRICT_PROTOTYPES = YES; 395 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 396 | CLANG_WARN_UNREACHABLE_CODE = YES; 397 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 398 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 399 | COPY_PHASE_STRIP = NO; 400 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 401 | ENABLE_STRICT_OBJC_MSGSEND = YES; 402 | ENABLE_TESTABILITY = YES; 403 | GCC_C_LANGUAGE_STANDARD = gnu99; 404 | GCC_DYNAMIC_NO_PIC = NO; 405 | GCC_NO_COMMON_BLOCKS = YES; 406 | GCC_OPTIMIZATION_LEVEL = 0; 407 | GCC_PREPROCESSOR_DEFINITIONS = ( 408 | "DEBUG=1", 409 | "$(inherited)", 410 | ); 411 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 412 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 413 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 414 | GCC_WARN_UNDECLARED_SELECTOR = YES; 415 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 416 | GCC_WARN_UNUSED_FUNCTION = YES; 417 | GCC_WARN_UNUSED_VARIABLE = YES; 418 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 419 | MTL_ENABLE_DEBUG_INFO = YES; 420 | ONLY_ACTIVE_ARCH = YES; 421 | SDKROOT = iphoneos; 422 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 423 | }; 424 | name = Debug; 425 | }; 426 | 607FACEE1AFB9204008FA782 /* Release */ = { 427 | isa = XCBuildConfiguration; 428 | buildSettings = { 429 | ALWAYS_SEARCH_USER_PATHS = NO; 430 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 431 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 432 | CLANG_CXX_LIBRARY = "libc++"; 433 | CLANG_ENABLE_MODULES = YES; 434 | CLANG_ENABLE_OBJC_ARC = YES; 435 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 436 | CLANG_WARN_BOOL_CONVERSION = YES; 437 | CLANG_WARN_COMMA = YES; 438 | CLANG_WARN_CONSTANT_CONVERSION = YES; 439 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 440 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 441 | CLANG_WARN_EMPTY_BODY = YES; 442 | CLANG_WARN_ENUM_CONVERSION = YES; 443 | CLANG_WARN_INFINITE_RECURSION = YES; 444 | CLANG_WARN_INT_CONVERSION = YES; 445 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 446 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 447 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 448 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 449 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 450 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 451 | CLANG_WARN_STRICT_PROTOTYPES = YES; 452 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 453 | CLANG_WARN_UNREACHABLE_CODE = YES; 454 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 455 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 456 | COPY_PHASE_STRIP = NO; 457 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 458 | ENABLE_NS_ASSERTIONS = NO; 459 | ENABLE_STRICT_OBJC_MSGSEND = YES; 460 | GCC_C_LANGUAGE_STANDARD = gnu99; 461 | GCC_NO_COMMON_BLOCKS = YES; 462 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 463 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 464 | GCC_WARN_UNDECLARED_SELECTOR = YES; 465 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 466 | GCC_WARN_UNUSED_FUNCTION = YES; 467 | GCC_WARN_UNUSED_VARIABLE = YES; 468 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 469 | MTL_ENABLE_DEBUG_INFO = NO; 470 | SDKROOT = iphoneos; 471 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 472 | VALIDATE_PRODUCT = YES; 473 | }; 474 | name = Release; 475 | }; 476 | 607FACF01AFB9204008FA782 /* Debug */ = { 477 | isa = XCBuildConfiguration; 478 | baseConfigurationReference = 54225B8273D655E5B950E17C /* Pods-StravaSwift_Example.debug.xcconfig */; 479 | buildSettings = { 480 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; 481 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 482 | CLANG_ENABLE_MODULES = YES; 483 | DEVELOPMENT_TEAM = ""; 484 | INFOPLIST_FILE = StravaSwift/Info.plist; 485 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 486 | MODULE_NAME = ExampleApp; 487 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 488 | PRODUCT_NAME = "$(TARGET_NAME)"; 489 | SWIFT_OBJC_BRIDGING_HEADER = ""; 490 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 491 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 492 | SWIFT_VERSION = 5.0; 493 | }; 494 | name = Debug; 495 | }; 496 | 607FACF11AFB9204008FA782 /* Release */ = { 497 | isa = XCBuildConfiguration; 498 | baseConfigurationReference = 50961F78A3E9092861111DD5 /* Pods-StravaSwift_Example.release.xcconfig */; 499 | buildSettings = { 500 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; 501 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 502 | CLANG_ENABLE_MODULES = YES; 503 | DEVELOPMENT_TEAM = ""; 504 | INFOPLIST_FILE = StravaSwift/Info.plist; 505 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 506 | MODULE_NAME = ExampleApp; 507 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 508 | PRODUCT_NAME = "$(TARGET_NAME)"; 509 | SWIFT_OBJC_BRIDGING_HEADER = ""; 510 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 511 | SWIFT_VERSION = 5.0; 512 | }; 513 | name = Release; 514 | }; 515 | 607FACF31AFB9204008FA782 /* Debug */ = { 516 | isa = XCBuildConfiguration; 517 | baseConfigurationReference = 2E43E18C32F4437137707402 /* Pods-StravaSwift_Tests.debug.xcconfig */; 518 | buildSettings = { 519 | DEVELOPMENT_TEAM = ""; 520 | FRAMEWORK_SEARCH_PATHS = "$(inherited)"; 521 | GCC_PREPROCESSOR_DEFINITIONS = ( 522 | "DEBUG=1", 523 | "$(inherited)", 524 | ); 525 | INFOPLIST_FILE = Tests/Info.plist; 526 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 527 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 528 | PRODUCT_NAME = "$(TARGET_NAME)"; 529 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 530 | SWIFT_VERSION = 5.0; 531 | }; 532 | name = Debug; 533 | }; 534 | 607FACF41AFB9204008FA782 /* Release */ = { 535 | isa = XCBuildConfiguration; 536 | baseConfigurationReference = 28B11C57DFEF6CF47F7E1A14 /* Pods-StravaSwift_Tests.release.xcconfig */; 537 | buildSettings = { 538 | DEVELOPMENT_TEAM = ""; 539 | FRAMEWORK_SEARCH_PATHS = "$(inherited)"; 540 | INFOPLIST_FILE = Tests/Info.plist; 541 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 542 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 543 | PRODUCT_NAME = "$(TARGET_NAME)"; 544 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 545 | SWIFT_VERSION = 5.0; 546 | }; 547 | name = Release; 548 | }; 549 | /* End XCBuildConfiguration section */ 550 | 551 | /* Begin XCConfigurationList section */ 552 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "StravaSwift" */ = { 553 | isa = XCConfigurationList; 554 | buildConfigurations = ( 555 | 607FACED1AFB9204008FA782 /* Debug */, 556 | 607FACEE1AFB9204008FA782 /* Release */, 557 | ); 558 | defaultConfigurationIsVisible = 0; 559 | defaultConfigurationName = Release; 560 | }; 561 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "StravaSwift_Example" */ = { 562 | isa = XCConfigurationList; 563 | buildConfigurations = ( 564 | 607FACF01AFB9204008FA782 /* Debug */, 565 | 607FACF11AFB9204008FA782 /* Release */, 566 | ); 567 | defaultConfigurationIsVisible = 0; 568 | defaultConfigurationName = Release; 569 | }; 570 | 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "StravaSwift_Tests" */ = { 571 | isa = XCConfigurationList; 572 | buildConfigurations = ( 573 | 607FACF31AFB9204008FA782 /* Debug */, 574 | 607FACF41AFB9204008FA782 /* Release */, 575 | ); 576 | defaultConfigurationIsVisible = 0; 577 | defaultConfigurationName = Release; 578 | }; 579 | /* End XCConfigurationList section */ 580 | }; 581 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 582 | } 583 | -------------------------------------------------------------------------------- /Example/StravaSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/StravaSwift.xcodeproj/xcshareddata/xcschemes/StravaSwift-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 78 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Example/StravaSwift.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/StravaSwift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/StravaSwift/ActivitiesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivitiesViewController.swift 3 | // StravaSwift 4 | // 5 | // Created by MATTHEW CLARKSON on 23/05/2016. 6 | // Copyright © 2016 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StravaSwift 11 | 12 | class ActivitiesViewController: UITableViewController { 13 | 14 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView? 15 | 16 | fileprivate var activities: [Activity] = [] 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | self.tableView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0) 21 | update() 22 | } 23 | 24 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 25 | switch segue.identifier { 26 | case "SegueActivityDetail": 27 | // User tapped on a row and wants to view the ActivityDetailVC 28 | // We must set the activityId property during the segue 29 | if let vc = segue.destination as? ActivityDetailVC?, let indexPath = tableView.indexPathForSelectedRow { 30 | vc?.activityID = activities[indexPath.row].id 31 | } 32 | default: break 33 | } 34 | } 35 | 36 | } 37 | 38 | extension ActivitiesViewController { 39 | 40 | fileprivate func update(params: Router.Params = nil) { 41 | activityIndicator?.startAnimating() 42 | StravaClient.sharedInstance.request(Router.athleteActivities(params: params), result: { [weak self] (activities: [Activity]?) in 43 | guard let self = self else { return } 44 | self.activityIndicator?.stopAnimating() 45 | 46 | guard let activities = activities else { return } 47 | self.activities = activities 48 | 49 | self.tableView?.reloadData() 50 | }, failure: { (error: NSError) in 51 | self.activityIndicator?.stopAnimating() 52 | debugPrint(error) 53 | }) 54 | } 55 | } 56 | 57 | extension ActivitiesViewController { 58 | 59 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 60 | return activities.count 61 | } 62 | 63 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 64 | 65 | let cell = tableView.dequeueReusableCell(withIdentifier: "activity", for: indexPath) 66 | let activity = activities[indexPath.row] 67 | 68 | cell.textLabel?.text = activity.name 69 | if let date = activity.startDate { 70 | cell.detailTextLabel?.text = "\(date)" 71 | } 72 | 73 | return cell 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Example/StravaSwift/ActivityDetailVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityDetailVC.swift 3 | // StravaSwift_Example 4 | // 5 | // Created by Michael Chartier on 12/6/21. 6 | // Copyright © 2021 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StravaSwift 11 | 12 | 13 | // This VC is presented by ActivitiesViewController 14 | // The purpose here is to demonstrate how to download the details for just one specific activity. 15 | // Yes the presenting VC already has those details, but for this demo we just ignore those and download again. 16 | class ActivityDetailVC: UIViewController 17 | { 18 | @IBOutlet weak var O_id: UILabel! 19 | @IBOutlet weak var O_name: UILabel! 20 | @IBOutlet weak var O_description: UILabel! 21 | 22 | 23 | 24 | var activityID: Int! // set by the presenting VC 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | 29 | // Download the details for the specific activity by ID 30 | StravaClient.sharedInstance.request(Router.activities(id: activityID, params: nil), result: { [weak self] (activity: Activity?) in 31 | guard let self = self else { return } 32 | guard let activity = activity else { return } 33 | self.updateUI(activity: activity) 34 | }, failure: { (error: NSError) in 35 | debugPrint(error) 36 | }) 37 | } 38 | 39 | private func updateUI( activity: Activity ) { 40 | if let id = activity.id { 41 | O_id.text = "ID: \(id)" 42 | } else { 43 | O_id.text = "ID: ??" 44 | } 45 | O_name.text = activity.name 46 | O_description.text = activity.description 47 | } 48 | 49 | } 50 | 51 | 52 | /* 53 | 54 | public let id: Int? 55 | public let resourceState: ResourceState? 56 | public let externalId: String? 57 | public let uploadId: Int? 58 | public let athlete: Athlete? 59 | public let name: String? 60 | public let description: String? 61 | public let distance: Double? 62 | public let movingTime: TimeInterval? 63 | public let elapsedTime: TimeInterval? 64 | public let highElevation : Double? 65 | public let lowElevation : Double? 66 | public let totalElevationGain: Double? 67 | public let type: ActivityType? 68 | public let startDate: Date? 69 | public let startDateLocal: Date? 70 | public let timeZone: String? 71 | public let startLatLng: Location? 72 | public let endLatLng: Location? 73 | public let achievementCount: Count? 74 | public let kudosCount: Count? 75 | public let commentCount: Count? 76 | public let athleteCount: Count? 77 | public let photoCount: Count? 78 | public let totalPhotoCount: Count? 79 | public let photos: [Photo]? 80 | public let map: Map? 81 | public let trainer: Bool? 82 | public let commute: Bool? 83 | public let manual: Bool? 84 | public let `private`: Bool? 85 | public let flagged: Bool? 86 | public let workoutType: WorkoutType? 87 | public let gear: Gear? 88 | public let averageSpeed: Speed? 89 | public let maxSpeed: Speed? 90 | public let calories: Double? 91 | public let hasKudoed: Bool? 92 | public let segmentEfforts: [Effort]? 93 | public let splitsMetric: [Split]? 94 | public let splitsStandard: [Split]? 95 | public let bestEfforts: [Split]? 96 | public let kiloJoules: Double? 97 | public let averagePower : Double? 98 | public let maxPower : Double? 99 | public let deviceWatts : Bool? 100 | public let hasHeartRate : Bool? 101 | public let averageHeartRate : Double? 102 | public let maxHeartRate : Double? 103 | 104 | */ 105 | -------------------------------------------------------------------------------- /Example/StravaSwift/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StravaSwift 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | let strava: StravaClient 18 | 19 | override init() { 20 | let config = StravaConfig( 21 | clientId: 8873, 22 | clientSecret: "97b97b29ede769eec2dc26c52dd281b5a1efe594", 23 | redirectUri: "stravaswift://mpclarkson.github.io", 24 | scopes: [.activityReadAll, .activityWrite] 25 | ) 26 | strava = StravaClient.sharedInstance.initWithConfig(config) 27 | 28 | super.init() 29 | } 30 | 31 | lazy var storyboard = { return UIStoryboard(name: "Main", bundle: nil) }() 32 | 33 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 34 | loadInitialViewController() 35 | return true 36 | } 37 | 38 | private func loadInitialViewController() { 39 | if let _ = StravaClient.sharedInstance.token { 40 | window?.rootViewController = storyboard.instantiateViewController(withIdentifier: "tab") as! UITabBarController 41 | self.window?.makeKeyAndVisible() 42 | } 43 | } 44 | 45 | func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { 46 | return strava.handleAuthorizationRedirect(url) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Example/StravaSwift/AthleteViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewController.swift 3 | // StravaSwift 4 | // 5 | // Created by MATTHEW CLARKSON on 20/05/2016. 6 | // Copyright © 2016 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StravaSwift 11 | 12 | extension UIImageView { 13 | func from(url: URL?) { 14 | guard let url = url else { return } 15 | do { 16 | let data = try Data(contentsOf: url) 17 | self.image = UIImage(data: data) 18 | } 19 | catch { 20 | return 21 | } 22 | } 23 | } 24 | 25 | 26 | class AthleteViewController: UIViewController { 27 | 28 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView? 29 | 30 | @IBOutlet weak var name: UILabel? 31 | @IBOutlet weak var avatar: UIImageView? 32 | @IBOutlet weak var rides: UILabel? 33 | @IBOutlet weak var runs: UILabel? 34 | @IBOutlet weak var swims: UILabel? 35 | @IBOutlet weak var O_upload: UIButton! 36 | @IBOutlet weak var O_uploadSpinner: UIActivityIndicatorView! 37 | @IBOutlet weak var O_monitor: UIButton! 38 | 39 | var athlete: Athlete? { 40 | didSet { 41 | name?.text = "\(athlete?.firstname ?? "") \(athlete?.lastname ?? "")" 42 | avatar?.from(url: athlete?.profile) 43 | } 44 | } 45 | 46 | var stats: AthleteStats? { 47 | didSet { 48 | if let ridesInt = stats?.allRideTotals?.count { 49 | rides?.text = String(ridesInt) 50 | } 51 | if let runsInt = stats?.allRunTotals?.count { 52 | runs?.text = String(runsInt) 53 | } 54 | if let swimsInt = stats?.allSwimTotals?.count { 55 | swims?.text = String(swimsInt) 56 | } 57 | } 58 | } 59 | 60 | 61 | // Couple counters used for uploading new activities 62 | var uploadID: Int? 63 | var activityID: Int? 64 | var matchCounter = 0 65 | var previousEffortCount = 0 66 | var monitorCounter = 0 67 | let maximumReties = 20 68 | 69 | override func viewDidLoad() { 70 | super.viewDidLoad() 71 | update() 72 | } 73 | 74 | @IBAction func A_upload(_ sender: Any) { 75 | O_upload.setTitle("Uploading...", for: .normal) 76 | O_upload.isEnabled = false 77 | O_monitor.isHidden = true 78 | O_uploadSpinner.startAnimating() 79 | 80 | // Grab a GPX test file from the app bundle. Load it into a Data object. 81 | guard let filePath = Bundle.main.url(forResource: "test2", withExtension: "gpx") else { return } 82 | guard let fileData = try? Data(contentsOf: filePath) else { return } 83 | 84 | let params = StravaSwift.UploadData(activityType: .ride, name: "test1", description: "upload test", private: false, trainer: nil, externalId: nil, dataType: .gpx, file: fileData) 85 | 86 | StravaClient.sharedInstance.upload(Router.uploadFile(upload: params), upload: params, result: { [weak self] (status: UploadStatus?) in 87 | guard let self = self else { return } 88 | if let status = status, let uploadID = status.id { 89 | // At this point only status.id will be valid. All other properties will be nil. 90 | self.uploadID = uploadID 91 | self.O_upload.setTitle("Processing new activity", for: .normal) 92 | self.monitorCounter = self.maximumReties 93 | self.monitorUploadProgress() 94 | }}, failure: { (error: NSError) in 95 | debugPrint(error) 96 | self.doAlert(title: "Strava Error", message: error.localizedDescription) 97 | }) 98 | } 99 | 100 | @IBAction func A_monitor(_ sender: Any) { 101 | O_monitor.setTitle("Monitoring...", for: .normal) 102 | O_monitor.isEnabled = false 103 | O_uploadSpinner.startAnimating() 104 | monitorSegments() 105 | } 106 | 107 | private func monitorUploadProgress() { 108 | // Monitor the upload record until we get a valid activityID 109 | guard let uploadID = uploadID else { return } 110 | Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in 111 | StravaClient.sharedInstance.request(Router.uploads(id: uploadID), result: { [weak self] (status: UploadStatus?) in 112 | guard let self = self else { return } 113 | guard let status = status else { return } 114 | if let error = status.error { 115 | self.doAlert(title: "Upload Failed", message: error) 116 | return 117 | } else if let activityID = status.activityId { 118 | // We have a valid activityID so the upload is considered complete. 119 | // However, note that segment processing can continue for quite a while. 120 | self.activityID = activityID 121 | self.O_upload.setTitle("Upload Complete", for: .normal) 122 | self.O_uploadSpinner.stopAnimating() 123 | self.O_monitor.setTitle("Monitor Segments", for: .normal) 124 | self.O_monitor.isHidden = false 125 | self.O_monitor.isEnabled = true 126 | } else { 127 | // Start another timer 128 | self.monitorUploadProgress() 129 | } 130 | }, failure: { (error: NSError) in 131 | debugPrint(error) 132 | }) 133 | } 134 | } 135 | 136 | private func monitorSegments() { 137 | // We want to know when the Strava server has finished processing the new upload. 138 | // Processing can take anywhere from a few seconds to several minutes depending on many factors. 139 | // When we first upload a new activity the number of segments will be zero. 140 | // This number will increase as new segments are detected by the Strava server. 141 | // This number will stay at zero if no segments are found. 142 | // For example, for a 3 hour recording with 20 segments this number will typically increment 143 | // by 1 or 2 every second until all 20 segments are found. 144 | // As far as I can tell, there is no "done" flag we can monitor. 145 | // The approach here is to check segment_count once every 2 seconds until we detect no further changes. 146 | 147 | monitorCounter = maximumReties 148 | matchCounter = 0 149 | previousEffortCount = 0 150 | 151 | getEffortCount() 152 | } 153 | 154 | private func getEffortCount() { 155 | guard let activityID = activityID else { return } 156 | Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in 157 | // Download the details for the specific activity by ID 158 | StravaClient.sharedInstance.request(Router.activities(id: activityID, params: nil), result: { [weak self] (activity: Activity?) in 159 | guard let self = self else { return } 160 | guard let activity = activity else { return } 161 | if let efforts = activity.segmentEfforts, efforts.count > 0 { 162 | if efforts.count == self.previousEffortCount { 163 | self.matchCounter += 1 164 | if (self.matchCounter >= 3) { 165 | // There is a good chance that processing has completed. 166 | self.O_monitor.setTitle("\(efforts.count) segments found", for: .normal) 167 | self.O_uploadSpinner.stopAnimating() 168 | self.O_upload.isEnabled = true 169 | return // stop monitoring 170 | } 171 | } 172 | self.previousEffortCount = efforts.count 173 | } 174 | // Schedule another timer OR abort 175 | self.monitorCounter -= 1 176 | if (self.monitorCounter > 0) { 177 | self.getEffortCount() 178 | } else { 179 | self.O_monitor.setTitle("Timeout Error", for: .normal) 180 | self.O_uploadSpinner.stopAnimating() 181 | self.O_upload.isEnabled = true 182 | } 183 | }, failure: { (error: NSError) in 184 | debugPrint(error) 185 | self.doAlert(title: "Strava Error", message: error.localizedDescription) 186 | }) 187 | } 188 | } 189 | 190 | func doAlert(title: String, message: String) 191 | { 192 | O_uploadSpinner.stopAnimating() 193 | O_upload.setTitle("Upload to Strava", for: .normal) 194 | O_upload.isEnabled = true 195 | O_monitor.isHidden = true 196 | 197 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 198 | let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) 199 | alert.addAction(okAction) 200 | self.present(alert, animated: true, completion: nil) 201 | } 202 | 203 | } 204 | 205 | extension AthleteViewController { 206 | 207 | func update() { 208 | activityIndicator?.startAnimating() 209 | StravaClient.sharedInstance.request(Router.athlete, result: { [weak self] (athlete: Athlete?) in 210 | guard let self = self else { return } 211 | self.activityIndicator?.stopAnimating() 212 | 213 | guard let athlete = athlete else { return } 214 | self.athlete = athlete 215 | 216 | StravaClient.sharedInstance.request(Router.athletesStats(id: athlete.id!, params: nil), result: { [weak self] (stats: AthleteStats?) in 217 | guard let self = self else { return } 218 | self.activityIndicator?.stopAnimating() 219 | self.stats = stats 220 | 221 | }, failure: { (error: NSError) in 222 | self.activityIndicator?.stopAnimating() 223 | debugPrint(error) 224 | }) 225 | 226 | }, failure: { (error: NSError) in 227 | self.activityIndicator?.stopAnimating() 228 | debugPrint(error) 229 | }) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /Example/StravaSwift/ConnectViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import StravaSwift 10 | import UIKit 11 | 12 | class ConnectViewController: UIViewController { 13 | 14 | @IBOutlet weak var loginButton: UIButton! 15 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView? 16 | 17 | var code: String? 18 | private var token: OAuthToken? 19 | 20 | @IBAction func login(_ sender: AnyObject) { 21 | activityIndicator?.startAnimating() 22 | loginButton.isHidden = true 23 | StravaClient.sharedInstance.authorize() { [weak self] (result: Result) in 24 | guard let self = self else { return } 25 | self.activityIndicator?.stopAnimating() 26 | self.loginButton.isHidden = false 27 | self.didAuthenticate(result: result) 28 | } 29 | } 30 | 31 | private func didAuthenticate(result: Result) { 32 | switch result { 33 | case .success(let token): 34 | self.token = token 35 | self.performSegue(withIdentifier: "navigation", sender: self) 36 | case .failure(let error): 37 | debugPrint(error) 38 | } 39 | } 40 | 41 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 42 | if segue.identifier == "navigation" { 43 | let barViewControllers = segue.destination as! UITabBarController 44 | let vc = barViewControllers.viewControllers![0] as! AthleteViewController 45 | vc.athlete = self.token?.athlete 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Example/StravaSwift/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/StravaSwift/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/StravaSwift/Images.xcassets/Strava.imageset/ConnectWithStrava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpclarkson/StravaSwift/2a6db87aa19f14dc67f97a4006f14d0c0eacc4f9/Example/StravaSwift/Images.xcassets/Strava.imageset/ConnectWithStrava.png -------------------------------------------------------------------------------- /Example/StravaSwift/Images.xcassets/Strava.imageset/ConnectWithStrava@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpclarkson/StravaSwift/2a6db87aa19f14dc67f97a4006f14d0c0eacc4f9/Example/StravaSwift/Images.xcassets/Strava.imageset/ConnectWithStrava@2x.png -------------------------------------------------------------------------------- /Example/StravaSwift/Images.xcassets/Strava.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ConnectWithStrava.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ConnectWithStrava@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Example/StravaSwift/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Editor 26 | CFBundleURLName 27 | com.stravaswift.ios 28 | CFBundleURLSchemes 29 | 30 | stravaswift 31 | 32 | 33 | 34 | CFBundleVersion 35 | 1 36 | LSApplicationQueriesSchemes 37 | 38 | strava 39 | 40 | LSRequiresIPhoneOS 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /Example/StravaSwift/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Example/StravaSwift/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 64 | 70 | 76 | 82 | 88 | 94 | 100 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 118 | 126 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 200 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 239 | 245 | 251 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | -------------------------------------------------------------------------------- /Example/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | // https://github.com/Quick/Quick 2 | 3 | //import Quick 4 | //import Nimble 5 | //import StravaSwift 6 | // 7 | //class TableOfContentsSpec: QuickSpec { 8 | // override func spec() { 9 | // describe("these will fail") { 10 | // 11 | // it("can do maths") { 12 | // expect(1) == 2 13 | // } 14 | // 15 | // it("can read") { 16 | // expect("number") == "string" 17 | // } 18 | // 19 | // it("will eventually fail") { 20 | // expect("time").toEventually( equal("done") ) 21 | // } 22 | // 23 | // context("these will pass") { 24 | // 25 | // it("can do maths") { 26 | // expect(23) == 23 27 | // } 28 | // 29 | // it("can read") { 30 | // expect("🐮") == "🐮" 31 | // } 32 | // 33 | // it("will eventually pass") { 34 | // var time = "passing" 35 | // 36 | // dispatch_async(dispatch_get_main_queue()) { 37 | // time = "done" 38 | // } 39 | // 40 | // waitUntil { done in 41 | // NSThread.sleepForTimeInterval(0.5) 42 | // expect(time) == "done" 43 | // 44 | // done() 45 | // } 46 | // } 47 | // } 48 | // } 49 | // } 50 | //} 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Matthew Clarkson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Alamofire", 6 | "repositoryURL": "https://github.com/Alamofire/Alamofire.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "ce5be6fbc6f51414c49f56fc8e2b7c99253d9f8e", 10 | "version": "4.9.0" 11 | } 12 | }, 13 | { 14 | "package": "SwiftyJSON", 15 | "repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "2b6054efa051565954e1d2b9da831680026cd768", 19 | "version": "5.0.0" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "StravaSwift", 8 | products: [ 9 | .library(name: "StravaSwift", targets: ["StravaSwift"]), 10 | ], 11 | dependencies: [ 12 | .package(url: "https://github.com/Alamofire/Alamofire.git", from: "4.9.0"), 13 | .package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"), 14 | ], 15 | targets: [ 16 | .target(name: "StravaSwift", dependencies: ["Alamofire", "SwiftyJSON"]), 17 | .testTarget(name: "StravaSwiftTests", dependencies: ["StravaSwift"]), 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StravaSwift 2 | 3 | [![CI Status](http://img.shields.io/travis/mpclarkson/StravaSwift.svg?style=flat)](https://travis-ci.org/mpclarkson/StravaSwift) 4 | [![codebeat badge](https://codebeat.co/badges/d58ef23f-b2c6-45df-83cb-35af96b6980d)](https://codebeat.co/projects/github-com-mpclarkson-stravaswift) 5 | [![Version](https://img.shields.io/cocoapods/v/StravaSwift.svg?style=flat)](http://cocoapods.org/pods/StravaSwift) 6 | [![License](https://img.shields.io/cocoapods/l/StravaSwift.svg?style=flat)](http://cocoapods.org/pods/StravaSwift) 7 | [![Platform](https://img.shields.io/cocoapods/p/StravaSwift.svg?style=flat)](http://cocoapods.org/pods/StravaSwift) 8 | 9 | This is a Swift wrapper for the [Strava v3 API](https://strava.github.io/api/). 10 | 11 | As this is a passion project, I only work on it when I have time. So, if you are interested in contributing, feel free to submit PRs. 12 | 13 | ## Example 14 | 15 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 16 | 17 | ## Installation 18 | 19 | [StravaSwift](https://github.com/mpclarkson/StravaSwift) is available through the [Swift Package Manager](https://swift.org/package-manager/) and [CocoaPods](http://cocoapods.org). 20 | 21 | ### Swift Package Manager 22 | 23 | ```swift 24 | dependencies: [ 25 | .package(url: "https://github.com/mpclarkson/StravaSwift.git", from: "1.0.1") 26 | ] 27 | ``` 28 | 29 | ### Cocoapods 30 | 31 | To install it, simply add the following line to your Podfile: 32 | 33 | ```ruby 34 | pod "StravaSwift", '~> 1.0.1' 35 | ``` 36 | 37 | ## Quick Start 38 | 39 | The full library documentation is available [here](http://cocoadocs.org/docsets/StravaSwift). 40 | 41 | * First, you must [register your app](http://labs.strava.com/developers/) with Strava and get an OAuth `client id` and `client secret`. 42 | 43 | * Initialize the Strava Client as follows, preferably in your `AppDelegate.swift` to ensure it is configured before you call it: 44 | 45 | ```swift 46 | let config = StravaConfig( 47 | clientId: YourStravaClientId, 48 | clientSecret: YourStravaClientSecret, 49 | redirectUri: YourRedirectUrl 50 | ) 51 | 52 | StravaClient.sharedInstance.initWithConfig(config) 53 | ``` 54 | 55 | * Note, by default the OAuth token is only available while the app is running, which means you need to request a new token. You can implement custom token storage and retrieval behaviour by overriding the default token `delegate` in the `StravaConfig` initializer which must implement the `TokenDelegate` protocol. 56 | 57 | * Register your redirect URL scheme in your `info.plist` file. 58 | 59 | * The authentication will use the Strava app be default if it is installed on the device. If the user does not have Strava installed, it will fallback on `SFAuthenticationSession` or `ASWebAuthenticationSession` depending on the iOS version. If your app is linked on or after iOS 9.0, you must add `strava` in you app’s `info.plist` file. It should be added as an entry fo the array under the `LSApplicationQueriesSchemes` key. Failure to do this will result in a crash when calling `canOpenUrl:`. 60 | 61 | ```xml 62 | LSApplicationQueriesSchemes 63 | 64 | strava 65 | 66 | ``` 67 | 68 | * Implement the following method in your `AppDelegate.swift` to handle the OAuth redirection from Strava: 69 | 70 | ```swift 71 | let strava = StravaClient.sharedInstance 72 | 73 | func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { 74 | return strava.handleAuthorizationRedirect(url) 75 | } 76 | ``` 77 | 78 | * After authorizing, you can start requesting resources: 79 | 80 | ```swift 81 | strava.authorize() { result in 82 | switch result { 83 | case .success(let token): 84 | //do something for success 85 | case .failure(let error): 86 | //do something for error 87 | } 88 | } 89 | ``` 90 | 91 | * Requesting resources: 92 | 93 | > The Router implementation is based on this 94 | Alamofire [example](https://github.com/Alamofire/Alamofire#api-parameter-abstraction): 95 | 96 | ```swift 97 | strava.request(Router.athletes(id: 9999999999)) { (athlete: Athlete?) in 98 | //do something with the athlete 99 | } 100 | 101 | let params = [ 102 | 'page' = 2, 103 | 'per_page' = 25 104 | ] 105 | 106 | strava.request(Router.athleteActivities(params: params) { (activities: [Activity]?) in 107 | //do something with the activities 108 | } 109 | ``` 110 | 111 | ## Todos 112 | 113 | - [ ] 100% API coverage (about 90% now) 114 | - [ ] Documentation 115 | - [ ] Tests 116 | - [ ] Better example app 117 | 118 | ## Author 119 | 120 | Matthew Clarkson, mpclarkson@gmail.com 121 | 122 | ## License 123 | 124 | StravaSwift is available under the MIT license. See the LICENSE file for more info. 125 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Achievement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Achievement.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 24/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Achievement struct - details the type of achievement and the rank 14 | **/ 15 | public struct Achievement: Strava { 16 | /** Achievement type enum **/ 17 | public let type: AchievementType? 18 | 19 | /** Rank for the achievement **/ 20 | public let rank: Int? 21 | 22 | /** 23 | Initializer 24 | 25 | - Parameter json: SwiftyJSON object 26 | - Internal 27 | **/ 28 | public init(_ json: JSON) { 29 | type = json["type"].strava(AchievementType.self) 30 | rank = json["rank"].int 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Activity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Activity.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Achievement class - a detailed representation is returned if the activity is owned by the requesting athlete, otherwise 14 | a summary representation is returned for all other requests. 15 | 16 | Activity details, including segment efforts, splits and best efforts, are only available to the owner of the activity. 17 | 18 | By default, only “important” efforts are included. “Importance” is based on a number of factors and its value may change over time. 19 | 20 | **/ 21 | public final class Activity: Strava { 22 | 23 | public typealias Speed = Double 24 | public typealias Count = Int 25 | 26 | public let id: Int? 27 | public let resourceState: ResourceState? 28 | public let externalId: String? 29 | public let uploadId: Int? 30 | public let athlete: Athlete? 31 | public let name: String? 32 | public let description: String? 33 | public let distance: Double? 34 | public let movingTime: TimeInterval? 35 | public let elapsedTime: TimeInterval? 36 | public let highElevation : Double? 37 | public let lowElevation : Double? 38 | public let totalElevationGain: Double? 39 | public let type: ActivityType? 40 | public let startDate: Date? 41 | public let startDateLocal: Date? 42 | public let timeZone: String? 43 | public let startLatLng: Location? 44 | public let endLatLng: Location? 45 | public let achievementCount: Count? 46 | public let kudosCount: Count? 47 | public let commentCount: Count? 48 | public let athleteCount: Count? 49 | public let photoCount: Count? 50 | public let totalPhotoCount: Count? 51 | public let photos: [Photo]? 52 | public let map: Map? 53 | public let trainer: Bool? 54 | public let commute: Bool? 55 | public let manual: Bool? 56 | public let `private`: Bool? 57 | public let flagged: Bool? 58 | public let workoutType: WorkoutType? 59 | public let gear: Gear? 60 | public let averageSpeed: Speed? 61 | public let maxSpeed: Speed? 62 | public let calories: Double? 63 | public let hasKudoed: Bool? 64 | public let segmentEfforts: [Effort]? 65 | public let splitsMetric: [Split]? 66 | public let splitsStandard: [Split]? 67 | public let bestEfforts: [Split]? 68 | public let kiloJoules: Double? 69 | public let averagePower : Double? 70 | public let maxPower : Double? 71 | public let deviceWatts : Bool? 72 | public let hasHeartRate : Bool? 73 | public let averageHeartRate : Double? 74 | public let maxHeartRate : Double? 75 | 76 | 77 | /** 78 | Initializer 79 | 80 | - Parameter json: SwiftyJSON object 81 | - Internal 82 | **/ 83 | required public init(_ json: JSON) { 84 | id = json["id"].int 85 | resourceState = json["resource_state"].strava(ResourceState.self) 86 | externalId = json["external_id"].string 87 | uploadId = json["upload_id"].int 88 | athlete = json["athlete"].strava(Athlete.self) 89 | name = json["name"].string 90 | description = json["description"].string 91 | distance = json["distance"].double 92 | movingTime = json["moving_time"].double 93 | elapsedTime = json["elapsed_time"].double 94 | lowElevation = json["elev_low"].double 95 | highElevation = json["elev_high"].double 96 | totalElevationGain = json["total_elevation_gain"].double 97 | type = json["type"].strava(ActivityType.self) 98 | startDate = json["start_date"].string?.toDate() 99 | startDateLocal = json["start_date_local"].string?.toDate() 100 | startLatLng = json["start_latlng"].strava(Location.self) 101 | endLatLng = json["end_latlng"].strava(Location.self) 102 | achievementCount = json["achievement_count"].int 103 | kudosCount = json["kudos_count"].int 104 | commentCount = json["comment_count"].int 105 | athleteCount = json["athlete_count"].int 106 | photoCount = json["php_count"].int 107 | totalPhotoCount = json["total_photo_count"].int 108 | photos = json["photos"].strava(Photo.self) 109 | trainer = json["trainer"].bool 110 | commute = json["commute"].bool 111 | manual = json["manual"].bool 112 | `private` = json["private"].bool 113 | flagged = json["flagged"].bool 114 | workoutType = json["workout_type"].strava(WorkoutType.self) 115 | gear = json["gear"].strava(Gear.self) 116 | averageSpeed = json["average_speed"].double 117 | maxSpeed = json["max_speed"].double 118 | calories = json["calories"].double 119 | hasKudoed = json["has_kudoed"].bool 120 | segmentEfforts = json["segment_efforts"].strava(Effort.self) 121 | splitsMetric = json["splits_metric"].strava(Split.self) 122 | splitsStandard = json["splits_standard"].strava(Split.self) 123 | bestEfforts = json["best_efforts"].strava(Split.self) 124 | map = json["map"].strava(Map.self) 125 | timeZone = json["timezone"].string 126 | kiloJoules = json["kilojoules"].double 127 | averagePower = json["average_watts"].double 128 | maxPower = json["max_watts"].double 129 | deviceWatts = json["device_watts"].bool 130 | hasHeartRate = json["has_heartrate"].bool 131 | averageHeartRate = json["average_heartrate"].double 132 | maxHeartRate = json["max_heartrate"].double 133 | } 134 | } 135 | 136 | // MetaActivity is used by Effort to hold unique Activity ID 137 | public final class MetaActivity: Strava { 138 | public let id: Int? 139 | 140 | required public init(_ json: JSON) { 141 | id = json["id"].int 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/StravaSwift/AlamofireRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyJSONRequest.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 15/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | import SwiftyJSON 12 | 13 | //MARK: - Methods 14 | 15 | extension DataRequest { 16 | 17 | @discardableResult 18 | func responseStrava(_ completionHandler: @escaping (DataResponse) -> Void) -> Self { 19 | return responseStrava(nil, keyPath: nil, completionHandler: completionHandler) 20 | } 21 | 22 | @discardableResult 23 | func responseStrava(_ keyPath: String, completionHandler: @escaping (DataResponse) -> Void) -> Self { 24 | return responseStrava(nil, keyPath: keyPath, completionHandler: completionHandler) 25 | } 26 | 27 | @discardableResult 28 | func responseStrava(_ queue: DispatchQueue?, keyPath: String?, completionHandler: @escaping (DataResponse) -> Void) -> Self { 29 | return response(queue: queue, responseSerializer: DataRequest.StravaSerializer(keyPath), completionHandler: completionHandler) 30 | } 31 | 32 | @discardableResult 33 | func responseStravaArray(_ completionHandler: @escaping (DataResponse<[T]>) -> Void) -> Self { 34 | return responseStravaArray(nil, keyPath: nil, completionHandler: completionHandler) 35 | } 36 | 37 | @discardableResult 38 | func responseStravaArray(_ keyPath: String, completionHandler: @escaping (DataResponse<[T]>) -> Void) -> Self { 39 | return responseStravaArray(nil, keyPath: keyPath, completionHandler: completionHandler) 40 | } 41 | 42 | @discardableResult 43 | func responseStravaArray(_ queue: DispatchQueue?, completionHandler: @escaping (DataResponse<[T]>) -> Void) -> Self { 44 | return responseStravaArray(queue, keyPath: nil, completionHandler: completionHandler) 45 | } 46 | 47 | @discardableResult 48 | func responseStravaArray(_ queue: DispatchQueue?, keyPath: String?, completionHandler: @escaping (DataResponse<[T]>) -> Void) -> Self { 49 | return response(queue: queue, responseSerializer: DataRequest.StravaArraySerializer(keyPath), completionHandler: completionHandler) 50 | } 51 | } 52 | 53 | //MARK: Serializers 54 | 55 | //TODO: Clean these up so there is no duplication 56 | 57 | extension DataRequest { 58 | 59 | typealias SerializeResponse = (URLRequest?, HTTPURLResponse?, Data?, Error?) 60 | 61 | fileprivate static func parseResponse(_ info: SerializeResponse) -> (Result?, Error?) { 62 | let (request, response, data, error) = info 63 | 64 | guard let _ = data else { 65 | let error = generateError(failureReason: "Data could not be serialized. Input data was nil.", response: response) 66 | return (nil, error) 67 | } 68 | 69 | let JSONResponseSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments) 70 | let result = JSONResponseSerializer.serializeResponse(request, response, data, error) 71 | 72 | return (result, nil) 73 | } 74 | 75 | fileprivate static func generateError(failureReason: String, response: HTTPURLResponse?) -> NSError { 76 | let errorDomain = "com.stravaswift.error" 77 | let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason] 78 | let code = response?.statusCode ?? 0 79 | let returnError = NSError(domain: errorDomain, code: code, userInfo: userInfo) 80 | 81 | return returnError 82 | } 83 | 84 | static func StravaSerializer(_ keyPath: String?) -> DataResponseSerializer { 85 | return DataResponseSerializer { request, response, data, error in 86 | let (result, e) = parseResponse((request, response, data, error)) 87 | 88 | if let e = e { 89 | return .failure(e) 90 | } 91 | 92 | if let json = result?.value { 93 | let object = T.init(JSON(json)) 94 | return .success(object) 95 | } 96 | 97 | return .failure(generateError(failureReason: "StravaSerializer failed to serialize response.", response: response)) 98 | } 99 | } 100 | 101 | static func StravaArraySerializer(_ keyPath: String?) -> DataResponseSerializer<[T]> { 102 | return DataResponseSerializer { request, response, data, error in 103 | 104 | let (result, e) = parseResponse((request, response, data, error)) 105 | 106 | if let e = e { 107 | return .failure(e) 108 | } 109 | 110 | if let json = result?.value { 111 | var results: [T] = [] 112 | JSON(json).array?.forEach { 113 | results.append(T.init($0)) 114 | } 115 | 116 | return .success(results) 117 | } 118 | 119 | return .failure(generateError(failureReason: "StravaSerializer failed to serialize response.", response: response)) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Athlete.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Athelete.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Athletes are Strava users, Strava users are athletes. The object is returned in detailed, summary or meta representations. 14 | **/ 15 | public final class Athlete: Strava { 16 | public let id: Int? 17 | public let resourceState: ResourceState? 18 | public let firstname: String? 19 | public let lastname: String? 20 | public let profileMedium: URL? 21 | public let profile: URL? 22 | public let city: String? 23 | public let state: String? 24 | public let country: String? 25 | public let sex: Sex? 26 | public let friend: FollowingStatus? 27 | public let follower: FollowingStatus? 28 | public let premium:Bool? 29 | public let createdAt: Date? 30 | public let updatedAt: Date? 31 | public let friendCount: Int? 32 | public let followerCount: Int? 33 | public let mutualFriendCount: Int? 34 | public let datePreference: String? 35 | public let measurementPreference: Units? 36 | public let email: String? 37 | public let FTP: Int? 38 | public let weight: Double? 39 | public let clubs: [Club]? 40 | public let bikes: [Bike]? 41 | public let shoes: [Shoe]? 42 | 43 | /** 44 | Initializer 45 | 46 | - Parameter json: SwiftyJSON object 47 | - Internal 48 | **/ 49 | required public init(_ json: JSON) { 50 | id = json["id"].int 51 | resourceState = json["resource_state"].strava(ResourceState.self) 52 | city = json["city"].string 53 | state = json["state"].string 54 | country = json["country"].string 55 | profileMedium = URL(optionalString: json["profile_medium"].string) 56 | profile = URL(optionalString: json["profile"].string) 57 | firstname = json["firstname"].string 58 | lastname = json["lastname"].string 59 | sex = json["sex"].strava(Sex.self) 60 | friend = json["friend"].strava(FollowingStatus.self) 61 | follower = json["follower"].strava(FollowingStatus.self) 62 | premium = json["premium"].bool 63 | createdAt = json["created_at"].string?.toDate() 64 | updatedAt = json["updated_at"].string?.toDate() 65 | followerCount = json["follower_count"].int 66 | friendCount = json["friend_count"].int 67 | mutualFriendCount = json["mutual_friend_count"].int 68 | datePreference = json["date_preference"].string 69 | measurementPreference = json["measurement_preference"].strava(Units.self) 70 | email = json["email"].string 71 | FTP = json["ftp"].int 72 | weight = json["weight"].double 73 | clubs = json["clubs"].strava(Club.self) 74 | bikes = json["bikes"].strava(Bike.self) 75 | shoes = json["shoes"].strava(Shoe.self) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/StravaSwift/AthleteStats.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AthleteStats.swift 3 | // StravaSwift 4 | // 5 | // Created by Andy on 01/22/2017 6 | // Copyright © 2017 Andy Gonzalez. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Stats are aggregated data for an athlete 14 | **/ 15 | public final class AthleteStats: Strava { 16 | 17 | public final class Totals { 18 | 19 | public let count: Int? 20 | public let distance: Double? 21 | public let movingTime: TimeInterval? 22 | public let elapsedTime: TimeInterval? 23 | public let elevationGain: Double? 24 | public let achievementCount: Int? 25 | 26 | required public init(_ json: JSON) { 27 | count = json["count"].int 28 | distance = json["distance"].double 29 | movingTime = json["moving_time"].double 30 | elapsedTime = json["elapsed_time"].double 31 | elevationGain = json["elevation_gain"].double 32 | achievementCount = json["achievement_count"].int 33 | } 34 | } 35 | 36 | public let biggestRideDistance: Double? 37 | public let biggestClimbElevationGain: Double? 38 | public let recentRideTotals: Totals? 39 | public let recentRunTotals: Totals? 40 | public let recentSwimTotals: Totals? 41 | public let ytdRideTotals: Totals? 42 | public let ytdRunTotals: Totals? 43 | public let ytdSwimTotals: Totals? 44 | public let allRideTotals: Totals? 45 | public let allRunTotals: Totals? 46 | public let allSwimTotals: Totals? 47 | 48 | /** 49 | Initializer 50 | 51 | - Parameter json: SwiftyJSON object 52 | - Internal 53 | **/ 54 | required public init(_ json: JSON) { 55 | 56 | biggestRideDistance = json["biggest_ride_distance"].double 57 | biggestClimbElevationGain = json["biggest_climb_elevation_gain"].double 58 | recentRideTotals = Totals(json["recent_ride_totals"]) 59 | recentRunTotals = Totals(json["recent_run_totals"]) 60 | recentSwimTotals = Totals(json["recent_swim_totals"]) 61 | ytdRideTotals = Totals(json["ytd_ride_totals"]) 62 | ytdRunTotals = Totals(json["ytd_run_totals"]) 63 | ytdSwimTotals = Totals(json["ytd_swim_totals"]) 64 | allRideTotals = Totals(json["all_ride_totals"]) 65 | allRunTotals = Totals(json["all_run_totals"]) 66 | allSwimTotals = Totals(json["all_swim_totals"]) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Club.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Club.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Clubs represent groups of athletes on Strava. They can be public or private. The object is returned in summary or detailed representations. 14 | **/ 15 | public final class Club: Strava { 16 | public let id: Int? 17 | public let profileMedium: URL? 18 | public let profile: URL? 19 | public let name: String? 20 | public let description: String? 21 | public let city: String? 22 | public let state: String? 23 | public let country: String? 24 | public let clubType: ClubType? 25 | public let sportType: SportType? 26 | public let isPrivate: Bool? 27 | public let memberCount: Int? 28 | public let resourceState: ResourceState? 29 | 30 | /** 31 | Initializer 32 | 33 | - Parameter json: SwiftyJSON object 34 | - Internal 35 | **/ 36 | required public init(_ json: JSON) { 37 | id = json["id"].int 38 | name = json["name"].string 39 | description = json["description"].string 40 | resourceState = json["resource_state"].strava(ResourceState.self) 41 | city = json["city"].string 42 | state = json["state"].string 43 | country = json["country"].string 44 | clubType = json["club_type"].strava(ClubType.self) 45 | sportType = json["sport_type"].strava(SportType.self) 46 | profileMedium = URL(optionalString: json["profile_medium"].string) 47 | profile = URL(optionalString: json["profile"].string) 48 | isPrivate = json["private"].bool 49 | memberCount = json["member_count"].int 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Comment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Comment.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 24/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Comments on an activity can be viewed by any user. However, only internal applications are allowed to create or delete them. 14 | Comment posting can be enabled on a per application basis, email developers@strava.com for more information. 15 | **/ 16 | public final class Comment: Strava { 17 | public let id: Int? 18 | public let resourceState: ResourceState? 19 | public let activityId: Int? 20 | public let text: String? 21 | public let athlete: Athlete? 22 | public let createdAt: Date? 23 | 24 | /** 25 | Initializer 26 | 27 | - Parameter json: SwiftyJSON object 28 | - Internal 29 | **/ 30 | required public init(_ json: JSON) { 31 | id = json["id"].int 32 | resourceState = json["resource_state"].strava(ResourceState.self) 33 | activityId = json["activity_id"].int 34 | text = json["text"].string 35 | athlete = json["athlete"].strava(Athlete.self) 36 | createdAt = json["created_at"].string?.toDate() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/StravaSwift/DefaultTokenDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultTokenDelegate.swift 3 | // Pods 4 | // 5 | // Created by MATTHEW CLARKSON on 24/05/2016. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | The default token delegate. You should replace this with something that persists the token (e.g. to NSUserDefaults) 13 | **/ 14 | open class DefaultTokenDelegate: TokenDelegate { 15 | fileprivate var token: OAuthToken? 16 | 17 | /** 18 | Retrieves the token 19 | 20 | - Returns: a optional OAuthToken 21 | **/ 22 | open func get() -> OAuthToken? { 23 | return token 24 | } 25 | 26 | /** 27 | Stores the token internally (note that it is not persisted between app start ups) 28 | 29 | - Parameter token: an optional OAuthToken 30 | **/ 31 | open func set(_ token: OAuthToken?) { 32 | self.token = token 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Effort.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Effort.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | A segment effort represents an athlete’s attempt at a segment. It can also be thought of as a portion of a ride that covers a segment. The object is returned in summary or detailed representations. They are currently the same. 14 | **/ 15 | public final class Effort: Strava { 16 | public let id: Int? 17 | public let resourceState: ResourceState? 18 | public let name: String? 19 | public let activity: MetaActivity? 20 | public let athlete: Athlete? 21 | public let elapsedTime: Int? 22 | public let movingTime: Int? 23 | public let startDate: Date? 24 | public let startDateLocal: Date? 25 | public let distance: Double? 26 | public let startIndex: Int? 27 | public let endIndex: Int? 28 | public let averageCadence: Double? 29 | public let averageWatts: Double? 30 | public let deviceWatts: Bool? 31 | public let averageHeartRate: Double? 32 | public let maxHeartRate: Int? 33 | public let segment: Segment? 34 | public let komRank: Int? 35 | public let prRank: Int? 36 | public let hidden: Bool? 37 | 38 | /** 39 | Initializer 40 | 41 | - Parameter json: SwiftyJSON object 42 | - Internal 43 | **/ 44 | required public init(_ json: JSON) { 45 | id = json["id"].int 46 | resourceState = json["resource_state"].strava(ResourceState.self) 47 | name = json["name"].string 48 | activity = json["activity"].strava(MetaActivity.self) 49 | athlete = json["athlete"].strava(Athlete.self) 50 | elapsedTime = json["elapsed_time"].int 51 | movingTime = json["moving_time"].int 52 | startDate = json["start_date"].string?.toDate() 53 | startDateLocal = json["start_date_local"].string?.toDate() 54 | distance = json["distance"].double 55 | startIndex = json["start_index"].int 56 | endIndex = json["end_index"].int 57 | averageCadence = json["average_cadence"].double 58 | averageWatts = json["average_watts"].double 59 | deviceWatts = json["device_watts"].bool 60 | averageHeartRate = json["average_heartrate"].double 61 | maxHeartRate = json["max_heartrate"].int 62 | segment = json["segment"].strava(Segment.self) 63 | komRank = json["kom_rank"].int 64 | prRank = json["pr_rank"].int 65 | hidden = json["hidden"].bool 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Event.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Event.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Group Events are optionally recurring events for club members. Only club members can access private club events. The objects are returned in summary representation. 14 | **/ 15 | public final class Event: Strava { 16 | public let id: Int? 17 | public let resourceState: ResourceState? 18 | public let title: String? 19 | public let descr: String? 20 | public let clubId: Int? 21 | public let organizingAthlete: Athlete? 22 | public let activityType: ActivityType? 23 | public let createdAt: Date? 24 | public let routeId: Int? 25 | public let womenOnly: Bool? 26 | public let `private`: Bool? 27 | public let skillLevels: SkillLevel? 28 | public let terrain: Terrain? 29 | public let upcomingOccurrences: [Date]? 30 | 31 | /** 32 | Initializer 33 | 34 | - Parameter json: SwiftyJSON object 35 | - Internal 36 | **/ 37 | required public init(_ json: JSON) { 38 | resourceState = json["resource_state"].strava(ResourceState.self) 39 | id = json["id"].int 40 | title = json["title"].string 41 | descr = json["description"].string 42 | clubId = json["club_id"].int 43 | organizingAthlete = json["organizing_athlete"].strava(Athlete.self) 44 | activityType = json["activity_type"].strava(ActivityType.self) 45 | createdAt = json["created_at"].string?.toDate() 46 | routeId = json["route_id"].int 47 | womenOnly = json["women_only"].bool 48 | `private` = json["private"].bool 49 | skillLevels = json["skill_level"].strava(SkillLevel.self) 50 | terrain = json["terrain"].strava(Terrain.self) 51 | upcomingOccurrences = json["terrain"].arrayValue.compactMap { $0.string?.toDate() } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/StravaSwift/GearBikeShoe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Gear.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 15/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Gear represents equipment used during an activity. The object is returned in summary or detailed representations. 14 | **/ 15 | open class Gear: Strava { 16 | public let id: String? 17 | public let primary: Bool? 18 | public let name: String? 19 | public let description: String? 20 | public let resourceState: ResourceState? 21 | public let distance: Double? 22 | public let brandName: String? 23 | public let modelName: String? 24 | 25 | /** 26 | Initializer 27 | 28 | - Parameter json: SwiftyJSON object 29 | - Internal 30 | **/ 31 | required public init(_ json: JSON) { 32 | id = json["id"].string 33 | primary = json["primary"].bool 34 | name = json["name"].string 35 | description = json["description"].string 36 | resourceState = json["resource_state"].strava(ResourceState.self) 37 | distance = json["distance"].double 38 | brandName = json["brand_name"].string 39 | modelName = json["model_name"].string 40 | } 41 | } 42 | 43 | /** 44 | Shoe represents shoes worn on a run. The object is returned in summary or detailed representations. 45 | **/ 46 | public final class Shoe: Gear {} 47 | 48 | /** 49 | Bike represents a... bike! The object is returned in summary or detailed representations. 50 | **/ 51 | public final class Bike: Gear { 52 | public let frameType: FrameType? 53 | 54 | /** 55 | Initializer 56 | 57 | - Parameter json: SwiftyJSON object 58 | - Internal 59 | **/ 60 | required public init(_ json: JSON) { 61 | frameType = json["frame_type"].strava(FrameType.self) 62 | super.init(json) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/StravaSwift/GeneralExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeneralExtensions.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 19/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension RawRepresentable { 12 | init?(optionalRawValue rawValue: RawValue?) { 13 | guard let rawValue = rawValue, let value = Self(rawValue: rawValue) else { return nil } 14 | self = value 15 | } 16 | } 17 | 18 | extension DateFormatter { 19 | func dateFromString(optional string: String?) -> Date? { 20 | guard let string = string else { return nil } 21 | return date(from: string) 22 | } 23 | } 24 | 25 | extension URL { 26 | init?(optionalString string: String?) { 27 | guard let string = string else { return nil } 28 | self.init(string: string) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Location.swift 3 | // StravaSwift 4 | // 5 | // Created by MATTHEW CLARKSON on 23/05/2016. 6 | // Copyright © 2016 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Represents the latitude and longitude of a point 14 | **/ 15 | public struct Location: Strava { 16 | public let lat: Double? 17 | public let lng: Double? 18 | 19 | /** 20 | Initializer (failable) 21 | 22 | - Parameter json: SwiftyJSON object 23 | - Internal 24 | **/ 25 | public init(_ json: JSON) { 26 | let points = json.arrayValue 27 | self.lat = points.first?.double 28 | self.lng = points.last?.double 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Map.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Map.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 24/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Represents a map of a ride or route 14 | **/ 15 | public final class Map: Strava { 16 | public let id: String? 17 | public let resourceState: ResourceState? 18 | public let polyline: String? 19 | public let summaryPolyline: String? 20 | 21 | /** 22 | Initializer 23 | 24 | - Parameter json: SwiftyJSON object 25 | - Internal 26 | **/ 27 | required public init(_ json: JSON) { 28 | id = json["id"].string 29 | resourceState = json["resource_state"].strava(ResourceState.self) 30 | polyline = json["polyline"].string 31 | summaryPolyline = json["summary_polyline"].string 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/StravaSwift/ModelEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelEnums.swift 3 | // StravaSwift 4 | // 5 | // Created by MATTHEW CLARKSON on 23/05/2016. 6 | // Copyright © 2016 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | 10 | /** 11 | Athlete's gender 12 | 13 | - Male 14 | - Female 15 | **/ 16 | public enum Sex: String { 17 | case male = "M" 18 | case female = "F" 19 | } 20 | 21 | /** 22 | Following status of the athlete 23 | 24 | - Pending 25 | - Accepted 26 | - Blocked 27 | **/ 28 | public enum FollowingStatus: String { 29 | case accepted 30 | case blocked 31 | case pending 32 | } 33 | 34 | /** 35 | Membership status for a club 36 | 37 | - Pending 38 | - Accepted 39 | **/ 40 | public enum MembershipStatus: String { 41 | case member 42 | case pending 43 | } 44 | 45 | /** 46 | Measurement units 47 | 48 | - Feet 49 | - Meters 50 | **/ 51 | public enum Units: String { 52 | case feet 53 | case meters 54 | } 55 | 56 | /** 57 | Resource state (describes the detail of the resource) 58 | 59 | - Meta 60 | - Summary 61 | - Detailed 62 | **/ 63 | public enum ResourceState: Int { 64 | case meta = 1 65 | case summary = 2 66 | case detailed = 3 67 | } 68 | 69 | /** 70 | Workout type 71 | 72 | - Run 73 | - RaceRun 74 | - LongRun 75 | - WorkOutRun 76 | - Ride 77 | - RaceRide 78 | - WorkoutRide 79 | **/ 80 | public enum WorkoutType: Int { 81 | case run = 0 82 | case raceRun = 1 83 | case longRun = 3 84 | case workoutRun = 4 85 | case ride = 10 86 | case raceRide = 11 87 | case workoutRide = 12 88 | } 89 | 90 | /** 91 | Activity type 92 | 93 | - AlpineSki 94 | - BackcountrySki 95 | - Canoeing 96 | - Crossfit 97 | - EBikeRide 98 | - Elliptical 99 | - Golf 100 | - Handcycle 101 | - Hike 102 | - IceSkate 103 | - InlineSkate 104 | - Kayaking 105 | - Kitesurf 106 | - NordicSki 107 | - Ride 108 | - RockClimbing 109 | - RollerSki 110 | - Rowing 111 | - Run 112 | - Sail 113 | - Skateboard 114 | - Snowboard 115 | - Snowshoe 116 | - Soccer 117 | - StairStepper 118 | - StandUpPaddling 119 | - Surfing 120 | - Swim 121 | - Velomobile 122 | - VirtualRide 123 | - VirtualRun 124 | - Walk 125 | - WeightTraining 126 | - Wheelchair 127 | - Windsurf 128 | - Workout 129 | - Yoga 130 | **/ 131 | public enum ActivityType: String { 132 | case alpineSki = "AlpineSki" 133 | case backcountrySki = "BackcountrySki" 134 | case canoeing = "Canoeing" 135 | case crossfit = "Crossfit" 136 | case eBikeRide = "EBikeRide" 137 | case elliptical = "Elliptical" 138 | case golf = "Golf" 139 | case handcycle = "Handcycle" 140 | case hike = "Hike" 141 | case iceSkate = "IceSkate" 142 | case inlineSkate = "InlineSkate" 143 | case kayaking = "Kayaking" 144 | case kitesurf = "Kitesurf" 145 | case nordicSki = "NordicSki" 146 | case ride = "Ride" 147 | case rockClimbing = "RockClimbing" 148 | case rollerSki = "RollerSki" 149 | case rowing = "Rowing" 150 | case run = "Run" 151 | case sail = "Sail" 152 | case skateboard = "Skateboard" 153 | case snowboard = "Snowboard" 154 | case snowshoe = "Snowshoe" 155 | case soccer = "Soccer" 156 | case stairStepper = "StairStepper" 157 | case standUpPaddling = "StandUpPaddling" 158 | case surfing = "Surfing" 159 | case swim = "Swim" 160 | case velomobile = "Velomobile" 161 | case virtualRide = "VirtualRide" 162 | case virtualRun = "VirtualRun" 163 | case walk = "Walk" 164 | case weightTraining = "WeightTraining" 165 | case wheelchair = "Wheelchair" 166 | case windsurf = "Windsurf" 167 | case workout = "Workout" 168 | case yoga = "Yoga" 169 | } 170 | 171 | /** 172 | Sport type 173 | 174 | - Cycling 175 | - Running 176 | - Triathlon 177 | - Other 178 | **/ 179 | public enum SportType: String { 180 | case cycling 181 | case running 182 | case triathlon 183 | case other 184 | } 185 | 186 | /** 187 | Club type 188 | 189 | - CasualClub 190 | - RacingTeam 191 | - Shop 192 | - Company 193 | - Other 194 | **/ 195 | public enum ClubType: String { 196 | case casualClub = "casual_club" 197 | case racingTeam = "racing_team" 198 | case shop 199 | case company 200 | case other 201 | } 202 | 203 | /** 204 | Frame type (cycling only) 205 | 206 | - MTB 207 | - Cross 208 | - Road 209 | - TimeTrial 210 | **/ 211 | public enum FrameType: Int { 212 | case mtb = 1 213 | case cross = 2 214 | case road = 3 215 | case timeTrial = 4 216 | } 217 | 218 | /** 219 | Resolution type 220 | 221 | - Low 222 | - Medium 223 | - High 224 | **/ 225 | public enum ResolutionType: String { 226 | case low 227 | case medium 228 | case high 229 | } 230 | 231 | /** 232 | Stream type (ie the data type) 233 | 234 | - Time 235 | - LatLng 236 | - Distance 237 | - Altitude 238 | - VelocitySmooth 239 | - HeartRate 240 | - Cadence 241 | - Watts 242 | - Temp 243 | - Moving 244 | - GradeSmooth 245 | **/ 246 | public enum StreamType: String { 247 | case time 248 | case latLng = "latlng" 249 | case distance 250 | case altitude 251 | case velocitySmooth = "velocity_smooth" 252 | case heartRate = "heartrate" 253 | case cadence 254 | case watts 255 | case temp 256 | case moving 257 | case gradeSmooth = "grade_smooth" 258 | 259 | //Description of the units associated with the stream 260 | var unit: String { 261 | switch self { 262 | case .time: 263 | return "integer seconds" 264 | case .latLng: 265 | return "floats [latitude, longitude]" 266 | case .distance: 267 | return "float meters" 268 | case .altitude: 269 | return "float meters" 270 | case .velocitySmooth: 271 | return "float meters per second" 272 | case .heartRate: 273 | return "integer BPM" 274 | case .cadence: 275 | return "integer RPM" 276 | case .watts: 277 | return "integer watts" 278 | case .temp: 279 | return "integer degrees Celsius" 280 | case .moving: 281 | return "boolean" 282 | case .gradeSmooth: 283 | return "float percent" 284 | } 285 | } 286 | } 287 | 288 | /** 289 | Skill level 290 | 291 | - Casual 292 | - Tempo 293 | - Hammerfest 294 | **/ 295 | public enum SkillLevel: Int { 296 | case casual = 1 297 | case tempo = 2 298 | case hammerfest = 4 299 | } 300 | 301 | /** 302 | Terrain description 303 | 304 | - MostlyFlat 305 | - RollingHills 306 | - KillerClimbs 307 | **/ 308 | public enum Terrain: Int { 309 | case mostlyFlat 310 | case rollingHills 311 | case killerClimbs 312 | } 313 | 314 | /** 315 | Photo source 316 | 317 | - Strava 318 | - Instagram 319 | **/ 320 | public enum PhotoSource: Int { 321 | case strava = 1 322 | case instagram = 2 323 | } 324 | 325 | /** 326 | Achievement type 327 | 328 | - Overall 329 | - PR 330 | - YearOverall 331 | **/ 332 | public enum AchievementType: String { 333 | case overall 334 | case pr 335 | case yearOverall = "year_overall" 336 | } 337 | 338 | /** Route type enum **/ 339 | public enum RouteType: Int { 340 | case ride = 1 341 | case run = 2 342 | } 343 | 344 | /** Route sub type enum **/ 345 | public enum RouteSubType: Int { 346 | case road = 1 347 | case mtb = 2 348 | case cx = 3 349 | case trail = 4 350 | case mixed = 5 351 | } 352 | 353 | /** 354 | Data type enum for uploaded activities 355 | **/ 356 | public enum DataType: String { 357 | case fit 358 | case fitGz = "fit.gz" 359 | case tcx 360 | case tcxGz = "tcx.gz" 361 | case gpx 362 | case gpxGz = "gpx.gz" 363 | } 364 | -------------------------------------------------------------------------------- /Sources/StravaSwift/NSURLExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSURLExtensions.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 13/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URL { 12 | func getQueryParameters() -> Dictionary? { 13 | var results = [String:String]() 14 | let keyValues = self.query?.components(separatedBy: "&") 15 | keyValues?.forEach { 16 | let kv = $0.components(separatedBy: "=") 17 | if kv.count > 1 { 18 | results[kv[0]] = kv[1] 19 | } 20 | } 21 | return results 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/StravaSwift/OAuthToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OAuthToken.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | OAuthToken which is required for requesting Strava resources 14 | **/ 15 | public struct OAuthToken: Strava { 16 | 17 | /** The access token **/ 18 | public let accessToken: String? 19 | 20 | /** The refresh token **/ 21 | public let refreshToken: String? 22 | 23 | /** Expiry for the token in seconds since the epoch **/ 24 | public let expiresAt : Int? 25 | 26 | /** The athlete **/ 27 | public let athlete: Athlete? 28 | 29 | /** 30 | Initializers 31 | 32 | - Parameter json: A SwiftyJSON object 33 | **/ 34 | public init(_ json: JSON) { 35 | accessToken = json["access_token"].string 36 | refreshToken = json["refresh_token"].string 37 | expiresAt = json["expires_at"].int 38 | athlete = Athlete(json["athlete"]) 39 | } 40 | 41 | public init(access: String?, refresh: String?, expiry: Int?) { 42 | self.accessToken = access 43 | self.refreshToken = refresh 44 | self.expiresAt = expiry 45 | self.athlete = nil 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Photo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Photo.swift 3 | // StravaSwift 4 | // 5 | // Created by MATTHEW CLARKSON on 23/05/2016. 6 | // Copyright © 2016 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Photos are objects associated with an activity. Currently, the only external photo source is Instagram. Photos can also be stored on Strava - these photos are referred to as “native”. 14 | **/ 15 | public final class Photo: Strava { 16 | public let id: Int? 17 | public let uniqueId: String? 18 | public let activityId: Int? 19 | public let resourceState: ResourceState? 20 | public let urls: [URL]? 21 | public let caption: String? 22 | public let source: PhotoSource? 23 | public let uploadedAt: Date? 24 | public let createdAt: Date? 25 | public let location: Location? 26 | public let refs: String? 27 | public let uuid: String? 28 | public let type: String? 29 | 30 | /** 31 | Initializer 32 | 33 | - Parameter json: SwiftyJSON object 34 | - Internal 35 | **/ 36 | required public init(_ json: JSON) { 37 | id = json["id"].int 38 | resourceState = json["resource_state"].strava(ResourceState.self) 39 | uniqueId = json["unique_id"].string 40 | activityId = json["activity_id"].int 41 | urls = json["urls"].dictionary?.compactMap { URL(optionalString: $0.1.string) } 42 | caption = json["caption"].string 43 | source = json["source"].strava(PhotoSource.self) 44 | uploadedAt = json["uploaded_at"].string?.toDate() 45 | createdAt = json["created_at"].string?.toDate() 46 | location = json["location"].strava(Location.self) 47 | refs = json["refs"].string 48 | uuid = json["uuid"].string 49 | type = json["type"].string 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Route.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Route.swift 3 | // Pods 4 | // 5 | // Created by MATTHEW CLARKSON on 25/05/2016. 6 | // 7 | // 8 | 9 | import SwiftyJSON 10 | 11 | /** 12 | Routes are manually-created paths made up of sections called legs. 13 | **/ 14 | public final class Route: Strava { 15 | public let id: Int? 16 | public let resourceState: ResourceState? 17 | public let name: String? 18 | public let description: String? 19 | public let athlete: Athlete? 20 | public let distance: Double? 21 | public let elevationGain: Double? 22 | public let map: Map? 23 | public let type: RouteType? 24 | public let subType: RouteSubType? 25 | public let `private`: Bool? 26 | public let starred: Bool? 27 | public let timeStamp: Int? 28 | public let segments: [Segment]? 29 | 30 | /** 31 | Initializer 32 | 33 | - Parameter json: SwiftyJSON object 34 | - Internal 35 | **/ 36 | required public init(_ json: JSON) { 37 | id = json["id"].int 38 | resourceState = json["resource_state"].strava(ResourceState.self) 39 | name = json["name"].string 40 | description = json["description"].string 41 | athlete = json["athlete"].strava(Athlete.self) 42 | distance = json["distance"].double 43 | elevationGain = json["elevation_gain"].double 44 | map = json["map"].strava(Map.self) 45 | type = json["type"].strava(RouteType.self) 46 | subType = json["sub_type"].strava(RouteSubType.self) 47 | `private` = json["private"].bool 48 | starred = json["starred"].bool 49 | timeStamp = json["time_stamp"].int 50 | segments = json["segments"].strava(Segment.self) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Router.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Router.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | import SwiftyJSON 12 | 13 | 14 | /** 15 | Router enum for type safe routing. For information on how this is used please [see here](https://github.com/Alamofire/Alamofire#api-parameter-abstraction). 16 | **/ 17 | public enum Router { 18 | 19 | public typealias Id = Int 20 | public typealias Params = [String: Any]? 21 | 22 | /** 23 | Requests a Strava OAuth token 24 | 25 | - parameter code: the code returned from Strava after granting access to the application 26 | **/ 27 | case token(code: String) 28 | 29 | /** 30 | Requests a Strava OAuth token refresh 31 | 32 | - parameter refresh: the refresh token returned from Strava after granting access to the application 33 | **/ 34 | case refresh(refreshToken: String) 35 | 36 | /** 37 | Allows an application to revoke its access to an athlete’s data. This will invalidate all access tokens associated with the ‘athlete,application’ pair used to create the token. The application will be removed from the Athlete Settings page on Strava. All requests made using invalidated tokens will receive a 401 Unauthorized response. 38 | 39 | - parameter token: the access token to deauthorize 40 | **/ 41 | case deauthorize(accessToken: String) 42 | 43 | /** 44 | Gets the current user/athlete 45 | **/ 46 | case athlete 47 | 48 | /** 49 | Updates the current user/athlete 50 | **/ 51 | case updateAthlete 52 | 53 | /** 54 | Lists the current user/athlete's activities 55 | 56 | - parameter params: a [String: String] dictionary of acceptable parameters 57 | **/ 58 | case athleteActivities(params: Params) 59 | 60 | /** 61 | Lists the current user/athlete's friends 62 | 63 | - parameter params: a [String: String] dictionary of acceptable parameters 64 | **/ 65 | case athleteFriends(params: Params) 66 | 67 | /** 68 | Lists the current user/athlete's followers 69 | 70 | - parameter params: a [String: String] dictionary of acceptable parameters 71 | **/ 72 | case athleteFollowers(params: Params) 73 | 74 | /** 75 | Lists the current user/athlete's clubs 76 | 77 | - parameter params: a [String: String] dictionary of acceptable parameters 78 | **/ 79 | case athleteClubs(params: Params) 80 | 81 | /** 82 | Gets a specific athlete 83 | 84 | - parameter id: the athlete id 85 | - parameter params: a [String: String] dictionary of acceptable parameters 86 | **/ 87 | case athletes(id: Id, params: Params) 88 | 89 | /** 90 | Lists a specific athlete's friends 91 | 92 | - parameter id: the athlete id 93 | - parameter params: a [String: String] dictionary of acceptable parameters 94 | **/ 95 | case athletesFriends(id: Id, params: Params) 96 | 97 | /** 98 | Lists a specific athlete's followers 99 | 100 | - parameter id: the athlete id 101 | - parameter params: a [String: String] dictionary of acceptable parameters 102 | **/ 103 | case athletesFollowers(id: Id, params: Params) 104 | 105 | /** 106 | Lists athletes the current user and the requested athlete are both following 107 | 108 | - parameter id: the athlete id 109 | - parameter params: a [String: String] dictionary of acceptable parameters 110 | **/ 111 | case athletesBothFollowing(id: Id, params: Params) 112 | 113 | /** 114 | Gets the statistics for a specific athlete 115 | 116 | - parameter id: the athlete id 117 | - parameter params: a [String: String] dictionary of acceptable parameters 118 | **/ 119 | case athletesStats(id: Id, params: Params) 120 | 121 | /** 122 | Lists the specific athlete's KOMS 123 | 124 | - parameter id: the athlete id 125 | - parameter params: a [String: String] dictionary of acceptable parameters 126 | **/ 127 | case athletesKoms(id: Id, params: Params) 128 | 129 | /** 130 | Creates a new manual activity in Strava for the athlete (not for uploading files) 131 | 132 | - parameter params: a [String: String] dictionary representing the activity 133 | **/ 134 | case createActivity(params: Params) 135 | 136 | /** 137 | Updates an activity, requires write permissions 138 | - parameter activity: an Activity object 139 | **/ 140 | case updateActivity(activity: Activity) 141 | 142 | /** 143 | Deletes an activity, requires write permissions 144 | 145 | - parameter activity: an Activity object 146 | **/ 147 | case deleteActivity(activity: Activity) 148 | 149 | /** 150 | Gets an activity 151 | 152 | - parameter id: the activity id 153 | - parameter params: a [String: String] dictionary of acceptable parameters 154 | **/ 155 | case activities(id: Id, params: Params) 156 | 157 | /** 158 | Lists kudos for an activity 159 | 160 | - parameter id: the activity id 161 | - parameter params: a [String: String] dictionary of acceptable parameters 162 | **/ 163 | case activitiesKudos(id: Id, params: Params) 164 | 165 | /** 166 | Lists comments for an activity 167 | 168 | - parameter id: the activity id 169 | - parameter params: a [String: String] dictionary of acceptable parameters 170 | **/ 171 | case activitiesComments(id: Id, params: Params) 172 | 173 | /** 174 | Lists photos for an activity 175 | 176 | - parameter id: the activity id 177 | - parameter params: a [String: String] dictionary of acceptable parameters 178 | **/ 179 | case activitiesPhotos(id: Id, params: Params) 180 | 181 | /** 182 | Lists related activities 183 | 184 | - parameter id: the activity id 185 | - parameter params: a [String: String] dictionary of acceptable parameters 186 | **/ 187 | case activitiesRelated(id: Id, params: Params) 188 | 189 | /** 190 | List the recent activities performed by the current athlete and those they are following. Pagination is supported. However, results are limited to the last 200 total activities. 191 | 192 | - parameter id: the activity id 193 | - parameter params: a [String: String] dictionary of acceptable parameters 194 | **/ 195 | case activitiesFriends(id: Id, params: Params) 196 | 197 | /** 198 | Heartrate and power zones are set by the athlete. This endpoint returns the time (seconds) in each zone. The distribution is not customizable. Requires an access token associated with the owner of the activity and the owner must be a premium user. 199 | 200 | - parameter id: the activity id 201 | - parameter params: a [String: String] dictionary of acceptable parameters 202 | **/ 203 | case activitiesZones(id: Id, params: Params) 204 | 205 | /** 206 | Lists all laps associated with an activity 207 | 208 | - parameter id: the activity id 209 | - parameter params: a [String: String] dictionary of acceptable parameters 210 | **/ 211 | case activitiesLaps(id: Id, params: Params) 212 | 213 | /** 214 | Retrieves a club 215 | 216 | - parameter id: the club id 217 | - parameter params: a [String: String] dictionary of acceptable parameters 218 | **/ 219 | case clubs(id: Id, params: Params) 220 | 221 | /** 222 | Lists announcments for a club 223 | 224 | - parameter id: the club id 225 | - parameter params: a [String: String] dictionary of acceptable parameters 226 | **/ 227 | case clubsAnnouncements(id: Id, params: Params) 228 | 229 | /** 230 | Lists events for a club 231 | 232 | - parameter id: the club id 233 | - parameter params: a [String: String] dictionary of acceptable parameters 234 | **/ 235 | case clubsEvents(id: Id, params: Params) 236 | 237 | /** 238 | Lists members of a club 239 | 240 | - parameter id: the club id 241 | - parameter params: a [String: String] dictionary of acceptable parameters 242 | **/ 243 | case clubsMembers(id: Id, params: Params) 244 | 245 | /** 246 | Lists activities by club members 247 | 248 | - parameter id: the club id 249 | - parameter params: a [String: String] dictionary of acceptable parameters 250 | **/ 251 | case clubsActivities(id: Id, params: Params) 252 | 253 | /** 254 | Join a club (ie the current athlete) 255 | 256 | - parameter id: the club id 257 | **/ 258 | case clubsJoin(id: Id) 259 | 260 | /** 261 | Leave a club (ie the current athlete) 262 | 263 | - parameter id: the club id 264 | **/ 265 | case clubsLeave(id: Id) 266 | 267 | /** 268 | Retrieves a gear object 269 | 270 | - parameter id: the gear id 271 | **/ 272 | case gear(id: Id, params: Params) 273 | 274 | /** 275 | Retrieves a segment 276 | 277 | - parameter id: the segment id 278 | - parameter params: a [String: String] dictionary of acceptable parameters 279 | **/ 280 | case segments(id: Id, params: Params) 281 | 282 | /** 283 | Lists segments the current athlete has starred 284 | 285 | - parameter params: a [String: String] dictionary of acceptable parameters 286 | **/ 287 | case segmentsStarred(params: Params) 288 | 289 | /** 290 | Lists efforts for a segment 291 | 292 | - parameter id: the segment id 293 | - parameter params: a [String: String] dictionary of acceptable parameters 294 | **/ 295 | case segmentsEfforts(id: Id, params: Params) 296 | 297 | /** 298 | Lists the leaderboards for a segment 299 | 300 | - parameter id: the segment id 301 | - parameter params: a [String: String] dictionary of acceptable parameters 302 | **/ 303 | case segmentsLeaderboards(id: Id, params: Params) 304 | 305 | /** 306 | Find popular segments within a given area. 307 | 308 | - parameter id: the segment id 309 | - parameter params: a [String: String] dictionary of acceptable parameters 310 | **/ 311 | case segmentsExplore(id: Id, params: Params) 312 | 313 | /** 314 | Retrieve details about a specific segment effort. The effort must be public or it must correspond to the current athlete. 315 | 316 | - parameter id: the segment efforr id 317 | - parameter params: a [String: String] dictionary of acceptable parameters 318 | **/ 319 | case segmentEfforts(id: Id, params: Params) 320 | 321 | /** 322 | Retrieves details about a route. Private routes can only be accessed if owned by the authenticating user and the token has view_private permissions. For raw data associated with a route see route streams. 323 | 324 | - parameter id: the route id 325 | **/ 326 | case routes(id: Id) 327 | 328 | /** 329 | Lists a specific athlete’s routes. Private routes will only be included if the authenticating user is viewing their own routes and the token has view_private permissions. 330 | 331 | - parameter id: the route id 332 | - parameter params: a [String: String] dictionary of acceptable parameters 333 | **/ 334 | case athleteRoutes(id: Id, params: Params) 335 | 336 | /** 337 | Streams represent the raw data of the uploaded file. External applications may only access this information for activities owned by the authenticated athlete. 338 | 339 | - parameter id: the activity id 340 | - parameter types: single stream type or comma-separated list of types, if the activity does not have that stream it will not be included in the response 341 | **/ 342 | case activityStreams(id: Id, types: String) 343 | 344 | /** 345 | A segment effort represents an attempt on a segment. This resource returns a subset of the activity streams that correspond to that effort. 346 | 347 | - parameter id: the effort id 348 | - parameter params: a [String: String] dictionary of acceptable parameters 349 | **/ 350 | case effortStreams(id: Id, types: String) 351 | 352 | /** 353 | Retrieve segment streams 354 | 355 | Only distance, altitude and latlng stream types are available. 356 | 357 | - parameter id: the effort id 358 | - parameter params: a [String: String] dictionary of acceptable parameters 359 | **/ 360 | case segmentStreams(id: Id, types: String) 361 | 362 | /** 363 | Retrieve route streams 364 | 365 | Distance, altitude and latlng stream types are always returned. 366 | 367 | - parameter id: the activity id 368 | **/ 369 | case routeStreams(id: Id) 370 | 371 | /** 372 | Upload an activity 373 | 374 | Requires write permissions, as requested during the authorization process. 375 | 376 | Posting a file for upload will enqueue it for processing. Initial checks will be done for malformed data and duplicates. 377 | 378 | - parameter upload: an upload object 379 | **/ 380 | case uploadFile(upload: StravaSwift.UploadData) 381 | 382 | /** 383 | Check upload status 384 | 385 | Upon upload, Strava will respond with an upload ID. You may use this ID to poll the status of your upload. Strava recommends polling no more than once a second. Polling more frequently is unnecessary. The mean processing time is around 8 seconds. 386 | 387 | - parameter id: the upload id 388 | **/ 389 | case uploads(id: Id) 390 | 391 | 392 | } 393 | 394 | extension Router: URLRequestConvertible { 395 | 396 | /** 397 | The Strava app authorization deeplink url including the oauth query parameters 398 | **/ 399 | static var appAuthorizationUrl: URL { 400 | let baseUrl = "strava://oauth/mobile/authorize" 401 | let authParams = StravaClient.sharedInstance.authParams 402 | .map { "\($0.key)=\($0.value)" } 403 | .joined(separator:"&") 404 | return URL(string: "\(baseUrl)?\(authParams)")! 405 | } 406 | 407 | /** 408 | The Strava web authorization url including the oauth query parameters 409 | **/ 410 | static var webAuthorizationUrl: URL { 411 | let baseUrl = "https://www.strava.com/oauth/authorize" 412 | let authParams = StravaClient.sharedInstance.authParams 413 | .map { "\($0.key)=\($0.value)" } 414 | .joined(separator:"&") 415 | return URL(string: "\(baseUrl)?\(authParams)")! 416 | } 417 | 418 | /** 419 | The Url Request 420 | **/ 421 | public func asURLRequest () throws -> URLRequest { 422 | let config = self.requestConfig 423 | 424 | var baseURL: URL { 425 | switch self { 426 | case .token, .deauthorize, .refresh: 427 | return URL(string: "https://www.strava.com/oauth")! 428 | default: 429 | return URL(string: "https://www.strava.com/api/v3")! 430 | } 431 | } 432 | 433 | var urlRequest = URLRequest(url: baseURL.appendingPathComponent(config.path)) 434 | urlRequest.httpMethod = config.method.rawValue 435 | 436 | if let token = StravaClient.sharedInstance.token?.accessToken { 437 | urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") 438 | } 439 | 440 | if let params = config.params, params.count > 0 { 441 | switch config.method { 442 | case .get: 443 | var urlComponents = URLComponents(url: urlRequest.url!, resolvingAgainstBaseURL: false)! 444 | urlComponents.queryItems = params.map { URLQueryItem(name: $0, value: "\($1)")} 445 | urlRequest.url = urlComponents.url! 446 | return urlRequest 447 | default: 448 | return try JSONEncoding.default.encode(urlRequest, with: params) 449 | } 450 | } else { 451 | return try JSONEncoding.default.encode(urlRequest) 452 | } 453 | } 454 | } 455 | 456 | extension Router { 457 | 458 | fileprivate var requestConfig: (path: String, params: Params, method: Alamofire.HTTPMethod) { 459 | switch self { 460 | 461 | case .token(let code): 462 | return ("/token", StravaClient.sharedInstance.tokenParams(code), .post) 463 | case .refresh(let refreshToken): 464 | return ("/token", StravaClient.sharedInstance.refreshParams(refreshToken), .post) 465 | case .deauthorize(let token): 466 | let params = ["access_token" : token] 467 | return ("/deauthorize", params, .post) 468 | 469 | case .athlete: 470 | return ("/athlete", nil, .get) 471 | case .updateAthlete: 472 | return ("/athlete", nil, .put) 473 | case .athleteFriends(let params): 474 | return ("/athlete/friends", params, .get) 475 | case .athleteFollowers(let params): 476 | return ("/athlete/followers", params, .get) 477 | case .athleteClubs(let params): 478 | return ("/athlete/clubs", params, .get) 479 | case .athleteActivities(let params): 480 | return ("/athlete/activities", params, .get) 481 | 482 | case .athletes(let id, let params): 483 | return ("/athletes/\(id)", params, .get) 484 | case .athletesFriends(let id, let params): 485 | return ("/athletes/\(id)/friends", params, .get) 486 | case .athletesFollowers(let id, let params): 487 | return ("/athletes/\(id)/friends", params, .get) 488 | case .athletesBothFollowing(let id, let params): 489 | return ("/athletes/\(id)/both-following", params, .get) 490 | case .athletesStats(let id, let params): 491 | return ("/athletes/\(id)/stats", params, .get) 492 | case .athletesKoms(let id, let params): 493 | return ("/athletes/\(id)/koms", params, .get) 494 | 495 | case .createActivity(let params): 496 | return ("/activities", params, .post) 497 | case .updateActivity(let activity): 498 | return ("/activities/\(activity.id!)", nil, .put) 499 | case .deleteActivity(let activity): 500 | return ("/activities/\(activity.id!)", nil, .delete) 501 | 502 | case .activities(let id, let params): 503 | return ("/activities/\(id)", params, .get) 504 | case .activitiesKudos(let id, let params): 505 | return ("/activities/\(id)/kudos", params, .get) 506 | case .activitiesComments(let id, let params): 507 | return ("/activities/\(id)/comments", params, .get) 508 | case .activitiesPhotos(let id, let params): 509 | return ("/activities/\(id)/photos/photo_sources=true", params, .get) 510 | case .activitiesRelated(let id, let params): 511 | return ("/activities/\(id)/related", params, .get) 512 | case .activitiesFriends(let id, let params): 513 | return ("/activities/\(id)/following", params, .get) 514 | case .activitiesZones(let id, let params): 515 | return ("/activities/\(id)/zones", params, .get) 516 | case .activitiesLaps(let id, let params): 517 | return ("/activities/\(id)/laps", params, .get) 518 | 519 | case .clubs(let id, let params): 520 | return ("/clubs/\(id)", params, .get) 521 | case .clubsAnnouncements(let id, let params): 522 | return ("/clubs/\(id)/announcements", params, .get) 523 | case .clubsEvents(let id, let params): 524 | return ("/clubs/\(id)/events", params, .get) 525 | case .clubsMembers(let id, let params): 526 | return ("/clubs/\(id)/members", params, .get) 527 | case .clubsActivities(let id, let params): 528 | return ("/clubs/\(id)/activities", params, .get) 529 | case .clubsJoin(let id): 530 | return ("/clubs/\(id)/join", nil, .post) 531 | case .clubsLeave(let id): 532 | return ("/clubs/\(id)/leave", nil, .post) 533 | 534 | case .segments(let id, let params): 535 | return ("/segments/\(id)", params, .get) 536 | case .segmentsEfforts(let id, let params): 537 | return ("/segments/\(id)/all_efforts", params, .get) 538 | case .segmentsLeaderboards(let id, let params): 539 | return ("/segments/\(id)/leaderboard", params, .get) 540 | case .segmentsExplore(let id, let params): 541 | return ("/segments/\(id)/explore", params, .get) 542 | case .segmentsStarred: 543 | return ("/segments/starred", nil, .get) 544 | case .segmentEfforts(let id, let params): 545 | return ("/segment_efforts/\(id)", params, .get) 546 | 547 | case .gear(let id, let params): 548 | return ("/gear/\(id)", params, .get) 549 | 550 | case .routes(let id): 551 | return ("/routes/\(id)", nil, .get) 552 | case .athleteRoutes(let id, let params): 553 | return ("/athletes/\(id)/routes", params, .get) 554 | 555 | case .activityStreams(let id, let type): 556 | return ("/activities/\(id)/streams/\(type)", nil, .get) 557 | case .effortStreams(let id, let type): 558 | return ("/segment_efforts/\(id)/streams/\(type)", nil, .get) 559 | case .segmentStreams(let id, let type): 560 | return ("/segments/\(id)/streams/\(type)", nil, .get) 561 | case .routeStreams(let id): 562 | return ("/routes/\(id)/streams", nil, .get) 563 | 564 | case .uploadFile(let upload): 565 | return ("/uploads", upload.params, .post) 566 | case .uploads(let id): 567 | return ("/uploads/\(id)", nil, .get) 568 | } 569 | } 570 | } 571 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Segment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Segment.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Segments are specific sections of road. Athletes’ times are compared on these segments and leaderboards are created. 14 | **/ 15 | public final class Segment: Strava { 16 | public let id: Int? 17 | public let name: String? 18 | public let descr: String? 19 | public let resourceState: ResourceState? 20 | public let activityType: ActivityType? 21 | public let distance: Double? 22 | public let averageGrade: Double? 23 | public let maximumGrade: Double? 24 | public let elevationHigh: Double? 25 | public let elevationLow: Double? 26 | public let startLatLng: Location? 27 | public let endLatLng: Location? 28 | public let climbCategory: Int? 29 | public let city: String? 30 | public let state: String? 31 | public let country: String? 32 | public let `private`: Bool? 33 | public let starred: Bool? 34 | public let createdAt: Date? 35 | public let updateAt: Date? 36 | public let totalElevationGain: Double? 37 | public let map: Map? 38 | public let effortCount: Int? 39 | public let athleteCount: Int? 40 | public let hazardous: Bool? 41 | public let starCount: Int? 42 | 43 | /** 44 | Initializer 45 | 46 | - Parameter json: SwiftyJSON object 47 | - Internal 48 | **/ 49 | required public init(_ json: JSON) { 50 | id = json["id"].int 51 | resourceState = json["resource_state"].strava(ResourceState.self) 52 | name = json["name"].string 53 | descr = json["description"].string 54 | activityType = json["activity_type"].strava(ActivityType.self) 55 | distance = json["distance"].double 56 | averageGrade = json["average_grade"].double 57 | maximumGrade = json["maximum_grade"].double 58 | elevationHigh = json["elevation_high"].double 59 | elevationLow = json["elevation_low"].double 60 | startLatLng = json["start_latlng"].strava(Location.self) 61 | endLatLng = json["end_latlng"].strava(Location.self) 62 | climbCategory = json["climb_category"].int 63 | city = json["city"].string 64 | state = json["state"].string 65 | country = json["country"].string 66 | `private` = json["private"].bool 67 | starred = json["starred"].bool 68 | createdAt = json["created_at"].string?.toDate() 69 | updateAt = json["updated_at"].string?.toDate() 70 | totalElevationGain = json["total_elevation_gained"].double 71 | map = json["map"].strava(Map.self) 72 | effortCount = json["effort_count"].int 73 | athleteCount = json["athlete_count"].int 74 | hazardous = json["hazardous"].bool 75 | starCount = json["star_count"].int 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Split.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Split.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 24/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Represents a summary of a split 14 | **/ 15 | public struct Split: Strava { 16 | public let distance: Double? 17 | public let elapsedTime: Int? 18 | public let movingTime: Int? 19 | public let elevationDifference: Int? 20 | public let split: Int? 21 | 22 | /** 23 | Initializer 24 | 25 | - Parameter json: SwiftyJSON object 26 | - Internal 27 | **/ 28 | public init(_ json: JSON) { 29 | distance = json["distance"].double 30 | elapsedTime = json["elapsed_time"].int 31 | movingTime = json["moving_time"].int 32 | elevationDifference = json["elevation_difference"].int 33 | split = json["split"].int 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/StravaSwift/StravaClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StravaClient.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import AuthenticationServices 10 | import Foundation 11 | import Alamofire 12 | import SwiftyJSON 13 | import SafariServices 14 | 15 | /** 16 | StravaClient responsible for making all api requests 17 | */ 18 | open class StravaClient: NSObject { 19 | 20 | /** 21 | Access the shared instance 22 | */ 23 | public static let sharedInstance = StravaClient() 24 | 25 | fileprivate override init() {} 26 | fileprivate var config: StravaConfig? 27 | 28 | public typealias AuthorizationHandler = (Swift.Result) -> () 29 | fileprivate var currentAuthorizationHandler: AuthorizationHandler? 30 | fileprivate var authSession: NSObject? // Holds a reference to ASWebAuthenticationSession / SFAuthenticationSession depending on iOS version 31 | 32 | /** 33 | The OAuthToken returned by the delegate 34 | **/ 35 | open var token: OAuthToken? { return config?.delegate.get() } 36 | 37 | internal var authParams: [String: Any] { 38 | return [ 39 | "client_id" : config?.clientId ?? 0, 40 | "redirect_uri" : config?.redirectUri ?? "", 41 | "scope" : (config?.scopes ?? []).map { $0.rawValue }.joined(separator: ","), 42 | "state" : "ios" as AnyObject, 43 | "approval_prompt" : config?.forcePrompt ?? true ? "force" : "auto", 44 | "response_type" : "code" 45 | ] 46 | } 47 | 48 | internal func tokenParams(_ code: String) -> [String: Any] { 49 | return [ 50 | "client_id" : config?.clientId ?? 0, 51 | "client_secret" : config?.clientSecret ?? "", 52 | "code" : code 53 | ] 54 | } 55 | 56 | internal func refreshParams(_ refreshToken: String) -> [String: Any] { 57 | return [ 58 | "client_id" : config?.clientId ?? 0, 59 | "client_secret" : config?.clientSecret ?? "", 60 | "grant_type" : "refresh_token", 61 | "refresh_token" : refreshToken 62 | ] 63 | } 64 | } 65 | 66 | //MARK:varConfig 67 | 68 | extension StravaClient { 69 | 70 | /** 71 | Initialize the shared instance with your credentials. You must use this otherwise fatal errors will be 72 | returned when making api requests. 73 | 74 | - Parameter config: a StravaConfig struct 75 | - Returns: An instance of self (i.e. StravaClient) 76 | */ 77 | public func initWithConfig(_ config: StravaConfig) -> StravaClient { 78 | self.config = config 79 | 80 | return self 81 | } 82 | } 83 | 84 | //MARK : - Auth 85 | 86 | extension StravaClient: ASWebAuthenticationPresentationContextProviding { 87 | 88 | var currentWindow: UIWindow? { return UIApplication.shared.keyWindow } 89 | var currentViewController: UIViewController? { return currentWindow?.rootViewController } 90 | 91 | /** 92 | Starts the Strava OAuth authorization. The authentication will use the Strava app be default if it is installed on the device. If the user does not have Strava installed, it will fallback on `SFAuthenticationSession` or `ASWebAuthenticationSession` depending on the iOS version used at runtime. 93 | */ 94 | public func authorize(result: @escaping AuthorizationHandler) { 95 | let appAuthorizationUrl = Router.appAuthorizationUrl 96 | if UIApplication.shared.canOpenURL(appAuthorizationUrl) { 97 | currentAuthorizationHandler = result // Stores the handler to be executed once `handleAuthorizationRedirect(url:)` is called 98 | if #available(iOS 10.0, *) { 99 | UIApplication.shared.open(appAuthorizationUrl, options: [:]) 100 | } else { 101 | UIApplication.shared.openURL(appAuthorizationUrl) 102 | } 103 | } else { 104 | if #available(iOS 12.0, *) { 105 | let webAuthenticationSession = ASWebAuthenticationSession(url: Router.webAuthorizationUrl, 106 | callbackURLScheme: config?.redirectUri.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 107 | completionHandler: { (url, error) in 108 | if let url = url, error == nil { 109 | self.handleAuthorizationRedirect(url, result: result) 110 | } else { 111 | result(.failure(error!)) 112 | } 113 | }) 114 | authSession = webAuthenticationSession 115 | if #available(iOS 13.0, *) { 116 | webAuthenticationSession.presentationContextProvider = self 117 | } 118 | webAuthenticationSession.start() 119 | } else { 120 | currentAuthorizationHandler = result // Stores the handler to be executed once `handleAuthorizationRedirect(url:)` is called 121 | UIApplication.shared.open(Router.webAuthorizationUrl, options: [:]) 122 | } 123 | } 124 | } 125 | 126 | /** 127 | Helper method to get the code from the redirection from Strava after the user has authorized the application (useful in AppDelegate) 128 | 129 | - Parameter url the url returned by Strava through the (ASWeb/SF)AuthenricationSession or application open options. 130 | - Returns: a boolean that indicates if this url is for Strava, has a code and can be handled properly 131 | **/ 132 | public func handleAuthorizationRedirect(_ url: URL) -> Bool { 133 | if let redirectUri = config?.redirectUri, url.absoluteString.starts(with: redirectUri), 134 | let params = url.getQueryParameters(), params["code"] != nil, params["scope"] != nil, params["state"] == "ios" { 135 | 136 | self.handleAuthorizationRedirect(url) { result in 137 | if let currentAuthorizationHandler = self.currentAuthorizationHandler { 138 | currentAuthorizationHandler(result) 139 | self.currentAuthorizationHandler = nil 140 | } 141 | } 142 | return true 143 | } else { 144 | return false 145 | } 146 | } 147 | 148 | /** 149 | Helper method to get the code from the redirection from Strava after the user has authorized the application (useful in AppDelegate) 150 | 151 | - Parameter url the url returned by Strava through the (ASWeb/SF)AuthenricationSession or application open options. 152 | - Parameter result a closure to handle the OAuthToken 153 | **/ 154 | private func handleAuthorizationRedirect(_ url: URL, result: @escaping AuthorizationHandler) { 155 | if let code = url.getQueryParameters()?["code"] { 156 | self.getAccessToken(code, result: result) 157 | } else { 158 | result(.failure(generateError(failureReason: "Invalid authorization code", response: nil))) 159 | } 160 | } 161 | 162 | /** 163 | Get an OAuth token from Strava 164 | 165 | - Parameter code: the code (string) returned from strava 166 | - Parameter result: a closure to handle the OAuthToken 167 | **/ 168 | private func getAccessToken(_ code: String, result: @escaping AuthorizationHandler) { 169 | do { 170 | try oauthRequest(Router.token(code: code))?.responseStrava { [weak self] (response: DataResponse) in 171 | guard let self = self else { return } 172 | if let token = response.result.value { 173 | self.config?.delegate.set(token) 174 | result(.success(token)) 175 | } else if let error = response.error { 176 | result(.failure(error)) 177 | } else { 178 | result(.failure(self.generateError(failureReason: "No valid token", response: nil))) 179 | } 180 | } 181 | } catch let error as NSError { 182 | result(.failure(error)) 183 | } 184 | } 185 | 186 | /** 187 | Refresh an OAuth token from Strava 188 | 189 | - Parameter refresh: the refresh token from Strava 190 | - Parameter result: a closure to handle the OAuthToken 191 | **/ 192 | public func refreshAccessToken(_ refreshToken: String, result: @escaping AuthorizationHandler) { 193 | do { 194 | try oauthRequest(Router.refresh(refreshToken: refreshToken))?.responseStrava { [weak self] (response: DataResponse) in 195 | guard let self = self else { return } 196 | if let token = response.result.value { 197 | self.config?.delegate.set(token) 198 | result(.success(token)) 199 | } else { 200 | result(.failure(self.generateError(failureReason: "No valid token", response: nil))) 201 | } 202 | } 203 | } catch let error as NSError { 204 | result(.failure(error)) 205 | } 206 | } 207 | 208 | // ASWebAuthenticationPresentationContextProviding 209 | 210 | @available(iOS 12.0, *) 211 | public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { 212 | return currentWindow ?? ASPresentationAnchor() 213 | } 214 | } 215 | 216 | 217 | //MARK: - Athlete 218 | 219 | extension StravaClient { 220 | 221 | public func upload(_ route: Router, upload: UploadData, result: @escaping (((T)?) -> Void), failure: @escaping (NSError) -> Void) { 222 | do { 223 | try oauthUpload(URLRequest: route.asURLRequest(), upload: upload) { (response: DataResponse) in 224 | if let statusCode = response.response?.statusCode, (400..<500).contains(statusCode) { 225 | failure(self.generateError(failureReason: "Strava API Error", response: response.response)) 226 | } else { 227 | result(response.result.value) 228 | } 229 | } 230 | } catch let error as NSError { 231 | failure(error) 232 | } 233 | } 234 | 235 | /** 236 | Request a single object from the Strava Api 237 | 238 | - Parameter route: a Router enum case which may require parameters 239 | - Parameter result: a closure to handle the returned object 240 | **/ 241 | public func request(_ route: Router, result: @escaping (((T)?) -> Void), failure: @escaping (NSError) -> Void) { 242 | do { 243 | try oauthRequest(route)?.responseStrava { (response: DataResponse) in 244 | // HTTP Status codes above 400 are errors 245 | if let statusCode = response.response?.statusCode, (400..<500).contains(statusCode) { 246 | failure(self.generateError(failureReason: "Strava API Error", response: response.response)) 247 | } else { 248 | result(response.result.value) 249 | } 250 | } 251 | } catch let error as NSError { 252 | failure(error) 253 | } 254 | } 255 | 256 | /** 257 | Request an array of objects from the Strava Api 258 | 259 | - Parameter route: a Router enum case which may require parameters 260 | - Parameter result: a closure to handle the returned objects 261 | **/ 262 | public func request(_ route: Router, result: @escaping ((([T])?) -> Void), failure: @escaping (NSError) -> Void) { 263 | do { 264 | try oauthRequest(route)?.responseStravaArray { (response: DataResponse<[T]>) in 265 | // HTTP Status codes above 400 are errors 266 | if let statusCode = response.response?.statusCode, (400..<500).contains(statusCode) { 267 | failure(self.generateError(failureReason: "Strava API Error", response: response.response)) 268 | } else { 269 | result(response.result.value) 270 | } 271 | } 272 | } catch let error as NSError { 273 | failure(error) 274 | } 275 | } 276 | 277 | fileprivate func generateError(failureReason: String, response: HTTPURLResponse?) -> NSError { 278 | let errorDomain = "com.stravaswift.error" 279 | let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason] 280 | let code = response?.statusCode ?? 0 281 | let returnError = NSError(domain: errorDomain, code: code, userInfo: userInfo) 282 | 283 | return returnError 284 | } 285 | 286 | } 287 | 288 | extension StravaClient { 289 | 290 | fileprivate func isConfigured() -> (Bool) { 291 | return config != nil 292 | } 293 | 294 | fileprivate func checkConfiguration() { 295 | if !isConfigured() { 296 | fatalError("Strava client is not configured") 297 | } 298 | } 299 | 300 | fileprivate func oauthRequest(_ urlRequest: URLRequestConvertible) throws -> DataRequest? { 301 | checkConfiguration() 302 | 303 | return Alamofire.request(urlRequest) 304 | } 305 | 306 | fileprivate func oauthUpload(URLRequest: URLRequestConvertible, upload: UploadData, completion: @escaping (DataResponse) -> ()) { 307 | checkConfiguration() 308 | 309 | guard let url = try? URLRequest.asURLRequest() else { return } 310 | 311 | Alamofire.upload(multipartFormData: { multipartFormData in 312 | multipartFormData.append(upload.file, withName: "file", fileName: "\(upload.name ?? "default").\(upload.dataType)", mimeType: "octet/stream") 313 | for (key, value) in upload.params { 314 | if let value = value as? String { 315 | multipartFormData.append(value.data(using: .utf8)!, withName: key) 316 | } 317 | } 318 | }, usingThreshold: SessionManager.multipartFormDataEncodingMemoryThreshold, with: url) { encodingResult in 319 | switch encodingResult { 320 | case .success(let upload, _, _): 321 | upload.responseStrava { (response: DataResponse) in 322 | completion(response) 323 | } 324 | case .failure(let encodingError): 325 | print(encodingError) 326 | } 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /Sources/StravaSwift/StravaClientError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StravaClientError.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | StravaClientError Enum 13 | */ 14 | public enum StravaClientError: Error { 15 | 16 | /** 17 | The OAuthCredentials are invalid 18 | **/ 19 | case invalidCredentials 20 | 21 | /** 22 | Uknown error 23 | **/ 24 | case unknown 25 | } 26 | -------------------------------------------------------------------------------- /Sources/StravaSwift/StravaConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | /** 10 | OAuth scope 11 | */ 12 | public enum Scope: String { 13 | /** Default: Read public segments, public routes, public profile data, public posts, public events, club feeds, and leaderboards **/ 14 | case read = "read" 15 | /** Read private routes, private segments, and private events for the user **/ 16 | case readAll = "read_all" 17 | /** Read all profile information even if the user has set their profile visibility to Followers or Only You **/ 18 | case profileReadAll = "profile:read_all" 19 | /** Update the user's weight and Functional Threshold Power (FTP), and access to star or unstar segments on their behalf **/ 20 | case profileWrite = "profile:write" 21 | /** Read the user's activity data for activities that are visible to Everyone and Followers, excluding privacy zone data **/ 22 | case activityRead = "activity:read" 23 | /** The same access as activity:read, plus privacy zone data and access to read the user's activities with visibility set to Only You **/ 24 | case activityReadAll = "activity:read_all" 25 | /** Access to create manual activities and uploads, and access to edit any activities that are visible to the app, based on activity read access level **/ 26 | case activityWrite = "activity:write" 27 | } 28 | 29 | /** 30 | Strava configuration struct which should be passed to the StravaClient.sharedInstance.initWithConfig(_:) method 31 | **/ 32 | public struct StravaConfig { 33 | 34 | /** The application's Id **/ 35 | public let clientId: Int 36 | /** The application's Secrent **/ 37 | public let clientSecret: String 38 | /** The application's RedirectURL - this should be registered in the info.plist **/ 39 | public let redirectUri: String 40 | /** The requested permission scope **/ 41 | public let scopes: [Scope] 42 | /** The delegate responsible for storing and retrieving the OAuth token in your app **/ 43 | public let delegate: TokenDelegate 44 | 45 | public let forcePrompt: Bool 46 | 47 | /** 48 | Initializer 49 | 50 | - Parameters: 51 | - clientId: Int 52 | - clientSecret: Int 53 | - redirectUri: String 54 | - scope: Scope enum - default is .read) 55 | - delegate: TokenDelegateProtocol - default is the DefaultTokenDelegate 56 | **/ 57 | public init(clientId: Int, 58 | clientSecret: String, 59 | redirectUri: String, 60 | scopes: [Scope] = [.read], 61 | delegate: TokenDelegate? = nil, 62 | forcePrompt: Bool = true) { 63 | self.clientId = clientId 64 | self.clientSecret = clientSecret 65 | self.redirectUri = redirectUri 66 | self.scopes = scopes 67 | self.delegate = delegate ?? DefaultTokenDelegate() 68 | self.forcePrompt = forcePrompt 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/StravaSwift/StravaProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StravaProtocols.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Base protocol for Strava resources 14 | 15 | - Internal 16 | **/ 17 | public protocol Strava: CustomStringConvertible { 18 | init(_ json: JSON) 19 | } 20 | 21 | extension Strava { 22 | public var description: String { 23 | let mirror = Mirror(reflecting: self) 24 | var desc = "" 25 | for child in mirror.children { 26 | desc += "\(child.label!): \(child.value) \n" 27 | } 28 | return desc 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Stream.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stream.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Streams is the Strava term for the raw data associated with an activity. All streams for a given activity or segment effort will be the same length and the values at a given index correspond to the same time. 14 | **/ 15 | public struct Stream: Strava { 16 | public let type: StreamType? 17 | public let data: [Any]? 18 | public let seriesType: String? 19 | public let originalSize: Int? 20 | public let resolution: ResolutionType? 21 | 22 | /** 23 | Initializer 24 | 25 | - Parameter json: SwiftyJSON object 26 | - Internal 27 | **/ 28 | public init(_ json: JSON) { 29 | type = json["type"].strava(StreamType.self) 30 | data = json["data"].arrayObject 31 | seriesType = json["series_type"].string 32 | originalSize = json["original_size"].int 33 | resolution = json["resolution"].strava(ResolutionType.self) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/StravaSwift/StringExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringExtensions.swift 3 | // StravaSwift 4 | // 5 | // Created by MATTHEW CLARKSON on 23/05/2016. 6 | // Copyright © 2016 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | func toDate(_ format: String = "yyyy-MM-dd'T'HH:mm:ssZZZZZ") -> Date? { 13 | let formatter = DateFormatter() 14 | formatter.dateFormat = format 15 | return formatter.date(from: self) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/StravaSwift/SwiftyJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftJSON.swift 3 | // Pods 4 | // 5 | // Created by MATTHEW CLARKSON on 28/05/2016. 6 | // 7 | // 8 | 9 | import SwiftyJSON 10 | 11 | extension RawRepresentable { 12 | init?(o rawValue: RawValue?) { 13 | guard let rawValue = rawValue, let value = Self(rawValue: rawValue) else { return nil } 14 | self = value 15 | } 16 | } 17 | 18 | extension JSON { 19 | public func strava(_ type: T.Type?) -> T? { 20 | return type?.init(self) 21 | } 22 | 23 | public func strava(_ type: T.Type?) -> T? where T.RawValue == Int { 24 | return type?.init(optionalRawValue: self.int) 25 | } 26 | 27 | public func strava(_ type: T.Type?) -> T? where T.RawValue == String { 28 | return type?.init(optionalRawValue: self.string) 29 | } 30 | 31 | public func strava(_ type: T.Type?) -> [T]? { 32 | return self.arrayValue.compactMap { T($0) } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/StravaSwift/TokenDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenDelegate.swift 3 | // StravaSwift 4 | // 5 | // Created by MATTHEW CLARKSON on 23/05/2016. 6 | // Copyright © 2016 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | /** 10 | Token Delegate protocol - responsible for storing and retrieving the OAuth token 11 | **/ 12 | public protocol TokenDelegate { 13 | 14 | /** 15 | Retrieves the token 16 | 17 | - Returns: an optional OAuthToken 18 | **/ 19 | func get() -> OAuthToken? 20 | 21 | /** 22 | Store the token 23 | 24 | - Parameter token: an optional OAuthToken 25 | **/ 26 | func set(_ token: OAuthToken?) 27 | } 28 | -------------------------------------------------------------------------------- /Sources/StravaSwift/Upload.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Upload.swift 3 | // StravaSwift 4 | // 5 | // Created by Matthew on 11/11/2015. 6 | // Copyright © 2015 Matthew Clarkson. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Upload struct 14 | 15 | Uploading to Strava is an asynchronous process. A file is uploaded using a multipart/form-data POST request which performs initial checks on the data and enqueues the file for processing. The activity will not appear in other API requests until it has finished processing successfully. 16 | 17 | Processing status may be checked by polling Strava. A one-second or longer polling interval is recommended. The mean processing time is currently around 8 seconds. Once processing is complete, Strava will respond to polling requests with the activity’s ID. 18 | 19 | Errors can occur during the submission or processing steps and may be due to malformed activity data or duplicate data submission. 20 | 21 | - warning: Not yet tested 22 | 23 | **/ 24 | public struct UploadData { 25 | public var activityType: ActivityType? 26 | public var name: String? 27 | public var description: String? 28 | public var `private`: Bool? 29 | public var trainer: Bool? 30 | public var externalId: String? 31 | 32 | public var dataType: DataType 33 | public var file: Data 34 | 35 | public init(name: String, dataType: DataType, file: Data) { 36 | self.name = name 37 | self.dataType = dataType 38 | self.file = file 39 | } 40 | 41 | public init(activityType: ActivityType?, name: String?, description: String?, 42 | `private`: Bool?, trainer: Bool?, externalId: String?, dataType: DataType, file: Data) { 43 | self.activityType = activityType 44 | self.description = description 45 | self.`private` = `private` 46 | self.trainer = trainer 47 | self.externalId = externalId 48 | self.name = name 49 | self.dataType = dataType 50 | self.file = file 51 | } 52 | 53 | internal var params: [String: Any] { 54 | 55 | var params: [String: Any] = [:] 56 | params["data_type"] = dataType.rawValue 57 | params["name"] = name 58 | params["description"] = description 59 | if let `private` = `private` { 60 | params["private"] = (`private` as NSNumber).stringValue 61 | } 62 | if let trainer = trainer { 63 | params["trainer"] = (trainer as NSNumber).stringValue 64 | } 65 | params["external_id"] = externalId 66 | return params 67 | } 68 | } 69 | 70 | /** 71 | Upload status 72 | **/ 73 | public final class UploadStatus: Strava { 74 | public let id: Int? 75 | public let externalId: String? 76 | public let error: String? 77 | public let status: String? 78 | public let activityId: Int? 79 | 80 | public required init(_ json: JSON) { 81 | id = json["id"].int 82 | externalId = json["external_id"].string 83 | error = json["error"].string 84 | status = json["status"].string 85 | activityId = json["activity_id"].int 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /StravaSwift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'StravaSwift' 3 | s.version = '1.0.2' 4 | s.summary = 'A Swift library for the Strava API v3' 5 | s.description = <<-DESC 6 | A Swift library for the Strava API v3. For complete details visit the Strava developer site. 7 | DESC 8 | s.homepage = 'https://github.com/mpclarkson/StravaSwift' 9 | s.license = { :type => 'MIT', :file => 'LICENSE' } 10 | s.author = { 'Matthew Clarkson' => 'mpclarkson@gmail.com' } 11 | s.source = { :git => 'https://github.com/mpclarkson/StravaSwift.git', :tag => s.version.to_s } 12 | s.social_media_url = 'https://twitter.com/matt_pc' 13 | s.swift_version = '5.0' 14 | s.ios.deployment_target = '12.0' 15 | s.source_files = 'Sources/StravaSwift/**/*' 16 | s.dependency 'Alamofire', '~> 4' 17 | s.dependency 'SwiftyJSON', '~> 5' 18 | end 19 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import RouterTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += RouterTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/StravaSwiftTests/RouterTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import StravaSwift 3 | 4 | final class RouterTests: XCTestCase { 5 | 6 | let baseApiUrl = "https://www.strava.com/api/v3" 7 | 8 | func testAthleteRequest() { 9 | let urlRequest = try! Router.athlete.asURLRequest() 10 | XCTAssertEqual(urlRequest.url?.absoluteString, "\(baseApiUrl)/athlete") 11 | XCTAssertEqual(urlRequest.httpMethod, "GET") 12 | XCTAssertNil(urlRequest.httpBody) 13 | } 14 | 15 | static var allTests = [ 16 | ("testAthleteRequest", testAthleteRequest), 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /Tests/StravaSwiftTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(RouterTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------