From 664af77477be2ba9d248b488c1b7306a230dc798 Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Wed, 17 Jan 2018 21:41:18 -0800 Subject: [PATCH] Initial working commit --- .gitignore | 5 +- FitKit.xcodeproj/project.pbxproj | 429 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + FitKit.xcworkspace/contents.xcworkspacedata | 10 + FitKit/AppDelegate.swift | 54 +++ .../AppIcon.appiconset/Contents.json | 98 ++++ FitKit/Base.lproj/LaunchScreen.storyboard | 25 + FitKit/Base.lproj/Main.storyboard | 60 +++ FitKit/FitKit.entitlements | 8 + FitKit/FitbitClient.swift | 164 +++++++ FitKit/FitbitWeight.swift | 131 ++++++ FitKit/HealthKitHelper.swift | 199 ++++++++ FitKit/Info.plist | 60 +++ FitKit/ViewController.swift | 83 ++++ Podfile | 15 + Podfile.lock | 23 + 16 files changed, 1369 insertions(+), 2 deletions(-) create mode 100644 FitKit.xcodeproj/project.pbxproj create mode 100644 FitKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 FitKit.xcworkspace/contents.xcworkspacedata create mode 100644 FitKit/AppDelegate.swift create mode 100644 FitKit/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 FitKit/Base.lproj/LaunchScreen.storyboard create mode 100644 FitKit/Base.lproj/Main.storyboard create mode 100644 FitKit/FitKit.entitlements create mode 100644 FitKit/FitbitClient.swift create mode 100644 FitKit/FitbitWeight.swift create mode 100644 FitKit/HealthKitHelper.swift create mode 100644 FitKit/Info.plist create mode 100644 FitKit/ViewController.swift create mode 100644 Podfile create mode 100644 Podfile.lock diff --git a/.gitignore b/.gitignore index 55d53bf..5ea3874 100644 --- a/.gitignore +++ b/.gitignore @@ -57,9 +57,9 @@ xcuserdata # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: -# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control # -# Pods/ +Pods/ # Carthage # @@ -68,3 +68,4 @@ xcuserdata Carthage/Build +.DS_Store diff --git a/FitKit.xcodeproj/project.pbxproj b/FitKit.xcodeproj/project.pbxproj new file mode 100644 index 0000000..996f6be --- /dev/null +++ b/FitKit.xcodeproj/project.pbxproj @@ -0,0 +1,429 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 0C314390200143FE00E214BA /* FitbitClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31438F200143FE00E214BA /* FitbitClient.swift */; }; + 0CC8D443200C69EF00480D5D /* FitbitWeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC8D442200C69EF00480D5D /* FitbitWeight.swift */; }; + 0CC8D446200C6EB900480D5D /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0CC8D445200C6EB900480D5D /* HealthKit.framework */; }; + 0CC8D448200C73D000480D5D /* HealthKitHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC8D447200C73D000480D5D /* HealthKitHelper.swift */; }; + 0CC990EB20013C3D00624436 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC990EA20013C3D00624436 /* AppDelegate.swift */; }; + 0CC990ED20013C3D00624436 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC990EC20013C3D00624436 /* ViewController.swift */; }; + 0CC990F020013C3D00624436 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0CC990EE20013C3D00624436 /* Main.storyboard */; }; + 0CC990F220013C3D00624436 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CC990F120013C3D00624436 /* Assets.xcassets */; }; + 0CC990F520013C3D00624436 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0CC990F320013C3D00624436 /* LaunchScreen.storyboard */; }; + A06966DFF86C6A1D956E3384 /* Pods_FitKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F45705BC26BDA2DDAB016F60 /* Pods_FitKit.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0C31438F200143FE00E214BA /* FitbitClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FitbitClient.swift; sourceTree = ""; }; + 0CC8D442200C69EF00480D5D /* FitbitWeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FitbitWeight.swift; sourceTree = ""; }; + 0CC8D444200C6EB900480D5D /* FitKit.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FitKit.entitlements; sourceTree = ""; }; + 0CC8D445200C6EB900480D5D /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; }; + 0CC8D447200C73D000480D5D /* HealthKitHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitHelper.swift; sourceTree = ""; }; + 0CC990E720013C3D00624436 /* FitKit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FitKit.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0CC990EA20013C3D00624436 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 0CC990EC20013C3D00624436 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 0CC990EF20013C3D00624436 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 0CC990F120013C3D00624436 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 0CC990F420013C3D00624436 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 0CC990F620013C3D00624436 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1EC11871EC6B6830CA17FD35 /* Pods-FitKit.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FitKit.release.xcconfig"; path = "Pods/Target Support Files/Pods-FitKit/Pods-FitKit.release.xcconfig"; sourceTree = ""; }; + BA903BC284979D6B461A77EC /* Pods-FitKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FitKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FitKit/Pods-FitKit.debug.xcconfig"; sourceTree = ""; }; + F45705BC26BDA2DDAB016F60 /* Pods_FitKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FitKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0CC990E420013C3D00624436 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A06966DFF86C6A1D956E3384 /* Pods_FitKit.framework in Frameworks */, + 0CC8D446200C6EB900480D5D /* HealthKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0CC990DE20013C3D00624436 = { + isa = PBXGroup; + children = ( + 0CC990E920013C3D00624436 /* FitKit */, + 0CC990E820013C3D00624436 /* Products */, + B1383E672235FD59368B2A65 /* Pods */, + BE7D9CC7BCC66E086BEBA09B /* Frameworks */, + ); + sourceTree = ""; + }; + 0CC990E820013C3D00624436 /* Products */ = { + isa = PBXGroup; + children = ( + 0CC990E720013C3D00624436 /* FitKit.app */, + ); + name = Products; + sourceTree = ""; + }; + 0CC990E920013C3D00624436 /* FitKit */ = { + isa = PBXGroup; + children = ( + 0CC8D444200C6EB900480D5D /* FitKit.entitlements */, + 0C31438F200143FE00E214BA /* FitbitClient.swift */, + 0CC990EA20013C3D00624436 /* AppDelegate.swift */, + 0CC990EC20013C3D00624436 /* ViewController.swift */, + 0CC990EE20013C3D00624436 /* Main.storyboard */, + 0CC990F120013C3D00624436 /* Assets.xcassets */, + 0CC990F320013C3D00624436 /* LaunchScreen.storyboard */, + 0CC990F620013C3D00624436 /* Info.plist */, + 0CC8D442200C69EF00480D5D /* FitbitWeight.swift */, + 0CC8D447200C73D000480D5D /* HealthKitHelper.swift */, + ); + path = FitKit; + sourceTree = ""; + }; + B1383E672235FD59368B2A65 /* Pods */ = { + isa = PBXGroup; + children = ( + BA903BC284979D6B461A77EC /* Pods-FitKit.debug.xcconfig */, + 1EC11871EC6B6830CA17FD35 /* Pods-FitKit.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + BE7D9CC7BCC66E086BEBA09B /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0CC8D445200C6EB900480D5D /* HealthKit.framework */, + F45705BC26BDA2DDAB016F60 /* Pods_FitKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 0CC990E620013C3D00624436 /* FitKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0CC990F920013C3D00624436 /* Build configuration list for PBXNativeTarget "FitKit" */; + buildPhases = ( + E5B50210F26B509CB1E5FF42 /* [CP] Check Pods Manifest.lock */, + 0CC990E320013C3D00624436 /* Sources */, + 0CC990E420013C3D00624436 /* Frameworks */, + 0CC990E520013C3D00624436 /* Resources */, + 6D2599DAA5282C983E684673 /* [CP] Embed Pods Frameworks */, + 8BEED8791DD8D7C053FA9DA5 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FitKit; + productName = FitKit; + productReference = 0CC990E720013C3D00624436 /* FitKit.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0CC990DF20013C3D00624436 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0910; + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = iamthefij; + TargetAttributes = { + 0CC990E620013C3D00624436 = { + CreatedOnToolsVersion = 9.1; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.HealthKit = { + enabled = 1; + }; + }; + }; + }; + }; + buildConfigurationList = 0CC990E220013C3D00624436 /* Build configuration list for PBXProject "FitKit" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 0CC990DE20013C3D00624436; + productRefGroup = 0CC990E820013C3D00624436 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 0CC990E620013C3D00624436 /* FitKit */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0CC990E520013C3D00624436 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0CC990F520013C3D00624436 /* LaunchScreen.storyboard in Resources */, + 0CC990F220013C3D00624436 /* Assets.xcassets in Resources */, + 0CC990F020013C3D00624436 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6D2599DAA5282C983E684673 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FitKit/Pods-FitKit-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8BEED8791DD8D7C053FA9DA5 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FitKit/Pods-FitKit-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E5B50210F26B509CB1E5FF42 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../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"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0CC990E320013C3D00624436 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0CC8D448200C73D000480D5D /* HealthKitHelper.swift in Sources */, + 0CC990ED20013C3D00624436 /* ViewController.swift in Sources */, + 0CC990EB20013C3D00624436 /* AppDelegate.swift in Sources */, + 0C314390200143FE00E214BA /* FitbitClient.swift in Sources */, + 0CC8D443200C69EF00480D5D /* FitbitWeight.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 0CC990EE20013C3D00624436 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 0CC990EF20013C3D00624436 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 0CC990F320013C3D00624436 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 0CC990F420013C3D00624436 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 0CC990F720013C3D00624436 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 0CC990F820013C3D00624436 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 0CC990FA20013C3D00624436 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BA903BC284979D6B461A77EC /* Pods-FitKit.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = FitKit/FitKit.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = Y462XNRPU6; + INFOPLIST_FILE = FitKit/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.iamthefij.FitKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 0CC990FB20013C3D00624436 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1EC11871EC6B6830CA17FD35 /* Pods-FitKit.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = FitKit/FitKit.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = Y462XNRPU6; + INFOPLIST_FILE = FitKit/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.iamthefij.FitKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 0CC990E220013C3D00624436 /* Build configuration list for PBXProject "FitKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0CC990F720013C3D00624436 /* Debug */, + 0CC990F820013C3D00624436 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0CC990F920013C3D00624436 /* Build configuration list for PBXNativeTarget "FitKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0CC990FA20013C3D00624436 /* Debug */, + 0CC990FB20013C3D00624436 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0CC990DF20013C3D00624436 /* Project object */; +} diff --git a/FitKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/FitKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..ea2312e --- /dev/null +++ b/FitKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/FitKit.xcworkspace/contents.xcworkspacedata b/FitKit.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..dd1a208 --- /dev/null +++ b/FitKit.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/FitKit/AppDelegate.swift b/FitKit/AppDelegate.swift new file mode 100644 index 0000000..e1581a0 --- /dev/null +++ b/FitKit/AppDelegate.swift @@ -0,0 +1,54 @@ +// +// AppDelegate.swift +// FitKit +// +// Created by Ian Fijolek on 1/6/18. +// Copyright © 2018 iamthefij. All rights reserved. +// + +import UIKit +import OAuthSwift + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { + // Captures OAuth callback response + if (url.host == "oauth-callback") { + OAuthSwift.handle(url: url) + } + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/FitKit/Assets.xcassets/AppIcon.appiconset/Contents.json b/FitKit/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d8db8d6 --- /dev/null +++ b/FitKit/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FitKit/Base.lproj/LaunchScreen.storyboard b/FitKit/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f83f6fd --- /dev/null +++ b/FitKit/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FitKit/Base.lproj/Main.storyboard b/FitKit/Base.lproj/Main.storyboard new file mode 100644 index 0000000..4203523 --- /dev/null +++ b/FitKit/Base.lproj/Main.storyboard @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + diff --git a/FitKit/FitKit.entitlements b/FitKit/FitKit.entitlements new file mode 100644 index 0000000..e10f430 --- /dev/null +++ b/FitKit/FitKit.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.developer.healthkit + + + diff --git a/FitKit/FitbitClient.swift b/FitKit/FitbitClient.swift new file mode 100644 index 0000000..7d2f652 --- /dev/null +++ b/FitKit/FitbitClient.swift @@ -0,0 +1,164 @@ +// +// Client.swift +// FitKit +// +// Created by Ian Fijolek on 1/6/18. +// Copyright © 2018 iamthefij. All rights reserved. +// + +import Foundation +import Alamofire +import OAuthSwift +import Locksmith +import OAuthSwiftAlamofire + +let ACCOUNT_NAME = "FitBit" +let SERVICE_NAME = "FitKit" + +class FitbitClient { + private enum FitKitRequestError: Error { + case unableToParseResults + } + + private var oauthClient: OAuth2Swift + private let consumerKey = "22CPJ4" + private let consumerSecret = "eb05e27f6aa224bcc1cf273119565b28" + private let callbackUrl = "fitkit://oauth-callback/fitbit" + private let authorizeUrl = "https://www.fitbit.com/oauth2/authorize" + private let accessTokenUrl = "https://api.fitbit.com/oauth2/token" + + init() { + // Init inner client + self.oauthClient = OAuth2Swift( + consumerKey: self.consumerKey, + consumerSecret: self.consumerSecret, + authorizeUrl: self.authorizeUrl, + accessTokenUrl: self.accessTokenUrl, + responseType: "token" + ) + + // Set OAuth client to handle default sessions + let sessionManager = SessionManager.default + sessionManager.adapter = self.oauthClient.requestAdapter + sessionManager.retrier = self.oauthClient.requestAdapter + } + + func loadStoredTokens() { + NSLog("Someday we'll load stored credentials here! Today is not that day") + /* + let (oAuthData, _) = Locksmith.loadDataForUserAccount(userAccount: ACCOUNT_NAME, inService: SERVICE_NAME) + if let accessToken = oAuthData?.objectForKey("accessToken") { + // We have the token!! + self.oauthClient.client.credentials.oauthToken = accessToken + } else { + // No token! Boo + } + */ + } + + /// Check if the current client is already authorized + /// + /// - Returns: True if the current client can make requests + func isAuthorized() -> Bool { + if self.oauthClient.client.credential.oauthToken != "" { + if self.oauthClient.client.credential.isTokenExpired() { + NSLog("Credentials are expired") + return false + } + return true + } + + // TODO: This should probably return true sometimes + return false + } + + /// Displays authorization screen for user in a Safari web view + /// + /// - Parameters: + /// - viewController: the source controller to create the new Safari view + /// - success: Callback to be executed on success + /// - failure: Callback to be executed on failure + func authorize(viewController: UIViewController, success: @escaping () -> Void, failure: @escaping () -> Void) { + // Set webview handler + self.oauthClient.authorizeURLHandler = SafariURLHandler( + viewController: viewController, + oauthSwift: self.oauthClient + ) + // Authorize + let _ = self.oauthClient.authorize( + withCallbackURL: URL(string: self.callbackUrl)!, + scope: "weight activity", + state:"FITKIT", // TODO: make CSRF token + success: { + credential, response, parameters in + NSLog("Succesfully authenticated with credentials %s", credential.oauthToken) + success() + }, + failure: { + error in + // TODO: Maybe return the error + NSLog(error.localizedDescription) + failure() + } + ) + } + + /// Request all weight data from Fitbit API for the current user beteween + /// two dates. Maximum allowed distance between the two dates is 31 days. + /// + /// - Parameters: + /// - start: initial date to search from + /// - end: last date to include in search + /// - handler: function to handle the response + func getWeight(start: Date, end: Date, handler: @escaping (DataResponse) -> Void) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + + let urlString = "https://api.fitbit.com/1/user/-/body/log/weight/date/\(dateFormatter.string(from: start))/\(dateFormatter.string(from: end)).json" + + NSLog("Maing request to \(urlString)") + + + Alamofire.request(urlString) + .validate(contentType: ["application/json"]) + .responseJSON(completionHandler: handler) + } + + /// Handler that parses responses from Fitbit Weight API and provides + /// instances of FitbitWeight to a new callback. + /// + /// - Parameters: + /// - response: Original DataResponse from the Fitbit request + /// - callback: Callback that will accept a list of FitbitWeight instances + /// and an Error if, if present. + class func weightResponseHandler(response: DataResponse, callback: @escaping ([FitbitWeight]?, Error?) -> Void) { + NSLog("Handling weight response \(response.debugDescription)") + + guard response.result.isSuccess else { + NSLog("Error while fetching weight: \(response.result.error!)") + callback(nil, response.result.error) + return + } + + NSLog("Got weight! \(response.result.value!)") + + guard let results = response.result.value as? [String: Any] else { + print("Return result not in form [String: Any]") + callback(nil, FitKitRequestError.unableToParseResults) + return + } + + guard let weights = results["weight"] as? [[String: Any]] else { + print("Return result does not contain a list of weights") + callback(nil, FitKitRequestError.unableToParseResults) + return + } + + var fbWeights = [FitbitWeight]() + for weight in weights { + fbWeights.append(FitbitWeight(withResult: weight)) + } + + callback(fbWeights, nil) + } +} diff --git a/FitKit/FitbitWeight.swift b/FitKit/FitbitWeight.swift new file mode 100644 index 0000000..734a7cc --- /dev/null +++ b/FitKit/FitbitWeight.swift @@ -0,0 +1,131 @@ +// +// FitbitWeight.swift +// FitKit +// +// Created by Ian Fijolek on 1/14/18. +// Copyright © 2018 iamthefij. All rights reserved. +// + +import Foundation +import HealthKit + +/// FitSample is a generic protocol that represents a datum from the Fitbit API +protocol FitSample { + /// The integer ID from Fitbit for the given sample + var logId: Int { get } + /// The Fitbit source device + var source: String { get } + /// The date that this sample was taken + var date: Date { get } + + /// Creates a HealthKit Sample from the Fitbit object + /// + /// - Returns: New HKSample instance that can be added to HealthKit + func makeSample() -> HKSample + + /// Generates a metadata dictionary to be stored in HealthKit + /// + /// - Returns: Map of custom metadata keys to their values + func metadata() -> [String: Any] + + /// Returns the HKSampleType for the FitSample for querying or creating of + /// new instances. + /// + /// - Returns: <#return value description#> + func getSampleType() -> HKSampleType + + /// Returns a predicate that can be used to retrieve this instance from + /// HealthKit, if it has been added. + /// + /// - Returns: A new NSPredicate that can filter results of an HKSampleQuery + func getPredicate() -> NSPredicate +} + +// MARK: - Extension FitSample to return a metdata dictionary +extension FitSample { + /// Returns default set of metadata for a FitSample + /// + /// - Returns: dictionary including the Fitbit source and Id + func metadata() -> [String: Any] { + return [ + "Fitbit Source": self.source, + "Fitbit Id": self.logId + ] + } + + /// Returns predicate filtering results based on the "Fitbit Id" metadata + /// + /// - Returns: A new NSPredicate that should return an equivalent sample + /// from HealthKit + func getPredicate() -> NSPredicate { + return HKQuery.predicateForObjects( + withMetadataKey: "Fitbit Id", + operatorType: .equalTo, + value: self.logId + ) + } +} + +/// FitbitWeight is a concrete subclass of FitSample. It represents a weigh +/// measurement retrieved from the Fitbit +class FitbitWeight: FitSample { + var weight: Double + var bmi: Double + var logId: Int + var source: String + var date: Date + + /// Convenience initializer to create a FitbitWeight based on a response + /// from the api + /// + /// - Parameter result: [String: Any?] dictionary response from the API + convenience init(withResult result: [String: Any?]) { + // TODO: Probably unsafe, but Swift... + NSLog("Trying to initialize \(result)") + let dateStringFormatter = DateFormatter() + dateStringFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + let dateString = result["date"] as! String + let timeString = result["time"] as! String + let weighDate = dateStringFormatter.date(from: "\(dateString) \(timeString)")! + self.init( + withWeight: result["weight"] as! Double, + withBMI: result["bmi"] as! Double, + withDate: weighDate, + withLogId: result["logId"] as! Int, + withSource: result["source"] as! String + ) + } + + /// Initializes a FitbitWeight with specified properties + /// + /// - Parameters: + /// - weight: Double weight value in kg + /// - bmi: Double bmi value (Might go away) + /// - date: Date that the measurement was taken + /// - logId: Int ID from Fitbit + /// - source: String source name from Fitbit + init(withWeight weight: Double, withBMI bmi: Double, withDate date: Date, withLogId logId: Int, withSource source: String) { + self.weight = weight + self.bmi = bmi + self.date = date + self.logId = logId + self.source = source + } + + func getSampleType() -> HKSampleType { + // Forcing as we should be handling permissions + return HKObjectType.quantityType(forIdentifier: .bodyMass)! + } + + func makeSample() -> HKSample { + let weight = HKQuantity(unit: HKUnit.gramUnit(with: .kilo), doubleValue: self.weight) + return HKQuantitySample( + // Forcing downcast since each concrete class implementation should provide compatible types + type: self.getSampleType() as! HKQuantityType, + quantity: weight, + start: self.date, + end: self.date, + metadata: self.metadata() + ) + } +} diff --git a/FitKit/HealthKitHelper.swift b/FitKit/HealthKitHelper.swift new file mode 100644 index 0000000..1e18fd7 --- /dev/null +++ b/FitKit/HealthKitHelper.swift @@ -0,0 +1,199 @@ +// +// HealthKitHelper.swift +// FitKit +// +// Created by Ian Fijolek on 1/14/18. +// Copyright © 2018 iamthefij. All rights reserved. +// + +import Foundation +import HealthKit + +/// Helper class with class functions to simplify interraction with HealthKit +class HealthKitHelper { + private enum HealthKitSetupError: Error { + case hkUnavailable + case dataTypeUnavailable + } + private enum FitKitSaveError: Error { + case sampleAlreadyExists + } + + /// Attempts to authorize the application with HealthKit + /// If HealthKit is unavailable or access is not provided to the requested + /// types, an error will be passed into the callback. + /// + /// - Parameter callback: Callback function for handling missing or unprovided + /// access. + class func authorizeHealthKit(_ callback: @escaping (Bool, Error?) -> Void) { + // Verify Health data is available + guard HKHealthStore.isHealthDataAvailable() else { + callback(false, HealthKitSetupError.hkUnavailable) + return + } + + // This is not very well abstracted yet + guard let bodyMass = HKObjectType.quantityType(forIdentifier: .bodyMass) else { + callback(false, HealthKitSetupError.dataTypeUnavailable) + return + } + + // Request access to the specified metrics + HKHealthStore().requestAuthorization( + toShare: [bodyMass], + read: [bodyMass], + completion: callback + ) + } + + /// Simplifies querying for HealthKit samples given types and predicates. + /// This is executed async and will dispatch the callback on the main thread + /// + /// - Parameters: + /// - sampleType: The HKSampleType that you wish to query + /// - predicate: An NSPredicate to use in the query + /// - limit: Return a limited number of samples + /// - callback: Callback to be executed on the main thread + class func querySamplesWithPredicate( + for sampleType: HKSampleType, + withPredicate predicate: NSPredicate, + limit: Int, + callback: @escaping ([HKQuantitySample]?, Error?) -> Void) + { + let sortDescriptor = NSSortDescriptor( + key: HKSampleSortIdentifierStartDate, + ascending: false + ) + + let sampleQuery = HKSampleQuery( + sampleType: sampleType, + predicate: predicate, + limit: limit, + sortDescriptors: [sortDescriptor] + ) { + (query, samples, error) in + + DispatchQueue.main.async { + guard let samples = samples as? [HKQuantitySample] else { + callback(nil, error) + return + } + callback(samples, error) + return + } + } + + HKHealthStore().execute(sampleQuery) + } + + /// Provides easy query for the most recent sample of a given type + /// + /// - Parameters: + /// - sampleType: The HKSampleType that you wish to query + /// - callback: Callback to be executed on the main thread + class func queryMostRecentSample(for sampleType: HKSampleType, callback: @escaping (HKQuantitySample?, Error?) -> Void) { + let mostRecentPredicate = HKQuery.predicateForSamples( + withStart: Date.distantPast, + end: Date(), + options: .strictStartDate + ) + self.querySamplesWithPredicate(for: sampleType, withPredicate: mostRecentPredicate, limit: 1) { + samples, error in + guard let sample = samples?.first else { + callback(nil, error) + return + } + callback(sample, nil) + return + } + } + + /// Simple query for a HealthKit sample of a given type and Fitbit Id + /// + /// - Parameters: + /// - sampleType: The HKSampleType that you wish to query + /// - logId: The Fitbit logId to be queried for (FitSample.logId) + /// - callback: Callback to be executed on the main thread + class func getSampleWithFitbitId(for sampleType: HKSampleType, withId logId: Int, callback: @escaping (HKQuantitySample?, Error?) -> Void) { + let logIdEquals = HKQuery.predicateForObjects(withMetadataKey: "Fitbit Id", operatorType: .equalTo, value: logId) + self.querySamplesWithPredicate(for: sampleType, withPredicate: logIdEquals, limit: 1) { + samples, error in + guard let sample = samples?.first else { + callback(nil, error) + return + } + callback(sample, nil) + return + } + } + + /// Simple query for a HealthKit for a given FitSample + /// + /// - Parameters: + /// - fitSample: The FitSample that you wish to query for in HealthKit + /// - callback: Callback to be executed on the main thread + class func querySample(for fitSample: FitSample, callback: @escaping (HKQuantitySample?, Error?) -> Void) { + self.querySamplesWithPredicate( + for: fitSample.getSampleType(), + withPredicate: fitSample.getPredicate(), + limit: 1 + ) { + samples, error in + guard let sample = samples?.first else { + callback(nil, error) + return + } + callback(sample, nil) + return + } + } + + /// Save a FitSample to HealthKit + /// + /// - Parameters: + /// - sample: FitSample instance to save + /// - callback: Callback function that will accept the completion from + /// HealthKit + class func saveFitSample(_ sample: FitSample, callback: @escaping (Bool, Error?) -> Void) { + HKHealthStore().save(sample.makeSample(), withCompletion: callback) + } + + /// Idempotent save of a FitSample to HealthKit. Before saving, executes an + /// async query for the sample in HealthKit. If found, it will not save. + /// + /// - Parameters: + /// - sample: FitSample instance to save + /// - callback: Callback funciton that accepts parameters indicating a + /// success or failure and any error that is returned. In the case of an + /// existing sample, it will be designated a success, but there it will + /// also return a FitKitSaveError.sampleAlreadyExists error. + class func maybeSaveSample(_ sample: FitSample, callback: @escaping (Bool, Error?) -> Void) { + self.querySample(for: sample) { + (existingSample, error) in + if let error = error { + NSLog("Error querying for sample") + callback(false, error) + return + } + if existingSample == nil { + // Save only if no existing sample was found + self.saveFitSample(sample) { + (success, error) in + if let error = error { + NSLog("Error Saving Sample: \(error.localizedDescription)") + callback(false, error) + return + } else { + NSLog("Successfully saved Sample") + callback(true, nil) + return + } + } + } else { + NSLog("Sample already found in HealthKit. Will not save") + callback(true, FitKitSaveError.sampleAlreadyExists) + return + } + } + } +} diff --git a/FitKit/Info.plist b/FitKit/Info.plist new file mode 100644 index 0000000..1347f49 --- /dev/null +++ b/FitKit/Info.plist @@ -0,0 +1,60 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + fitkit + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSHealthShareUsageDescription + Will use HealthKit to write Fitbit data so you can manage your data locally + NSHealthUpdateUsageDescription + Will use HealthKit to write Fitbit data so you can manage your data locally + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/FitKit/ViewController.swift b/FitKit/ViewController.swift new file mode 100644 index 0000000..3909fa4 --- /dev/null +++ b/FitKit/ViewController.swift @@ -0,0 +1,83 @@ +// +// ViewController.swift +// FitKit +// +// Created by Ian Fijolek on 1/6/18. +// Copyright © 2018 iamthefij. All rights reserved. +// + +import Alamofire +import HealthKit +import UIKit + +class ViewController: UIViewController { + + @IBOutlet var loggedIn: UILabel! + @IBOutlet var outputText: UITextView! + let client = FitbitClient() + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view, typically from a nib. + updateAuthorizedLabel() + } + + override func viewDidAppear(_ animated: Bool) { + authorize() + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + fileprivate func updateAuthorizedLabel() { + if self.client.isAuthorized() { + self.loggedIn.text = "Logged In" + } else { + self.loggedIn.text = "Not Logged In" + } + } + + fileprivate func authorize() { + HealthKitHelper.authorizeHealthKit() { + result, error in + // TODO: Do something with the result and error + if !self.client.isAuthorized() { + self.client.authorize(viewController: self, success: self.authorized, failure: self.unauthroized) + } + } + } + + fileprivate func authorized() { + NSLog("We have Fitbit auth!!!") + updateAuthorizedLabel() + self.displayWeight() + } + + fileprivate func unauthroized() { + NSLog("No auth. Booo!!!") + } + + func displayWeight() { + let today = Date() + let lastMonth = Calendar.current.date(byAdding: .day, value: -31, to: today)! + self.client.getWeight(start: lastMonth, end: today) { + response in FitbitClient.weightResponseHandler(response: response) { + fbWeights, error in + + if let error = error { + NSLog("Unable to get weights: \(error)") + return + } + + if let fbWeights = fbWeights { + for fbWeight in fbWeights { + NSLog("Got Weight: \(fbWeight.logId): \(fbWeight.date) \(fbWeight.weight)") + HealthKitHelper.maybeSaveSample(fbWeight, callback: {_,_ in}) + } + } + } + } + } +} diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..602a8b3 --- /dev/null +++ b/Podfile @@ -0,0 +1,15 @@ +project 'FitKit.xcodeproj' + +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'FitKit' do + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + use_frameworks! + + pod "Alamofire" + pod "Locksmith" + pod 'OAuthSwift', '~> 1.2.0' + pod 'OAuthSwiftAlamofire' + +end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000..c9d5087 --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,23 @@ +PODS: + - Alamofire (4.5.1) + - Locksmith (4.0.0) + - OAuthSwift (1.2.0) + - OAuthSwiftAlamofire (0.1.0): + - Alamofire + - OAuthSwift + +DEPENDENCIES: + - Alamofire + - Locksmith + - OAuthSwift (~> 1.2.0) + - OAuthSwiftAlamofire + +SPEC CHECKSUMS: + Alamofire: 2d95912bf4c34f164fdfc335872e8c312acaea4a + Locksmith: e9bebbaaa4cee3c511bc358a44f843c3fe00e21f + OAuthSwift: 7fd6855b8e4d58eb5a30d156ea9bed7a8aecd1ca + OAuthSwiftAlamofire: 20bb998ac506f82c3bab0625fbc98a9c6fca783f + +PODFILE CHECKSUM: 114ede9ace3efbf5222b8790a77f72e973a10d2b + +COCOAPODS: 1.2.0