From 6d2580da1afaa22d7e00d901421b342ee866b604 Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Sat, 3 Nov 2018 02:58:32 +0700 Subject: [PATCH 01/23] Initial Commit --- Weather.xcodeproj/project.pbxproj | 470 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcschemes/xcschememanagement.plist | 14 + Weather/AppDelegate.swift | 46 ++ .../AppIcon.appiconset/Contents.json | 98 ++++ Weather/Assets.xcassets/Contents.json | 6 + Weather/Base.lproj/LaunchScreen.storyboard | 25 + Weather/Base.lproj/Main.storyboard | 24 + Weather/Info.plist | 45 ++ Weather/ViewController.swift | 20 + WeatherTests/Info.plist | 22 + WeatherTests/WeatherTests.swift | 34 ++ 13 files changed, 819 insertions(+) create mode 100644 Weather.xcodeproj/project.pbxproj create mode 100644 Weather.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Weather.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 Weather/AppDelegate.swift create mode 100644 Weather/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Weather/Assets.xcassets/Contents.json create mode 100644 Weather/Base.lproj/LaunchScreen.storyboard create mode 100644 Weather/Base.lproj/Main.storyboard create mode 100644 Weather/Info.plist create mode 100644 Weather/ViewController.swift create mode 100644 WeatherTests/Info.plist create mode 100644 WeatherTests/WeatherTests.swift diff --git a/Weather.xcodeproj/project.pbxproj b/Weather.xcodeproj/project.pbxproj new file mode 100644 index 00000000..db6412a6 --- /dev/null +++ b/Weather.xcodeproj/project.pbxproj @@ -0,0 +1,470 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + AA3006A3218CE3D400A23A36 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006A2218CE3D400A23A36 /* AppDelegate.swift */; }; + AA3006A5218CE3D400A23A36 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006A4218CE3D400A23A36 /* ViewController.swift */; }; + AA3006A8218CE3D400A23A36 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA3006A6218CE3D400A23A36 /* Main.storyboard */; }; + AA3006AA218CE3DD00A23A36 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA3006A9218CE3DD00A23A36 /* Assets.xcassets */; }; + AA3006AD218CE3DD00A23A36 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA3006AB218CE3DD00A23A36 /* LaunchScreen.storyboard */; }; + AA3006B8218CE3DD00A23A36 /* WeatherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + AA3006B4218CE3DD00A23A36 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AA300697218CE3D400A23A36 /* Project object */; + proxyType = 1; + remoteGlobalIDString = AA30069E218CE3D400A23A36; + remoteInfo = Weather; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + AA30069F218CE3D400A23A36 /* Weather.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Weather.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AA3006A2218CE3D400A23A36 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + AA3006A4218CE3D400A23A36 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + AA3006A7218CE3D400A23A36 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + AA3006A9218CE3DD00A23A36 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AA3006AC218CE3DD00A23A36 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + AA3006AE218CE3DD00A23A36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AA3006B3218CE3DD00A23A36 /* WeatherTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WeatherTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherTests.swift; sourceTree = ""; }; + AA3006B9218CE3DD00A23A36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AA30069C218CE3D400A23A36 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AA3006B0218CE3DD00A23A36 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + AA300696218CE3D400A23A36 = { + isa = PBXGroup; + children = ( + AA3006A1218CE3D400A23A36 /* Weather */, + AA3006B6218CE3DD00A23A36 /* WeatherTests */, + AA3006A0218CE3D400A23A36 /* Products */, + ); + sourceTree = ""; + }; + AA3006A0218CE3D400A23A36 /* Products */ = { + isa = PBXGroup; + children = ( + AA30069F218CE3D400A23A36 /* Weather.app */, + AA3006B3218CE3DD00A23A36 /* WeatherTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + AA3006A1218CE3D400A23A36 /* Weather */ = { + isa = PBXGroup; + children = ( + AA3006A2218CE3D400A23A36 /* AppDelegate.swift */, + AA3006A4218CE3D400A23A36 /* ViewController.swift */, + AA3006A6218CE3D400A23A36 /* Main.storyboard */, + AA3006A9218CE3DD00A23A36 /* Assets.xcassets */, + AA3006AB218CE3DD00A23A36 /* LaunchScreen.storyboard */, + AA3006AE218CE3DD00A23A36 /* Info.plist */, + ); + path = Weather; + sourceTree = ""; + }; + AA3006B6218CE3DD00A23A36 /* WeatherTests */ = { + isa = PBXGroup; + children = ( + AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */, + AA3006B9218CE3DD00A23A36 /* Info.plist */, + ); + path = WeatherTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AA30069E218CE3D400A23A36 /* Weather */ = { + isa = PBXNativeTarget; + buildConfigurationList = AA3006BC218CE3DD00A23A36 /* Build configuration list for PBXNativeTarget "Weather" */; + buildPhases = ( + AA30069B218CE3D400A23A36 /* Sources */, + AA30069C218CE3D400A23A36 /* Frameworks */, + AA30069D218CE3D400A23A36 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Weather; + productName = Weather; + productReference = AA30069F218CE3D400A23A36 /* Weather.app */; + productType = "com.apple.product-type.application"; + }; + AA3006B2218CE3DD00A23A36 /* WeatherTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = AA3006BF218CE3DD00A23A36 /* Build configuration list for PBXNativeTarget "WeatherTests" */; + buildPhases = ( + AA3006AF218CE3DD00A23A36 /* Sources */, + AA3006B0218CE3DD00A23A36 /* Frameworks */, + AA3006B1218CE3DD00A23A36 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + AA3006B5218CE3DD00A23A36 /* PBXTargetDependency */, + ); + name = WeatherTests; + productName = WeatherTests; + productReference = AA3006B3218CE3DD00A23A36 /* WeatherTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AA300697218CE3D400A23A36 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1000; + LastUpgradeCheck = 1000; + ORGANIZATIONNAME = "Christian Huang"; + TargetAttributes = { + AA30069E218CE3D400A23A36 = { + CreatedOnToolsVersion = 10.0; + }; + AA3006B2218CE3DD00A23A36 = { + CreatedOnToolsVersion = 10.0; + TestTargetID = AA30069E218CE3D400A23A36; + }; + }; + }; + buildConfigurationList = AA30069A218CE3D400A23A36 /* Build configuration list for PBXProject "Weather" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AA300696218CE3D400A23A36; + productRefGroup = AA3006A0218CE3D400A23A36 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AA30069E218CE3D400A23A36 /* Weather */, + AA3006B2218CE3DD00A23A36 /* WeatherTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AA30069D218CE3D400A23A36 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AA3006AD218CE3DD00A23A36 /* LaunchScreen.storyboard in Resources */, + AA3006AA218CE3DD00A23A36 /* Assets.xcassets in Resources */, + AA3006A8218CE3D400A23A36 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AA3006B1218CE3DD00A23A36 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AA30069B218CE3D400A23A36 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AA3006A5218CE3D400A23A36 /* ViewController.swift in Sources */, + AA3006A3218CE3D400A23A36 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AA3006AF218CE3DD00A23A36 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AA3006B8218CE3DD00A23A36 /* WeatherTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + AA3006B5218CE3DD00A23A36 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AA30069E218CE3D400A23A36 /* Weather */; + targetProxy = AA3006B4218CE3DD00A23A36 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + AA3006A6218CE3D400A23A36 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AA3006A7218CE3D400A23A36 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + AA3006AB218CE3DD00A23A36 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AA3006AC218CE3DD00A23A36 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + AA3006BA218CE3DD00A23A36 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + AA3006BB218CE3DD00A23A36 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AA3006BD218CE3DD00A23A36 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7X5UG5NGMS; + INFOPLIST_FILE = Weather/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.inovidea.Weather; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AA3006BE218CE3DD00A23A36 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7X5UG5NGMS; + INFOPLIST_FILE = Weather/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.inovidea.Weather; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + AA3006C0218CE3DD00A23A36 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7X5UG5NGMS; + INFOPLIST_FILE = WeatherTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.inovidea.WeatherTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Weather.app/Weather"; + }; + name = Debug; + }; + AA3006C1218CE3DD00A23A36 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7X5UG5NGMS; + INFOPLIST_FILE = WeatherTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.inovidea.WeatherTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Weather.app/Weather"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AA30069A218CE3D400A23A36 /* Build configuration list for PBXProject "Weather" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA3006BA218CE3DD00A23A36 /* Debug */, + AA3006BB218CE3DD00A23A36 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AA3006BC218CE3DD00A23A36 /* Build configuration list for PBXNativeTarget "Weather" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA3006BD218CE3DD00A23A36 /* Debug */, + AA3006BE218CE3DD00A23A36 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AA3006BF218CE3DD00A23A36 /* Build configuration list for PBXNativeTarget "WeatherTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA3006C0218CE3DD00A23A36 /* Debug */, + AA3006C1218CE3DD00A23A36 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AA300697218CE3D400A23A36 /* Project object */; +} diff --git a/Weather.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Weather.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..561f654e --- /dev/null +++ b/Weather.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Weather.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Weather.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Weather.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist b/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..f22a76a3 --- /dev/null +++ b/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + Weather.xcscheme + + orderHint + 0 + + + + diff --git a/Weather/AppDelegate.swift b/Weather/AppDelegate.swift new file mode 100644 index 00000000..21c65efc --- /dev/null +++ b/Weather/AppDelegate.swift @@ -0,0 +1,46 @@ +// +// AppDelegate.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/Weather/Assets.xcassets/AppIcon.appiconset/Contents.json b/Weather/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d8db8d65 --- /dev/null +++ b/Weather/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/Weather/Assets.xcassets/Contents.json b/Weather/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Weather/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Weather/Base.lproj/LaunchScreen.storyboard b/Weather/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..bfa36129 --- /dev/null +++ b/Weather/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Weather/Base.lproj/Main.storyboard b/Weather/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f1bcf384 --- /dev/null +++ b/Weather/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Weather/Info.plist b/Weather/Info.plist new file mode 100644 index 00000000..16be3b68 --- /dev/null +++ b/Weather/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Weather/ViewController.swift b/Weather/ViewController.swift new file mode 100644 index 00000000..66d41180 --- /dev/null +++ b/Weather/ViewController.swift @@ -0,0 +1,20 @@ +// +// ViewController.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view, typically from a nib. + } + + +} + diff --git a/WeatherTests/Info.plist b/WeatherTests/Info.plist new file mode 100644 index 00000000..6c40a6cd --- /dev/null +++ b/WeatherTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/WeatherTests/WeatherTests.swift b/WeatherTests/WeatherTests.swift new file mode 100644 index 00000000..bb35d1f2 --- /dev/null +++ b/WeatherTests/WeatherTests.swift @@ -0,0 +1,34 @@ +// +// WeatherTests.swift +// WeatherTests +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import XCTest +@testable import Weather + +class WeatherTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} From 182bc716a9a431409eb3bbb7f3ae8d7d85dfc26d Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Sat, 3 Nov 2018 16:15:43 +0700 Subject: [PATCH 02/23] Add folders for MVVM pattern and storyboard UI design for WeatherViewController --- Weather.xcodeproj/project.pbxproj | 48 +++- Weather/Base.lproj/LaunchScreen.storyboard | 11 +- Weather/Base.lproj/Main.storyboard | 209 +++++++++++++++++- Weather/Info.plist | 2 - Weather/ViewController.swift | 20 -- Weather/ViewModel/WeatherViewController.swift | 30 +++ 6 files changed, 285 insertions(+), 35 deletions(-) delete mode 100644 Weather/ViewController.swift create mode 100644 Weather/ViewModel/WeatherViewController.swift diff --git a/Weather.xcodeproj/project.pbxproj b/Weather.xcodeproj/project.pbxproj index db6412a6..31fe49d9 100644 --- a/Weather.xcodeproj/project.pbxproj +++ b/Weather.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ AA3006A3218CE3D400A23A36 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006A2218CE3D400A23A36 /* AppDelegate.swift */; }; - AA3006A5218CE3D400A23A36 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006A4218CE3D400A23A36 /* ViewController.swift */; }; + AA3006A5218CE3D400A23A36 /* WeatherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006A4218CE3D400A23A36 /* WeatherViewController.swift */; }; AA3006A8218CE3D400A23A36 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA3006A6218CE3D400A23A36 /* Main.storyboard */; }; AA3006AA218CE3DD00A23A36 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA3006A9218CE3DD00A23A36 /* Assets.xcassets */; }; AA3006AD218CE3DD00A23A36 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA3006AB218CE3DD00A23A36 /* LaunchScreen.storyboard */; }; @@ -28,7 +28,7 @@ /* Begin PBXFileReference section */ AA30069F218CE3D400A23A36 /* Weather.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Weather.app; sourceTree = BUILT_PRODUCTS_DIR; }; AA3006A2218CE3D400A23A36 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - AA3006A4218CE3D400A23A36 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + AA3006A4218CE3D400A23A36 /* WeatherViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewController.swift; sourceTree = ""; }; AA3006A7218CE3D400A23A36 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; AA3006A9218CE3DD00A23A36 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; AA3006AC218CE3DD00A23A36 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -77,12 +77,16 @@ AA3006A1218CE3D400A23A36 /* Weather */ = { isa = PBXGroup; children = ( + AA3006C3218CF08D00A23A36 /* Service */, + AA3006C2218CF07F00A23A36 /* Utils */, AA3006A2218CE3D400A23A36 /* AppDelegate.swift */, - AA3006A4218CE3D400A23A36 /* ViewController.swift */, AA3006A6218CE3D400A23A36 /* Main.storyboard */, AA3006A9218CE3DD00A23A36 /* Assets.xcassets */, AA3006AB218CE3DD00A23A36 /* LaunchScreen.storyboard */, AA3006AE218CE3DD00A23A36 /* Info.plist */, + AA3006C4218CF09B00A23A36 /* Model */, + AA3006C5218CF0A300A23A36 /* View */, + AA3006C6218CF0BC00A23A36 /* ViewModel */, ); path = Weather; sourceTree = ""; @@ -96,6 +100,42 @@ path = WeatherTests; sourceTree = ""; }; + AA3006C2218CF07F00A23A36 /* Utils */ = { + isa = PBXGroup; + children = ( + ); + path = Utils; + sourceTree = ""; + }; + AA3006C3218CF08D00A23A36 /* Service */ = { + isa = PBXGroup; + children = ( + ); + path = Service; + sourceTree = ""; + }; + AA3006C4218CF09B00A23A36 /* Model */ = { + isa = PBXGroup; + children = ( + ); + path = Model; + sourceTree = ""; + }; + AA3006C5218CF0A300A23A36 /* View */ = { + isa = PBXGroup; + children = ( + ); + path = View; + sourceTree = ""; + }; + AA3006C6218CF0BC00A23A36 /* ViewModel */ = { + isa = PBXGroup; + children = ( + AA3006A4218CE3D400A23A36 /* WeatherViewController.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -197,7 +237,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - AA3006A5218CE3D400A23A36 /* ViewController.swift in Sources */, + AA3006A5218CE3D400A23A36 /* WeatherViewController.swift in Sources */, AA3006A3218CE3D400A23A36 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Weather/Base.lproj/LaunchScreen.storyboard b/Weather/Base.lproj/LaunchScreen.storyboard index bfa36129..3eb85391 100644 --- a/Weather/Base.lproj/LaunchScreen.storyboard +++ b/Weather/Base.lproj/LaunchScreen.storyboard @@ -1,7 +1,10 @@ - - + + + + + - + @@ -13,7 +16,7 @@ - + diff --git a/Weather/Base.lproj/Main.storyboard b/Weather/Base.lproj/Main.storyboard index f1bcf384..7344547e 100644 --- a/Weather/Base.lproj/Main.storyboard +++ b/Weather/Base.lproj/Main.storyboard @@ -1,21 +1,220 @@ - + + + + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Weather/Info.plist b/Weather/Info.plist index 16be3b68..89d7858b 100644 --- a/Weather/Info.plist +++ b/Weather/Info.plist @@ -31,8 +31,6 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad diff --git a/Weather/ViewController.swift b/Weather/ViewController.swift deleted file mode 100644 index 66d41180..00000000 --- a/Weather/ViewController.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ViewController.swift -// Weather -// -// Created by Christian Huang on 03/11/18. -// Copyright © 2018 Christian Huang. All rights reserved. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. - } - - -} - diff --git a/Weather/ViewModel/WeatherViewController.swift b/Weather/ViewModel/WeatherViewController.swift new file mode 100644 index 00000000..bf35fd22 --- /dev/null +++ b/Weather/ViewModel/WeatherViewController.swift @@ -0,0 +1,30 @@ +// +// WeatherViewController.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class WeatherViewController: UIViewController { + @IBOutlet weak var headerView: UIView! + @IBOutlet weak var areaLabel: UILabel! + @IBOutlet weak var weatherLabel: UILabel! + @IBOutlet weak var temperatureLabel: UILabel! + @IBOutlet weak var todayView: UIView! + @IBOutlet weak var todayForecastCollectionView: UICollectionView! + @IBOutlet weak var detailScrollView: UIScrollView! + @IBOutlet weak var dailyForecastStackView: UIStackView! + @IBOutlet weak var forecastDescriptionLabel: UILabel! + @IBOutlet weak var infoStackView: UIStackView! + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view, typically from a nib. + } + + +} + From 2687859c375461969fa88f08110efc5d89757f6b Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Sat, 3 Nov 2018 18:05:43 +0700 Subject: [PATCH 03/23] Add UI design detail for WeatherViewController --- Weather.xcodeproj/project.pbxproj | 42 +++++++++- Weather/Base.lproj/Main.storyboard | 46 +++++++++-- Weather/View/Cell/HourlyForecastCell.swift | 15 ++++ Weather/View/Custom/DailyForecastView.swift | 26 +++++++ Weather/View/Custom/DailyForecastView.xib | 63 +++++++++++++++ Weather/View/Custom/InfoView.swift | 26 +++++++ Weather/View/Custom/InfoView.xib | 78 +++++++++++++++++++ .../WeatherViewController.swift | 0 8 files changed, 290 insertions(+), 6 deletions(-) create mode 100644 Weather/View/Cell/HourlyForecastCell.swift create mode 100644 Weather/View/Custom/DailyForecastView.swift create mode 100644 Weather/View/Custom/DailyForecastView.xib create mode 100644 Weather/View/Custom/InfoView.swift create mode 100644 Weather/View/Custom/InfoView.xib rename Weather/{ViewModel => View}/WeatherViewController.swift (100%) diff --git a/Weather.xcodeproj/project.pbxproj b/Weather.xcodeproj/project.pbxproj index 31fe49d9..547a696c 100644 --- a/Weather.xcodeproj/project.pbxproj +++ b/Weather.xcodeproj/project.pbxproj @@ -13,6 +13,12 @@ AA3006AA218CE3DD00A23A36 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA3006A9218CE3DD00A23A36 /* Assets.xcassets */; }; AA3006AD218CE3DD00A23A36 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA3006AB218CE3DD00A23A36 /* LaunchScreen.storyboard */; }; AA3006B8218CE3DD00A23A36 /* WeatherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */; }; + AA4528C5218D9EEC00EC0901 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C4218D9EEB00EC0901 /* Constants.swift */; }; + AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */; }; + AA4528CB218D9FE900EC0901 /* DailyForecastView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */; }; + AA4528CD218DA5FD00EC0901 /* HourlyForecastCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528CC218DA5FD00EC0901 /* HourlyForecastCell.swift */; }; + AA4528CF218DB18400EC0901 /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528CE218DB18400EC0901 /* InfoView.swift */; }; + AA4528D1218DB19700EC0901 /* InfoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA4528D0218DB19700EC0901 /* InfoView.xib */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -36,6 +42,12 @@ AA3006B3218CE3DD00A23A36 /* WeatherTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WeatherTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherTests.swift; sourceTree = ""; }; AA3006B9218CE3DD00A23A36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AA4528C4218D9EEB00EC0901 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyForecastView.swift; sourceTree = ""; }; + AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DailyForecastView.xib; sourceTree = ""; }; + AA4528CC218DA5FD00EC0901 /* HourlyForecastCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HourlyForecastCell.swift; sourceTree = ""; }; + AA4528CE218DB18400EC0901 /* InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = ""; }; + AA4528D0218DB19700EC0901 /* InfoView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InfoView.xib; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -84,6 +96,7 @@ AA3006A9218CE3DD00A23A36 /* Assets.xcassets */, AA3006AB218CE3DD00A23A36 /* LaunchScreen.storyboard */, AA3006AE218CE3DD00A23A36 /* Info.plist */, + AA4528C4218D9EEB00EC0901 /* Constants.swift */, AA3006C4218CF09B00A23A36 /* Model */, AA3006C5218CF0A300A23A36 /* View */, AA3006C6218CF0BC00A23A36 /* ViewModel */, @@ -124,6 +137,9 @@ AA3006C5218CF0A300A23A36 /* View */ = { isa = PBXGroup; children = ( + AA4528C9218D9FAE00EC0901 /* Cell */, + AA4528C8218D9F9C00EC0901 /* Custom */, + AA3006A4218CE3D400A23A36 /* WeatherViewController.swift */, ); path = View; sourceTree = ""; @@ -131,11 +147,29 @@ AA3006C6218CF0BC00A23A36 /* ViewModel */ = { isa = PBXGroup; children = ( - AA3006A4218CE3D400A23A36 /* WeatherViewController.swift */, ); path = ViewModel; sourceTree = ""; }; + AA4528C8218D9F9C00EC0901 /* Custom */ = { + isa = PBXGroup; + children = ( + AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */, + AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */, + AA4528CE218DB18400EC0901 /* InfoView.swift */, + AA4528D0218DB19700EC0901 /* InfoView.xib */, + ); + path = Custom; + sourceTree = ""; + }; + AA4528C9218D9FAE00EC0901 /* Cell */ = { + isa = PBXGroup; + children = ( + AA4528CC218DA5FD00EC0901 /* HourlyForecastCell.swift */, + ); + path = Cell; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -219,7 +253,9 @@ files = ( AA3006AD218CE3DD00A23A36 /* LaunchScreen.storyboard in Resources */, AA3006AA218CE3DD00A23A36 /* Assets.xcassets in Resources */, + AA4528D1218DB19700EC0901 /* InfoView.xib in Resources */, AA3006A8218CE3D400A23A36 /* Main.storyboard in Resources */, + AA4528CB218D9FE900EC0901 /* DailyForecastView.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -239,6 +275,10 @@ files = ( AA3006A5218CE3D400A23A36 /* WeatherViewController.swift in Sources */, AA3006A3218CE3D400A23A36 /* AppDelegate.swift in Sources */, + AA4528C5218D9EEC00EC0901 /* Constants.swift in Sources */, + AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */, + AA4528CF218DB18400EC0901 /* InfoView.swift in Sources */, + AA4528CD218DA5FD00EC0901 /* HourlyForecastCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Weather/Base.lproj/Main.storyboard b/Weather/Base.lproj/Main.storyboard index 7344547e..62ef8254 100644 --- a/Weather/Base.lproj/Main.storyboard +++ b/Weather/Base.lproj/Main.storyboard @@ -64,19 +64,55 @@ - + - + - - + + - + + + + + + + + + + + + + + + + + + + + + + diff --git a/Weather/View/Cell/HourlyForecastCell.swift b/Weather/View/Cell/HourlyForecastCell.swift new file mode 100644 index 00000000..aabebe96 --- /dev/null +++ b/Weather/View/Cell/HourlyForecastCell.swift @@ -0,0 +1,15 @@ +// +// HourlyForecastCell.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class HourlyForecastCell: UICollectionViewCell { + @IBOutlet private weak var hourLabel: UILabel! + @IBOutlet private weak var chanceLabel: UILabel! + @IBOutlet private weak var temperatureLabel: UILabel! +} diff --git a/Weather/View/Custom/DailyForecastView.swift b/Weather/View/Custom/DailyForecastView.swift new file mode 100644 index 00000000..675e32df --- /dev/null +++ b/Weather/View/Custom/DailyForecastView.swift @@ -0,0 +1,26 @@ +// +// DailyForecastView.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class DailyForecastView: UIView { + @IBOutlet private weak var dayLabel: UILabel! + @IBOutlet private weak var todayLabel: UILabel! + @IBOutlet private weak var maxLabel: UILabel! + @IBOutlet private weak var minLabel: UILabel! + + + /* + // Only override draw() if you perform custom drawing. + // An empty implementation adversely affects performance during animation. + override func draw(_ rect: CGRect) { + // Drawing code + } + */ + +} diff --git a/Weather/View/Custom/DailyForecastView.xib b/Weather/View/Custom/DailyForecastView.xib new file mode 100644 index 00000000..1f8962e5 --- /dev/null +++ b/Weather/View/Custom/DailyForecastView.xib @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Weather/View/Custom/InfoView.swift b/Weather/View/Custom/InfoView.swift new file mode 100644 index 00000000..d016e5b8 --- /dev/null +++ b/Weather/View/Custom/InfoView.swift @@ -0,0 +1,26 @@ +// +// InfoView.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class InfoView: UIView { + @IBOutlet private weak var title1Label: UILabel! + @IBOutlet private weak var detail1Label: UILabel! + @IBOutlet private weak var title2Label: UILabel! + @IBOutlet private weak var detail2Label: UILabel! + @IBOutlet private weak var separatorView: UIView! + + /* + // Only override draw() if you perform custom drawing. + // An empty implementation adversely affects performance during animation. + override func draw(_ rect: CGRect) { + // Drawing code + } + */ + +} diff --git a/Weather/View/Custom/InfoView.xib b/Weather/View/Custom/InfoView.xib new file mode 100644 index 00000000..a37b3d38 --- /dev/null +++ b/Weather/View/Custom/InfoView.xib @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Weather/ViewModel/WeatherViewController.swift b/Weather/View/WeatherViewController.swift similarity index 100% rename from Weather/ViewModel/WeatherViewController.swift rename to Weather/View/WeatherViewController.swift From e4ac91bd95530600a51bca7da78a5f42e3f9fc70 Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Sun, 4 Nov 2018 09:35:52 +0700 Subject: [PATCH 04/23] Add Location Manager --- Weather.xcodeproj/project.pbxproj | 4 + Weather/Base.lproj/Main.storyboard | 19 ++++ Weather/Info.plist | 2 + Weather/Service/LocationManager.swift | 109 +++++++++++++++++++++++ Weather/View/WeatherViewController.swift | 24 +++++ 5 files changed, 158 insertions(+) create mode 100644 Weather/Service/LocationManager.swift diff --git a/Weather.xcodeproj/project.pbxproj b/Weather.xcodeproj/project.pbxproj index 547a696c..0c6dc053 100644 --- a/Weather.xcodeproj/project.pbxproj +++ b/Weather.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ AA3006AA218CE3DD00A23A36 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA3006A9218CE3DD00A23A36 /* Assets.xcassets */; }; AA3006AD218CE3DD00A23A36 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA3006AB218CE3DD00A23A36 /* LaunchScreen.storyboard */; }; AA3006B8218CE3DD00A23A36 /* WeatherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */; }; + AA316FA3218DD78B00DD1794 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA316FA2218DD78B00DD1794 /* LocationManager.swift */; }; AA4528C5218D9EEC00EC0901 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C4218D9EEB00EC0901 /* Constants.swift */; }; AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */; }; AA4528CB218D9FE900EC0901 /* DailyForecastView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */; }; @@ -42,6 +43,7 @@ AA3006B3218CE3DD00A23A36 /* WeatherTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WeatherTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherTests.swift; sourceTree = ""; }; AA3006B9218CE3DD00A23A36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AA316FA2218DD78B00DD1794 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; AA4528C4218D9EEB00EC0901 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyForecastView.swift; sourceTree = ""; }; AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DailyForecastView.xib; sourceTree = ""; }; @@ -123,6 +125,7 @@ AA3006C3218CF08D00A23A36 /* Service */ = { isa = PBXGroup; children = ( + AA316FA2218DD78B00DD1794 /* LocationManager.swift */, ); path = Service; sourceTree = ""; @@ -276,6 +279,7 @@ AA3006A5218CE3D400A23A36 /* WeatherViewController.swift in Sources */, AA3006A3218CE3D400A23A36 /* AppDelegate.swift in Sources */, AA4528C5218D9EEC00EC0901 /* Constants.swift in Sources */, + AA316FA3218DD78B00DD1794 /* LocationManager.swift in Sources */, AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */, AA4528CF218DB18400EC0901 /* InfoView.swift in Sources */, AA4528CD218DA5FD00EC0901 /* HourlyForecastCell.swift in Sources */, diff --git a/Weather/Base.lproj/Main.storyboard b/Weather/Base.lproj/Main.storyboard index 62ef8254..6e7d7ed5 100644 --- a/Weather/Base.lproj/Main.storyboard +++ b/Weather/Base.lproj/Main.storyboard @@ -217,6 +217,19 @@ + + + + + + + + + + + @@ -224,11 +237,15 @@ + + + + @@ -246,6 +263,8 @@ + + diff --git a/Weather/Info.plist b/Weather/Info.plist index 89d7858b..fae6e68d 100644 --- a/Weather/Info.plist +++ b/Weather/Info.plist @@ -20,6 +20,8 @@ 1 LSRequiresIPhoneOS + NSLocationWhenInUseUsageDescription + This app need to access your location, for Weather Forecasting. UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/Weather/Service/LocationManager.swift b/Weather/Service/LocationManager.swift new file mode 100644 index 00000000..6da14af2 --- /dev/null +++ b/Weather/Service/LocationManager.swift @@ -0,0 +1,109 @@ +// +// LocationManager.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit +import CoreLocation + +class LocationManager: NSObject { + static private var shared: LocationManager! + private var locationManager = CLLocationManager() + var lastLocation: CLLocation? + var lastPlacemark: CLPlacemark? + var isPlacemarkNeeded = false + var locationUpdateClosure: ((CLLocation?, CLPlacemark?)->())? + + static func sharedInstance() -> LocationManager { + if shared == nil { + shared = LocationManager() + } + return shared + } + + func startUpdatingLocation(placemarkNeeded: Bool = false) { + isPlacemarkNeeded = placemarkNeeded + if self.requestAuthorization() { + locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.delegate = self + locationManager.startUpdatingLocation() + } else { + print("PLease turn on location services or GPS") + } + } + + func stopUpdatingLocation() { + locationManager.stopUpdatingLocation() + } + + func requestLocation(placemarkNeeded: Bool = true) { + isPlacemarkNeeded = placemarkNeeded + if self.requestAuthorization() { + locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.delegate = self + locationManager.requestLocation() + } else { + print("PLease turn on location services or GPS") + } + } + + func lookUCurrentLocation(_ location: CLLocation, completionHandler: @escaping (CLPlacemark?) -> Void ) { + + let geocoder = CLGeocoder() + + // Look up the location and pass it to the completion handler + geocoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) in + if error == nil { + let firstLocation = placemarks?[0] + completionHandler(firstLocation) + } + else { + // An error occurred during geocoding. + completionHandler(nil) + } + }) + } +} + +//MARK:- private func +extension LocationManager { + private func requestAuthorization() -> Bool { + if CLLocationManager.locationServicesEnabled() == true { + if CLLocationManager.authorizationStatus() == .restricted || CLLocationManager.authorizationStatus() == .denied || CLLocationManager.authorizationStatus() == .notDetermined { + locationManager.requestWhenInUseAuthorization() + } + return true + } else { + print("PLease turn on location services or GPS") + return false + } + } +} + +//MARK:- CLLocationManager Delegates +extension LocationManager: CLLocationManagerDelegate { + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + lastLocation = locations[0] + print("user latitude = \(lastLocation!.coordinate.latitude)") + print("user longitude = \(lastLocation!.coordinate.longitude)") + + if isPlacemarkNeeded { + lookUCurrentLocation(lastLocation!) { placemark in + self.lastPlacemark = placemark + print("Placemark: \(placemark?.name ?? "") - \(placemark?.subLocality ?? "") - \(placemark?.country ?? "") - \(placemark?.locality ?? "") - \(placemark?.country ?? "")") + + self.locationUpdateClosure?(locations[0], placemark) + } + } else { + locationUpdateClosure?(lastLocation, nil) + } + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + print("Unable to access your current location: \(error)") + } +} diff --git a/Weather/View/WeatherViewController.swift b/Weather/View/WeatherViewController.swift index bf35fd22..7fe0c512 100644 --- a/Weather/View/WeatherViewController.swift +++ b/Weather/View/WeatherViewController.swift @@ -20,11 +20,35 @@ class WeatherViewController: UIViewController { @IBOutlet weak var forecastDescriptionLabel: UILabel! @IBOutlet weak var infoStackView: UIStackView! + @IBOutlet weak var loadingBGView: UIView! + @IBOutlet weak var loadingView: UIActivityIndicatorView! + + private var locationManager = LocationManager.sharedInstance() + override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. + locationManager.requestLocation() } + + override var preferredStatusBarStyle : UIStatusBarStyle { + return .lightContent + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + showLoadingView() + } + +} +//MARK:- private func +extension WeatherViewController { + private func showLoadingView(){ + loadingBGView.backgroundColor = view.backgroundColor + loadingBGView.alpha = 1 + loadingView.startAnimating() + } } From 5bab6ffe00dc7ec5c96afb557fd7123a39428e85 Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Sun, 4 Nov 2018 09:37:53 +0700 Subject: [PATCH 05/23] Add LocationManager --- Weather/Constants.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Weather/Constants.swift diff --git a/Weather/Constants.swift b/Weather/Constants.swift new file mode 100644 index 00000000..de026431 --- /dev/null +++ b/Weather/Constants.swift @@ -0,0 +1,14 @@ +// +// Constants.swift +// Weather +// +// Created by Christian Huang on 03/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +struct Constants { + static let apiKey = "" +} From f7f73579fe1db46a584e750e91e66e2d1d267024 Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Sun, 4 Nov 2018 09:38:35 +0700 Subject: [PATCH 06/23] Add LocationManager --- Weather/Service/LocationManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Weather/Service/LocationManager.swift b/Weather/Service/LocationManager.swift index 6da14af2..57976378 100644 --- a/Weather/Service/LocationManager.swift +++ b/Weather/Service/LocationManager.swift @@ -31,7 +31,7 @@ class LocationManager: NSObject { locationManager.delegate = self locationManager.startUpdatingLocation() } else { - print("PLease turn on location services or GPS") + print("PLease turn on location services or GPS ") } } From 2a714d40578965e1c4f4dfe4afa35e7e76c55b3b Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Sun, 4 Nov 2018 20:14:49 +0700 Subject: [PATCH 07/23] Add Network Layer and Models --- Weather.xcodeproj/project.pbxproj | 96 +++++++++++++++ Weather/Info.plist | 5 + Weather/Model/Dynamic.swift | 29 +++++ Weather/Model/Weather.swift | 111 ++++++++++++++++++ Weather/Service/LocationManager.swift | 22 ++-- .../Encoding/JSONParameterEncoder.swift | 22 ++++ .../Encoding/ParameterEncoding.swift | 52 ++++++++ .../Encoding/URLParameterEncoding.swift | 32 +++++ .../Networking/EndPoint/WeatherEndPoint.swift | 81 +++++++++++++ .../Networking/Manager/NetworkManager.swift | 103 ++++++++++++++++ .../Networking/Service/EndPointType.swift | 22 ++++ .../Networking/Service/HTTPMethod.swift | 15 +++ .../Service/Networking/Service/HTTPTask.swift | 26 ++++ .../Networking/Service/NetworkLogger.swift | 39 ++++++ .../Service/Networking/Service/Router.swift | 96 +++++++++++++++ Weather/View/WeatherViewController.swift | 8 +- Weather/ViewModel/ForecastViewModel.swift | 15 +++ Weather/ViewModel/WeatherViewModel.swift | 89 ++++++++++++++ 18 files changed, 844 insertions(+), 19 deletions(-) create mode 100755 Weather/Model/Dynamic.swift create mode 100644 Weather/Model/Weather.swift create mode 100755 Weather/Service/Networking/Encoding/JSONParameterEncoder.swift create mode 100755 Weather/Service/Networking/Encoding/ParameterEncoding.swift create mode 100755 Weather/Service/Networking/Encoding/URLParameterEncoding.swift create mode 100755 Weather/Service/Networking/EndPoint/WeatherEndPoint.swift create mode 100755 Weather/Service/Networking/Manager/NetworkManager.swift create mode 100755 Weather/Service/Networking/Service/EndPointType.swift create mode 100755 Weather/Service/Networking/Service/HTTPMethod.swift create mode 100755 Weather/Service/Networking/Service/HTTPTask.swift create mode 100755 Weather/Service/Networking/Service/NetworkLogger.swift create mode 100755 Weather/Service/Networking/Service/Router.swift create mode 100644 Weather/ViewModel/ForecastViewModel.swift create mode 100644 Weather/ViewModel/WeatherViewModel.swift diff --git a/Weather.xcodeproj/project.pbxproj b/Weather.xcodeproj/project.pbxproj index 0c6dc053..19e8dc4d 100644 --- a/Weather.xcodeproj/project.pbxproj +++ b/Weather.xcodeproj/project.pbxproj @@ -7,6 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + AA0E4B01218E940500379569 /* JSONParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF4218E940500379569 /* JSONParameterEncoder.swift */; }; + AA0E4B02218E940500379569 /* ParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF5218E940500379569 /* ParameterEncoding.swift */; }; + AA0E4B03218E940500379569 /* URLParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF6218E940500379569 /* URLParameterEncoding.swift */; }; + AA0E4B04218E940500379569 /* WeatherEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF8218E940500379569 /* WeatherEndPoint.swift */; }; + AA0E4B05218E940500379569 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AFA218E940500379569 /* NetworkManager.swift */; }; + AA0E4B06218E940500379569 /* EndPointType.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AFC218E940500379569 /* EndPointType.swift */; }; + AA0E4B07218E940500379569 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AFD218E940500379569 /* HTTPMethod.swift */; }; + AA0E4B08218E940500379569 /* HTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AFE218E940500379569 /* HTTPTask.swift */; }; + AA0E4B09218E940500379569 /* NetworkLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AFF218E940500379569 /* NetworkLogger.swift */; }; + AA0E4B0A218E940500379569 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4B00218E940500379569 /* Router.swift */; }; AA3006A3218CE3D400A23A36 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006A2218CE3D400A23A36 /* AppDelegate.swift */; }; AA3006A5218CE3D400A23A36 /* WeatherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006A4218CE3D400A23A36 /* WeatherViewController.swift */; }; AA3006A8218CE3D400A23A36 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA3006A6218CE3D400A23A36 /* Main.storyboard */; }; @@ -14,12 +24,16 @@ AA3006AD218CE3DD00A23A36 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA3006AB218CE3DD00A23A36 /* LaunchScreen.storyboard */; }; AA3006B8218CE3DD00A23A36 /* WeatherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */; }; AA316FA3218DD78B00DD1794 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA316FA2218DD78B00DD1794 /* LocationManager.swift */; }; + AA3685C2218F19D6002CD889 /* ForecastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3685C1218F19D6002CD889 /* ForecastViewModel.swift */; }; AA4528C5218D9EEC00EC0901 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C4218D9EEB00EC0901 /* Constants.swift */; }; AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */; }; AA4528CB218D9FE900EC0901 /* DailyForecastView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */; }; AA4528CD218DA5FD00EC0901 /* HourlyForecastCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528CC218DA5FD00EC0901 /* HourlyForecastCell.swift */; }; AA4528CF218DB18400EC0901 /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528CE218DB18400EC0901 /* InfoView.swift */; }; AA4528D1218DB19700EC0901 /* InfoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA4528D0218DB19700EC0901 /* InfoView.xib */; }; + AA60F075218EC7D20080F2C7 /* WeatherViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */; }; + AAC62752218EAF7600A221C0 /* Weather.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC62751218EAF7600A221C0 /* Weather.swift */; }; + AAFF64E2218EF84500C512B8 /* Dynamic.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFF64E1218EF84500C512B8 /* Dynamic.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -33,6 +47,16 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + AA0E4AF4218E940500379569 /* JSONParameterEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONParameterEncoder.swift; sourceTree = ""; }; + AA0E4AF5218E940500379569 /* ParameterEncoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParameterEncoding.swift; sourceTree = ""; }; + AA0E4AF6218E940500379569 /* URLParameterEncoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLParameterEncoding.swift; sourceTree = ""; }; + AA0E4AF8218E940500379569 /* WeatherEndPoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeatherEndPoint.swift; sourceTree = ""; }; + AA0E4AFA218E940500379569 /* NetworkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; + AA0E4AFC218E940500379569 /* EndPointType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EndPointType.swift; sourceTree = ""; }; + AA0E4AFD218E940500379569 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; + AA0E4AFE218E940500379569 /* HTTPTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPTask.swift; sourceTree = ""; }; + AA0E4AFF218E940500379569 /* NetworkLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkLogger.swift; sourceTree = ""; }; + AA0E4B00218E940500379569 /* Router.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; AA30069F218CE3D400A23A36 /* Weather.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Weather.app; sourceTree = BUILT_PRODUCTS_DIR; }; AA3006A2218CE3D400A23A36 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; AA3006A4218CE3D400A23A36 /* WeatherViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewController.swift; sourceTree = ""; }; @@ -44,12 +68,16 @@ AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherTests.swift; sourceTree = ""; }; AA3006B9218CE3DD00A23A36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AA316FA2218DD78B00DD1794 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; + AA3685C1218F19D6002CD889 /* ForecastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastViewModel.swift; sourceTree = ""; }; AA4528C4218D9EEB00EC0901 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyForecastView.swift; sourceTree = ""; }; AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DailyForecastView.xib; sourceTree = ""; }; AA4528CC218DA5FD00EC0901 /* HourlyForecastCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HourlyForecastCell.swift; sourceTree = ""; }; AA4528CE218DB18400EC0901 /* InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = ""; }; AA4528D0218DB19700EC0901 /* InfoView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InfoView.xib; sourceTree = ""; }; + AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModel.swift; sourceTree = ""; }; + AAC62751218EAF7600A221C0 /* Weather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weather.swift; sourceTree = ""; }; + AAFF64E1218EF84500C512B8 /* Dynamic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dynamic.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -70,6 +98,55 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + AA0E4AF2218E940500379569 /* Networking */ = { + isa = PBXGroup; + children = ( + AA0E4AF3218E940500379569 /* Encoding */, + AA0E4AF7218E940500379569 /* EndPoint */, + AA0E4AF9218E940500379569 /* Manager */, + AA0E4AFB218E940500379569 /* Service */, + ); + path = Networking; + sourceTree = ""; + }; + AA0E4AF3218E940500379569 /* Encoding */ = { + isa = PBXGroup; + children = ( + AA0E4AF4218E940500379569 /* JSONParameterEncoder.swift */, + AA0E4AF5218E940500379569 /* ParameterEncoding.swift */, + AA0E4AF6218E940500379569 /* URLParameterEncoding.swift */, + ); + path = Encoding; + sourceTree = ""; + }; + AA0E4AF7218E940500379569 /* EndPoint */ = { + isa = PBXGroup; + children = ( + AA0E4AF8218E940500379569 /* WeatherEndPoint.swift */, + ); + path = EndPoint; + sourceTree = ""; + }; + AA0E4AF9218E940500379569 /* Manager */ = { + isa = PBXGroup; + children = ( + AA0E4AFA218E940500379569 /* NetworkManager.swift */, + ); + path = Manager; + sourceTree = ""; + }; + AA0E4AFB218E940500379569 /* Service */ = { + isa = PBXGroup; + children = ( + AA0E4AFC218E940500379569 /* EndPointType.swift */, + AA0E4AFD218E940500379569 /* HTTPMethod.swift */, + AA0E4AFE218E940500379569 /* HTTPTask.swift */, + AA0E4AFF218E940500379569 /* NetworkLogger.swift */, + AA0E4B00218E940500379569 /* Router.swift */, + ); + path = Service; + sourceTree = ""; + }; AA300696218CE3D400A23A36 = { isa = PBXGroup; children = ( @@ -125,6 +202,7 @@ AA3006C3218CF08D00A23A36 /* Service */ = { isa = PBXGroup; children = ( + AA0E4AF2218E940500379569 /* Networking */, AA316FA2218DD78B00DD1794 /* LocationManager.swift */, ); path = Service; @@ -133,6 +211,8 @@ AA3006C4218CF09B00A23A36 /* Model */ = { isa = PBXGroup; children = ( + AAFF64E1218EF84500C512B8 /* Dynamic.swift */, + AAC62751218EAF7600A221C0 /* Weather.swift */, ); path = Model; sourceTree = ""; @@ -150,6 +230,8 @@ AA3006C6218CF0BC00A23A36 /* ViewModel */ = { isa = PBXGroup; children = ( + AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */, + AA3685C1218F19D6002CD889 /* ForecastViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -276,13 +358,27 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AA0E4B05218E940500379569 /* NetworkManager.swift in Sources */, + AAFF64E2218EF84500C512B8 /* Dynamic.swift in Sources */, + AA0E4B02218E940500379569 /* ParameterEncoding.swift in Sources */, + AA3685C2218F19D6002CD889 /* ForecastViewModel.swift in Sources */, + AA0E4B07218E940500379569 /* HTTPMethod.swift in Sources */, + AA0E4B06218E940500379569 /* EndPointType.swift in Sources */, + AAC62752218EAF7600A221C0 /* Weather.swift in Sources */, + AA0E4B01218E940500379569 /* JSONParameterEncoder.swift in Sources */, AA3006A5218CE3D400A23A36 /* WeatherViewController.swift in Sources */, + AA0E4B08218E940500379569 /* HTTPTask.swift in Sources */, AA3006A3218CE3D400A23A36 /* AppDelegate.swift in Sources */, + AA0E4B09218E940500379569 /* NetworkLogger.swift in Sources */, + AA0E4B03218E940500379569 /* URLParameterEncoding.swift in Sources */, + AA0E4B0A218E940500379569 /* Router.swift in Sources */, AA4528C5218D9EEC00EC0901 /* Constants.swift in Sources */, AA316FA3218DD78B00DD1794 /* LocationManager.swift in Sources */, AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */, + AA60F075218EC7D20080F2C7 /* WeatherViewModel.swift in Sources */, AA4528CF218DB18400EC0901 /* InfoView.swift in Sources */, AA4528CD218DA5FD00EC0901 /* HourlyForecastCell.swift in Sources */, + AA0E4B04218E940500379569 /* WeatherEndPoint.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Weather/Info.plist b/Weather/Info.plist index fae6e68d..c11238e0 100644 --- a/Weather/Info.plist +++ b/Weather/Info.plist @@ -20,6 +20,11 @@ 1 LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSLocationWhenInUseUsageDescription This app need to access your location, for Weather Forecasting. UILaunchStoryboardName diff --git a/Weather/Model/Dynamic.swift b/Weather/Model/Dynamic.swift new file mode 100755 index 00000000..d38266c8 --- /dev/null +++ b/Weather/Model/Dynamic.swift @@ -0,0 +1,29 @@ +// +// Dynamic.swift +// ZenJob +// +// + +class Dynamic { + typealias Listener = (T) -> () + var listener: Listener? + + func bind(_ listener: Listener?) { + self.listener = listener + } + + func bindAndFire(_ listener: Listener?) { + self.listener = listener + listener?(value) + } + + var value: T { + didSet { + listener?(value) + } + } + + init(_ v: T) { + value = v + } +} diff --git a/Weather/Model/Weather.swift b/Weather/Model/Weather.swift new file mode 100644 index 00000000..ab017ce4 --- /dev/null +++ b/Weather/Model/Weather.swift @@ -0,0 +1,111 @@ +// +// Weather.swift +// Weather +// +// Created by Christian Huang on 04/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + +enum DayTime: Int { + case morning + case noon + case afternoon + case night +} + +struct WeatherResponse: Codable { + let coord: Coordinate + let weather: [Weather] + let base: String + let main: WeatherData + let wind: Wind + let clouds: Clouds + //let rain: Rain? + //let snow: Snow? + let dt: TimeInterval + let sys: SystemData + let id: Int + let name: String +} +struct Coordinate: Codable { + let longitude: Double + let latitude: Double + + enum CodingKeys: String, CodingKey { + case longitude = "lon" + case latitude = "lat" + } +} + +struct Weather: Codable { + let id: Int + let main: String + let description: String + let icon: String +} + +struct WeatherData: Codable { + let temperature: Double + let pressure: Int + let humidity: Int + let temperatureMin: Double + let temperatureMax: Double + let seaLevel: Double? + let groundLevel: Double? + + enum CodingKeys: String, CodingKey { + case temperature = "temp" + case pressure + case humidity + case temperatureMin = "temp_min" + case temperatureMax = "temp_max" + case seaLevel = "sea_level" + case groundLevel = "grnd_level" + } +} + +struct Wind: Codable { + let speed: Double + let degree: Double + + enum CodingKeys: String, CodingKey { + case speed + case degree = "deg" + } +} + +struct Clouds: Codable { + let cloudiness: Double + + enum CodingKeys: String, CodingKey { + case cloudiness = "all" + } +} + +/*struct Rain: Codable { + let rain3h: Double + + enum CodingKeys: String, CodingKey { + case rain3h = "3h" + } +} + +struct Snow: Codable { + let snow3h: Double + + enum CodingKeys: String, CodingKey { + case snow3h = "3h" + } +}*/ + +struct SystemData: Codable { + let type: Int + let id: Int + let message: Double + let country: String + let sunrise: TimeInterval + let sunset: TimeInterval + +} diff --git a/Weather/Service/LocationManager.swift b/Weather/Service/LocationManager.swift index 57976378..d064f71c 100644 --- a/Weather/Service/LocationManager.swift +++ b/Weather/Service/LocationManager.swift @@ -30,8 +30,6 @@ class LocationManager: NSObject { locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.delegate = self locationManager.startUpdatingLocation() - } else { - print("PLease turn on location services or GPS ") } } @@ -39,18 +37,16 @@ class LocationManager: NSObject { locationManager.stopUpdatingLocation() } - func requestLocation(placemarkNeeded: Bool = true) { + func requestLocation(placemarkNeeded: Bool = false) { isPlacemarkNeeded = placemarkNeeded if self.requestAuthorization() { locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.delegate = self locationManager.requestLocation() - } else { - print("PLease turn on location services or GPS") } } - func lookUCurrentLocation(_ location: CLLocation, completionHandler: @escaping (CLPlacemark?) -> Void ) { + func lookUpLocation(_ location: CLLocation, completionHandler: @escaping (CLPlacemark?) -> Void ) { let geocoder = CLGeocoder() @@ -77,7 +73,7 @@ extension LocationManager { } return true } else { - print("PLease turn on location services or GPS") + print("Please turn on location services or GPS") return false } } @@ -88,19 +84,15 @@ extension LocationManager: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { lastLocation = locations[0] - print("user latitude = \(lastLocation!.coordinate.latitude)") - print("user longitude = \(lastLocation!.coordinate.longitude)") - if isPlacemarkNeeded { - lookUCurrentLocation(lastLocation!) { placemark in + lookUpLocation(lastLocation!) { placemark in self.lastPlacemark = placemark - print("Placemark: \(placemark?.name ?? "") - \(placemark?.subLocality ?? "") - \(placemark?.country ?? "") - \(placemark?.locality ?? "") - \(placemark?.country ?? "")") - self.locationUpdateClosure?(locations[0], placemark) } - } else { - locationUpdateClosure?(lastLocation, nil) } + + locationUpdateClosure?(lastLocation, nil) + } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { diff --git a/Weather/Service/Networking/Encoding/JSONParameterEncoder.swift b/Weather/Service/Networking/Encoding/JSONParameterEncoder.swift new file mode 100755 index 00000000..7cf4ba40 --- /dev/null +++ b/Weather/Service/Networking/Encoding/JSONParameterEncoder.swift @@ -0,0 +1,22 @@ +// +// JSONEncoding.swift +// NetworkLayer +// +// + +import Foundation + +public struct JSONParameterEncoder: ParameterEncoder { + public func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws { + do { + let jsonAsData = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted) + urlRequest.httpBody = jsonAsData + if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + } + }catch { + throw NetworkError.encodingFailed + } + } +} + diff --git a/Weather/Service/Networking/Encoding/ParameterEncoding.swift b/Weather/Service/Networking/Encoding/ParameterEncoding.swift new file mode 100755 index 00000000..05556fc6 --- /dev/null +++ b/Weather/Service/Networking/Encoding/ParameterEncoding.swift @@ -0,0 +1,52 @@ +// +// ParameterEncoding.swift +// NetworkLayer +// +// + +import Foundation + +public typealias Parameters = [String:Any] + +public protocol ParameterEncoder { + func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws +} + +public enum ParameterEncoding { + + case urlEncoding + case jsonEncoding + case urlAndJsonEncoding + + public func encode(urlRequest: inout URLRequest, + bodyParameters: Parameters?, + urlParameters: Parameters?) throws { + do { + switch self { + case .urlEncoding: + guard let urlParameters = urlParameters else { return } + try URLParameterEncoder().encode(urlRequest: &urlRequest, with: urlParameters) + + case .jsonEncoding: + guard let bodyParameters = bodyParameters else { return } + try JSONParameterEncoder().encode(urlRequest: &urlRequest, with: bodyParameters) + + case .urlAndJsonEncoding: + guard let bodyParameters = bodyParameters, + let urlParameters = urlParameters else { return } + try URLParameterEncoder().encode(urlRequest: &urlRequest, with: urlParameters) + try JSONParameterEncoder().encode(urlRequest: &urlRequest, with: bodyParameters) + + } + }catch { + throw error + } + } +} + + +public enum NetworkError : String, Error { + case parametersNil = "Parameters were nil." + case encodingFailed = "Parameter encoding failed." + case missingURL = "URL is nil." +} diff --git a/Weather/Service/Networking/Encoding/URLParameterEncoding.swift b/Weather/Service/Networking/Encoding/URLParameterEncoding.swift new file mode 100755 index 00000000..5e92bdcb --- /dev/null +++ b/Weather/Service/Networking/Encoding/URLParameterEncoding.swift @@ -0,0 +1,32 @@ +// +// URLEncoding.swift +// NetworkLayer +// +// + +import Foundation + +public struct URLParameterEncoder: ParameterEncoder { + public func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws { + + guard let url = urlRequest.url else { throw NetworkError.missingURL } + + if var urlComponents = URLComponents(url: url, + resolvingAgainstBaseURL: false), !parameters.isEmpty { + + urlComponents.queryItems = [URLQueryItem]() + + for (key,value) in parameters { + let queryItem = URLQueryItem(name: key, + value: "\(value)") //.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) + urlComponents.queryItems?.append(queryItem) + } + urlRequest.url = urlComponents.url + } + + if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { + urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type") + } + + } +} diff --git a/Weather/Service/Networking/EndPoint/WeatherEndPoint.swift b/Weather/Service/Networking/EndPoint/WeatherEndPoint.swift new file mode 100755 index 00000000..683ea290 --- /dev/null +++ b/Weather/Service/Networking/EndPoint/WeatherEndPoint.swift @@ -0,0 +1,81 @@ +// +// WeatherPoint.swift +// NetworkLayer +// +// + +import Foundation + +public enum WeatherAPI { + case weather(latitude: Float, longitude: Float, apiKey: String) + case forecast(latitude: Float, longitude: Float, apiKey: String) + case forecastDaily(latitude: Float, longitude: Float, apiKey: String) +} + +extension WeatherAPI: EndPointType { + /** API base urls. */ + var environmentBaseURL : String { + switch NetworkManager.environment { + case .production: return "http://worksample-api.herokuapp.com/" + default: return "http://worksample-api.herokuapp.com/" + } + } + + var baseURL: URL { + guard let url = URL(string: environmentBaseURL) else { fatalError("baseURL could not be configured.")} + return url + } + /** API path for specific request. */ + var path: String { + switch self { + case .weather: + return "weather" + case .forecast: + return "forecast" + case .forecastDaily: + return "forecast/daily" + } + + } + + var httpMethod: HTTPMethod { + switch self { + default: + return .get + } + } + + /** generate task based on requested Auto1's API. */ + var task: HTTPTask { + switch self { + case .weather(let latitude, let longitude, let apiKey): + return .requestParametersAndHeaders(bodyParameters: nil, + bodyEncoding: .urlEncoding, + urlParameters: ["lat" : latitude, + "lon" : longitude, + "key" : apiKey], + additionHeaders: nil) + case .forecast(let latitude, let longitude, let apiKey): + return .requestParametersAndHeaders(bodyParameters: nil, + bodyEncoding: .urlEncoding, + urlParameters: ["lat" : latitude, + "lon" : longitude, + "key" : apiKey], + additionHeaders: nil) + case .forecastDaily(let latitude, let longitude, let apiKey): + return .requestParametersAndHeaders(bodyParameters: nil, + bodyEncoding: .urlEncoding, + urlParameters: ["lat" : latitude, + "lon" : longitude, + "key" : apiKey], + additionHeaders: nil) + } + } + + var headers: HTTPHeaders? { + return nil + } +} + + + diff --git a/Weather/Service/Networking/Manager/NetworkManager.swift b/Weather/Service/Networking/Manager/NetworkManager.swift new file mode 100755 index 00000000..143dd8a4 --- /dev/null +++ b/Weather/Service/Networking/Manager/NetworkManager.swift @@ -0,0 +1,103 @@ +// +// NetworkManager.swift +// NetworkLayer +// +// + +import Foundation + +enum NetworkResponse:String { + case success + case authenticationError = "network_authentication_error" + case badRequest = "network_bad_request" + case outdated = "network_outdated" + case failed = "network_failed" + case noData = "network_no_data" + case unableToDecode = "network_unable_to_decode" + case noResponseObject = "network_no_response_object" +} + +enum Result{ + case success + case failure(String) +} + +protocol WeatherNetworkManager { + var apiKey: String { get set } + + init(apiKey: String, environment: NetworkEnvironment) + /** + Fetch weather data from Weather API server + - Parameter latitude: user's latitude position. + - Parameter longitude: user's longitude position. + - Parameter completion: block to handle the fetch results + */ + func fetchWeather(latitude: Double, longitude: Double, completion: @escaping (_ response: WeatherResponse?,_ error: String?)->()) +} + +class NetworkManager: WeatherNetworkManager { + static var environment : NetworkEnvironment = .production + var apiKey: String + let router = Router() + + required init(apiKey: String, environment: NetworkEnvironment) { + self.apiKey = apiKey + NetworkManager.environment = environment + } + + /** + Fetch weather data from Weather API server + - Parameter lat: user's latitude position. + - Parameter lon: user's longitude position. + - Parameter completion: block to handle the fetch results + */ + func fetchWeather(latitude: Double, longitude: Double, completion: @escaping (_ response: WeatherResponse?,_ error: String?)->()) { + router.request(.weather(latitude: Float(latitude), longitude: Float(longitude), apiKey: apiKey)) { data, response, error in + let result = self.handleNetworkResponse(data: data, response: response, error: error) + switch result{ + case .success: + do { + /*let jsonData = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) + print(jsonData)*/ + let weatherResponse = try JSONDecoder().decode(WeatherResponse.self, from: data!) + completion(weatherResponse, nil) + }catch { + print(error) + completion(nil, NetworkResponse.unableToDecode.rawValue) + } + case .failure(let error): + completion(nil, error) + } + } + } + + func cancelLastRequest() { + router.cancel() + } + + /** + Handle all basic network error responses + */ + fileprivate func handleNetworkResponse(data: Data?, response: URLResponse?, error: Error?) -> Result{ + if error != nil { + return .failure("Please check your network connection.") + } + + if let response = response as? HTTPURLResponse { + switch response.statusCode { + case 200...299: + print("HTTPURLResponse: success") + case 401...500: return .failure(NetworkResponse.authenticationError.rawValue) + case 501...599: return .failure(NetworkResponse.badRequest.rawValue) + case 600: return .failure(NetworkResponse.outdated.rawValue) + default: return .failure(NetworkResponse.failed.rawValue) + } + } + + guard data != nil else { + return .failure(NetworkResponse.noData.rawValue) + } + + return .success + } +} diff --git a/Weather/Service/Networking/Service/EndPointType.swift b/Weather/Service/Networking/Service/EndPointType.swift new file mode 100755 index 00000000..25a1ced2 --- /dev/null +++ b/Weather/Service/Networking/Service/EndPointType.swift @@ -0,0 +1,22 @@ +// +// EndPoint.swift +// NetworkLayer +// +// + +import Foundation + +enum NetworkEnvironment { + case qa + case production + case staging +} + +protocol EndPointType { + var baseURL: URL { get } + var path: String { get } + var httpMethod: HTTPMethod { get } + var task: HTTPTask { get } + var headers: HTTPHeaders? { get } +} + diff --git a/Weather/Service/Networking/Service/HTTPMethod.swift b/Weather/Service/Networking/Service/HTTPMethod.swift new file mode 100755 index 00000000..35db0736 --- /dev/null +++ b/Weather/Service/Networking/Service/HTTPMethod.swift @@ -0,0 +1,15 @@ +// +// HTTPMethod.swift +// NetworkLayer +// +// + +import Foundation + +public enum HTTPMethod : String { + case get = "GET" + case post = "POST" + case put = "PUT" + case patch = "PATCH" + case delete = "DELETE" +} diff --git a/Weather/Service/Networking/Service/HTTPTask.swift b/Weather/Service/Networking/Service/HTTPTask.swift new file mode 100755 index 00000000..38de0b6e --- /dev/null +++ b/Weather/Service/Networking/Service/HTTPTask.swift @@ -0,0 +1,26 @@ +// +// HTTPTask.swift +// NetworkLayer +// +// + +import Foundation + +public typealias HTTPHeaders = [String:String] + +public enum HTTPTask { + case request + + case requestParameters(bodyParameters: Parameters?, + bodyEncoding: ParameterEncoding, + urlParameters: Parameters?) + + case requestParametersAndHeaders(bodyParameters: Parameters?, + bodyEncoding: ParameterEncoding, + urlParameters: Parameters?, + additionHeaders: HTTPHeaders?) + + // case download, upload...etc +} + + diff --git a/Weather/Service/Networking/Service/NetworkLogger.swift b/Weather/Service/Networking/Service/NetworkLogger.swift new file mode 100755 index 00000000..86e28389 --- /dev/null +++ b/Weather/Service/Networking/Service/NetworkLogger.swift @@ -0,0 +1,39 @@ +// +// NetworkLogger.swift +// NetworkLayer +// +// + +import Foundation + +class NetworkLogger { + static func log(request: URLRequest) { + + print("\n - - - - - - - - - - OUTGOING - - - - - - - - - - \n") + defer { print("\n - - - - - - - - - - END - - - - - - - - - - \n") } + + let urlAsString = request.url?.absoluteString ?? "" + let urlComponents = NSURLComponents(string: urlAsString) + + let method = request.httpMethod != nil ? "\(request.httpMethod ?? "")" : "" + let path = "\(urlComponents?.path ?? "")" + let query = "\(urlComponents?.query ?? "")" + let host = "\(urlComponents?.host ?? "")" + + var logOutput = """ + \(urlAsString) \n\n + \(method) \(path)?\(query) HTTP/1.1 \n + HOST: \(host)\n + """ + for (key,value) in request.allHTTPHeaderFields ?? [:] { + logOutput += "\(key): \(value) \n" + } + if let body = request.httpBody { + logOutput += "\n \(NSString(data: body, encoding: String.Encoding.utf8.rawValue) ?? "")" + } + + print(logOutput) + } + + static func log(response: URLResponse) {} +} diff --git a/Weather/Service/Networking/Service/Router.swift b/Weather/Service/Networking/Service/Router.swift new file mode 100755 index 00000000..1a1e8da7 --- /dev/null +++ b/Weather/Service/Networking/Service/Router.swift @@ -0,0 +1,96 @@ +// +// NetworkService.swift +// NetworkLayer +// +// + +import Foundation + +public typealias NetworkRouterCompletion = (_ data: Data?,_ response: URLResponse?,_ error: Error?)->() + +protocol NetworkRouter: class { + associatedtype EndPoint: EndPointType + func request(_ route: EndPoint, completion: @escaping NetworkRouterCompletion) + func cancel() +} + +class Router: NetworkRouter { + private let session = URLSession(configuration: .default) + private var task: URLSessionTask? + + func request(_ route: EndPoint, completion: @escaping NetworkRouterCompletion) { + //let session = URLSession.shared + do { + let request = try self.buildRequest(from: route) + NetworkLogger.log(request: request) + task = session.dataTask(with: request, completionHandler: { data, response, error in + completion(data, response, error) + }) + }catch { + completion(nil, nil, error) + } + self.task?.resume() + } + + func cancel() { + self.task?.cancel() + } + + fileprivate func buildRequest(from route: EndPoint) throws -> URLRequest { + + var request = URLRequest(url: route.baseURL.appendingPathComponent(route.path), + cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, + timeoutInterval: 10.0) + + request.httpMethod = route.httpMethod.rawValue + do { + switch route.task { + case .request: + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + case .requestParameters(let bodyParameters, + let bodyEncoding, + let urlParameters): + + try self.configureParameters(bodyParameters: bodyParameters, + bodyEncoding: bodyEncoding, + urlParameters: urlParameters, + request: &request) + + case .requestParametersAndHeaders(let bodyParameters, + let bodyEncoding, + let urlParameters, + let additionalHeaders): + + self.addAdditionalHeaders(additionalHeaders, request: &request) + try self.configureParameters(bodyParameters: bodyParameters, + bodyEncoding: bodyEncoding, + urlParameters: urlParameters, + request: &request) + } + return request + } catch { + throw error + } + } + + fileprivate func configureParameters(bodyParameters: Parameters?, + bodyEncoding: ParameterEncoding, + urlParameters: Parameters?, + request: inout URLRequest) throws { + do { + try bodyEncoding.encode(urlRequest: &request, + bodyParameters: bodyParameters, urlParameters: urlParameters) + } catch { + throw error + } + } + + fileprivate func addAdditionalHeaders(_ additionalHeaders: HTTPHeaders?, request: inout URLRequest) { + guard let headers = additionalHeaders else { return } + for (key, value) in headers { + request.setValue(value, forHTTPHeaderField: key) + } + } + +} + diff --git a/Weather/View/WeatherViewController.swift b/Weather/View/WeatherViewController.swift index 7fe0c512..50ad538f 100644 --- a/Weather/View/WeatherViewController.swift +++ b/Weather/View/WeatherViewController.swift @@ -23,21 +23,21 @@ class WeatherViewController: UIViewController { @IBOutlet weak var loadingBGView: UIView! @IBOutlet weak var loadingView: UIActivityIndicatorView! - private var locationManager = LocationManager.sharedInstance() + private var weatherViewModel = WeatherViewModel() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. - locationManager.requestLocation() } override var preferredStatusBarStyle : UIStatusBarStyle { return .lightContent } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) showLoadingView() + weatherViewModel.startWeatherForcasting() } diff --git a/Weather/ViewModel/ForecastViewModel.swift b/Weather/ViewModel/ForecastViewModel.swift new file mode 100644 index 00000000..236cf874 --- /dev/null +++ b/Weather/ViewModel/ForecastViewModel.swift @@ -0,0 +1,15 @@ +// +// ForecastViewModel.swift +// Weather +// +// Created by Christian Huang on 04/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class ForecastViewModel: NSObject { + var networkManager: WeatherNetworkManager! + + +} diff --git a/Weather/ViewModel/WeatherViewModel.swift b/Weather/ViewModel/WeatherViewModel.swift new file mode 100644 index 00000000..33b99434 --- /dev/null +++ b/Weather/ViewModel/WeatherViewModel.swift @@ -0,0 +1,89 @@ +// +// WeatherViewModel.swift +// Weather +// +// Created by Christian Huang on 04/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit +import CoreLocation + +class WeatherViewModel: NSObject { + private var location: Coordinate? + var area = Dynamic("") + var weather = Dynamic("") + var temperature: Dynamic = Dynamic(0) + var summary = Dynamic("") + var dayTime: Dynamic = Dynamic(DayTime.morning) + var dayString = Dynamic("") + var temperatureMin: Dynamic = Dynamic(0) + var temperatureMax: Dynamic = Dynamic(0) + var sunrise = Dynamic("00:00") + var sunset = Dynamic("00:00") + var pressure = Dynamic("") + var humidity = Dynamic("") + + + lazy var networkManager: WeatherNetworkManager = NetworkManager(apiKey: Constants.apiKey, environment: Constants.networkEnvironment) + private var locationManager = LocationManager() + + + +} + +//MARK: - public func +extension WeatherViewModel { + func startWeatherForcasting() { + initLocationManager() + locationManager.requestLocation() + } +} + +//MARK: - private func +extension WeatherViewModel { + private func initLocationManager() { + if locationManager.locationUpdateClosure == nil { + locationManager.locationUpdateClosure = { location, placemark in + if let location = location { + print("Location: \(location.coordinate.latitude) - \(location.coordinate.longitude)") + self.location = Coordinate(longitude: location.coordinate.longitude, latitude: location.coordinate.latitude) + self.fetchWeather() + } else { + // cannot get user location + } + } + } + } + + private func fetchWeather() { + guard let location = location else { + print("Location is not avaliable") + return + } + + networkManager.fetchWeather(latitude: location.latitude, longitude: location.longitude) { (weatherResponse, error) in + if let error = error { + print("Network Error: \(error)") + } else { + self.processWeatherResponse(weatherResponse!) + } + } + } + + private func processWeatherResponse(_ response: WeatherResponse) { + area.value = response.name + weather.value = response.weather[0].main + temperature.value = response.main.temperature + summary.value = String(format: "Today: %@ currently. It's %d°; the high today was forecast as %d°.", response.weather[0].main, Int(response.main.temperature), Int(response.main.temperatureMax)) + + dayTime.value = .night + dayString.value = "Sunday" + temperatureMin.value = response.main.temperatureMin + temperatureMax.value = response.main.temperatureMax + sunrise.value = "06:00" + sunset.value = "17:45" + pressure.value = String(format: "%d hPa", response.main.pressure) + humidity.value = String(format: "%d%", response.main.humidity) + } +} From 6fd90203e113ec8ad8bfb198d963e665277d70aa Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Tue, 6 Nov 2018 03:31:27 +0700 Subject: [PATCH 08/23] Add WeatherViewModel and connect it to fetch weather data --- Weather.xcodeproj/project.pbxproj | 20 +++- .../xcschemes/xcschememanagement.plist | 5 + Weather/Base.lproj/Main.storyboard | 33 +++--- Weather/Model/Forecast.swift | 33 ++++++ Weather/Model/ForecastDaily.swift | 39 +++++++ Weather/Model/MiscInfo.swift | 105 ++++++++++++++++++ Weather/Model/Weather.swift | 73 +++--------- Weather/View/Custom/DailyForecastView.swift | 14 +++ Weather/View/Custom/InfoView.swift | 23 ++++ Weather/View/WeatherViewController.swift | 76 ++++++++++--- Weather/ViewModel/WeatherViewModel.swift | 36 +++--- 11 files changed, 347 insertions(+), 110 deletions(-) create mode 100644 Weather/Model/Forecast.swift create mode 100644 Weather/Model/ForecastDaily.swift create mode 100644 Weather/Model/MiscInfo.swift diff --git a/Weather.xcodeproj/project.pbxproj b/Weather.xcodeproj/project.pbxproj index 19e8dc4d..800f40e9 100644 --- a/Weather.xcodeproj/project.pbxproj +++ b/Weather.xcodeproj/project.pbxproj @@ -24,7 +24,7 @@ AA3006AD218CE3DD00A23A36 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA3006AB218CE3DD00A23A36 /* LaunchScreen.storyboard */; }; AA3006B8218CE3DD00A23A36 /* WeatherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */; }; AA316FA3218DD78B00DD1794 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA316FA2218DD78B00DD1794 /* LocationManager.swift */; }; - AA3685C2218F19D6002CD889 /* ForecastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3685C1218F19D6002CD889 /* ForecastViewModel.swift */; }; + AA3685C2218F19D6002CD889 /* ForecastListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3685C1218F19D6002CD889 /* ForecastListViewModel.swift */; }; AA4528C5218D9EEC00EC0901 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C4218D9EEB00EC0901 /* Constants.swift */; }; AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */; }; AA4528CB218D9FE900EC0901 /* DailyForecastView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */; }; @@ -33,6 +33,9 @@ AA4528D1218DB19700EC0901 /* InfoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA4528D0218DB19700EC0901 /* InfoView.xib */; }; AA60F075218EC7D20080F2C7 /* WeatherViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */; }; AAC62752218EAF7600A221C0 /* Weather.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC62751218EAF7600A221C0 /* Weather.swift */; }; + AAF473082190804000D09B00 /* Forecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF473072190804000D09B00 /* Forecast.swift */; }; + AAF4730A2190830400D09B00 /* MiscInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF473092190830400D09B00 /* MiscInfo.swift */; }; + AAF4730C2190851300D09B00 /* ForecastDaily.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF4730B2190851300D09B00 /* ForecastDaily.swift */; }; AAFF64E2218EF84500C512B8 /* Dynamic.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFF64E1218EF84500C512B8 /* Dynamic.swift */; }; /* End PBXBuildFile section */ @@ -68,7 +71,7 @@ AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherTests.swift; sourceTree = ""; }; AA3006B9218CE3DD00A23A36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AA316FA2218DD78B00DD1794 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; - AA3685C1218F19D6002CD889 /* ForecastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastViewModel.swift; sourceTree = ""; }; + AA3685C1218F19D6002CD889 /* ForecastListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastListViewModel.swift; sourceTree = ""; }; AA4528C4218D9EEB00EC0901 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyForecastView.swift; sourceTree = ""; }; AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DailyForecastView.xib; sourceTree = ""; }; @@ -77,6 +80,9 @@ AA4528D0218DB19700EC0901 /* InfoView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InfoView.xib; sourceTree = ""; }; AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModel.swift; sourceTree = ""; }; AAC62751218EAF7600A221C0 /* Weather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weather.swift; sourceTree = ""; }; + AAF473072190804000D09B00 /* Forecast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Forecast.swift; sourceTree = ""; }; + AAF473092190830400D09B00 /* MiscInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiscInfo.swift; sourceTree = ""; }; + AAF4730B2190851300D09B00 /* ForecastDaily.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDaily.swift; sourceTree = ""; }; AAFF64E1218EF84500C512B8 /* Dynamic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dynamic.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -213,6 +219,9 @@ children = ( AAFF64E1218EF84500C512B8 /* Dynamic.swift */, AAC62751218EAF7600A221C0 /* Weather.swift */, + AAF473072190804000D09B00 /* Forecast.swift */, + AAF4730B2190851300D09B00 /* ForecastDaily.swift */, + AAF473092190830400D09B00 /* MiscInfo.swift */, ); path = Model; sourceTree = ""; @@ -231,7 +240,7 @@ isa = PBXGroup; children = ( AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */, - AA3685C1218F19D6002CD889 /* ForecastViewModel.swift */, + AA3685C1218F19D6002CD889 /* ForecastListViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -359,9 +368,10 @@ buildActionMask = 2147483647; files = ( AA0E4B05218E940500379569 /* NetworkManager.swift in Sources */, + AAF4730A2190830400D09B00 /* MiscInfo.swift in Sources */, AAFF64E2218EF84500C512B8 /* Dynamic.swift in Sources */, AA0E4B02218E940500379569 /* ParameterEncoding.swift in Sources */, - AA3685C2218F19D6002CD889 /* ForecastViewModel.swift in Sources */, + AA3685C2218F19D6002CD889 /* ForecastListViewModel.swift in Sources */, AA0E4B07218E940500379569 /* HTTPMethod.swift in Sources */, AA0E4B06218E940500379569 /* EndPointType.swift in Sources */, AAC62752218EAF7600A221C0 /* Weather.swift in Sources */, @@ -373,11 +383,13 @@ AA0E4B03218E940500379569 /* URLParameterEncoding.swift in Sources */, AA0E4B0A218E940500379569 /* Router.swift in Sources */, AA4528C5218D9EEC00EC0901 /* Constants.swift in Sources */, + AAF4730C2190851300D09B00 /* ForecastDaily.swift in Sources */, AA316FA3218DD78B00DD1794 /* LocationManager.swift in Sources */, AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */, AA60F075218EC7D20080F2C7 /* WeatherViewModel.swift in Sources */, AA4528CF218DB18400EC0901 /* InfoView.swift in Sources */, AA4528CD218DA5FD00EC0901 /* HourlyForecastCell.swift in Sources */, + AAF473082190804000D09B00 /* Forecast.swift in Sources */, AA0E4B04218E940500379569 /* WeatherEndPoint.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist b/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist index f22a76a3..51757c96 100644 --- a/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,6 +9,11 @@ orderHint 0 + Weather.xcscheme_^#shared#^_ + + orderHint + 0 + diff --git a/Weather/Base.lproj/Main.storyboard b/Weather/Base.lproj/Main.storyboard index 6e7d7ed5..9a91e1db 100644 --- a/Weather/Base.lproj/Main.storyboard +++ b/Weather/Base.lproj/Main.storyboard @@ -1,10 +1,10 @@ - + - + @@ -139,11 +139,11 @@ - + - - + + @@ -155,24 +155,18 @@ - + - - - - - - - - - + + @@ -180,8 +174,8 @@ - - + + @@ -192,9 +186,8 @@ - - - + + diff --git a/Weather/Model/Forecast.swift b/Weather/Model/Forecast.swift new file mode 100644 index 00000000..d96c1268 --- /dev/null +++ b/Weather/Model/Forecast.swift @@ -0,0 +1,33 @@ +// +// Forecast.swift +// Weather +// +// Created by Christian Huang on 05/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +struct ForecastResponse: Codable { + let city: City + let list: [Forecast] +} + +struct Forecast: Codable { + let dayTime: TimeInterval + let main: WeatherData + let weather: [Weather] + let clouds: Clouds + let wind: Wind + let dayTimeText: String + + enum CodingKeys: String, CodingKey { + case dayTime = "dt" + case main + case weather + case clouds + case wind + case dayTimeText = "dt_txt" + } +} diff --git a/Weather/Model/ForecastDaily.swift b/Weather/Model/ForecastDaily.swift new file mode 100644 index 00000000..40887a91 --- /dev/null +++ b/Weather/Model/ForecastDaily.swift @@ -0,0 +1,39 @@ +// +// ForecastDaily.swift +// Weather +// +// Created by Christian Huang on 05/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +struct ForecastDailyResponse: Codable { + let city: City + let list: [ForecastDaily] +} + +struct ForecastDaily: Codable { + let dayTime: TimeInterval + let temperature: Temperature + let pressure: Double + let humidity: Double + let weather: [Weather] + let speed: Double + let degree: Double + let clouds: Double + let rain: Double + + enum CodingKeys: String, CodingKey { + case dayTime = "dt" + case temperature = "temp" + case pressure + case humidity + case weather + case speed + case degree = "deg" + case clouds + case rain + } +} diff --git a/Weather/Model/MiscInfo.swift b/Weather/Model/MiscInfo.swift new file mode 100644 index 00000000..ebd12edf --- /dev/null +++ b/Weather/Model/MiscInfo.swift @@ -0,0 +1,105 @@ +// +// Info.swift +// Weather +// +// Created by Christian Huang on 05/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +enum DayTime: Int { + case morning + case day + case evening + case night +} + +struct Coordinate: Codable { + let longitude: Double + let latitude: Double + + enum CodingKeys: String, CodingKey { + case longitude = "lon" + case latitude = "lat" + } +} + +struct Temperature: Codable { + let min: Double + let max: Double + let morning: Double + let day: Double + let evening: Double + let night: Double + + enum CodingKeys: String, CodingKey { + case min + case max + case morning = "morn" + case day + case evening = "eve" + case night + } +} + +struct Wind: Codable { + let speed: Double + let degree: Double? + + enum CodingKeys: String, CodingKey { + case speed + case degree = "deg" + } +} + +struct Clouds: Codable { + let cloudiness: Double + + enum CodingKeys: String, CodingKey { + case cloudiness = "all" + } +} + +struct Rain: Codable { + let rain3h: Double + + enum CodingKeys: String, CodingKey { + case rain3h = "3h" + } +} + +struct Snow: Codable { + let snow3h: Double + + enum CodingKeys: String, CodingKey { + case snow3h = "3h" + } +} + +struct SystemData: Codable { + let type: Int + let id: Int + let message: Double + let country: String + let sunrise: TimeInterval + let sunset: TimeInterval + +} + +struct City: Codable { + let id: Int + let name: String + let coordinate: Coordinate + let country: String + + enum CodingKeys: String, CodingKey { + case id + case name + case coordinate = "coord" + case country + } +} + + diff --git a/Weather/Model/Weather.swift b/Weather/Model/Weather.swift index ab017ce4..2f6a1253 100644 --- a/Weather/Model/Weather.swift +++ b/Weather/Model/Weather.swift @@ -8,15 +8,8 @@ import Foundation -enum DayTime: Int { - case morning - case noon - case afternoon - case night -} - struct WeatherResponse: Codable { - let coord: Coordinate + let coordinate: Coordinate let weather: [Weather] let base: String let main: WeatherData @@ -24,18 +17,22 @@ struct WeatherResponse: Codable { let clouds: Clouds //let rain: Rain? //let snow: Snow? - let dt: TimeInterval - let sys: SystemData + let dayTime: TimeInterval + let system: SystemData let id: Int let name: String -} -struct Coordinate: Codable { - let longitude: Double - let latitude: Double enum CodingKeys: String, CodingKey { - case longitude = "lon" - case latitude = "lat" + case coordinate = "coord" + case weather + case base + case main + case wind + case clouds + case dayTime = "dt" + case system = "sys" + case id + case name } } @@ -65,47 +62,3 @@ struct WeatherData: Codable { case groundLevel = "grnd_level" } } - -struct Wind: Codable { - let speed: Double - let degree: Double - - enum CodingKeys: String, CodingKey { - case speed - case degree = "deg" - } -} - -struct Clouds: Codable { - let cloudiness: Double - - enum CodingKeys: String, CodingKey { - case cloudiness = "all" - } -} - -/*struct Rain: Codable { - let rain3h: Double - - enum CodingKeys: String, CodingKey { - case rain3h = "3h" - } -} - -struct Snow: Codable { - let snow3h: Double - - enum CodingKeys: String, CodingKey { - case snow3h = "3h" - } -}*/ - -struct SystemData: Codable { - let type: Int - let id: Int - let message: Double - let country: String - let sunrise: TimeInterval - let sunset: TimeInterval - -} diff --git a/Weather/View/Custom/DailyForecastView.swift b/Weather/View/Custom/DailyForecastView.swift index 675e32df..1db29373 100644 --- a/Weather/View/Custom/DailyForecastView.swift +++ b/Weather/View/Custom/DailyForecastView.swift @@ -14,6 +14,13 @@ class DailyForecastView: UIView { @IBOutlet private weak var maxLabel: UILabel! @IBOutlet private weak var minLabel: UILabel! + + class func instanceFromNib() -> DailyForecastView { + let instance = UINib(nibName: "DailyForecastView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! DailyForecastView + instance.translatesAutoresizingMaskIntoConstraints = false + instance.heightAnchor.constraint(equalToConstant: instance.bounds.height).isActive = true + return instance + } /* // Only override draw() if you perform custom drawing. @@ -22,5 +29,12 @@ class DailyForecastView: UIView { // Drawing code } */ + + func configureTodayView(weatherViewModel: WeatherViewModel) { + dayLabel.text = weatherViewModel.dayString.value + todayLabel.alpha = 1 + maxLabel.text = "\(Int(weatherViewModel.temperatureMax.value))°" + minLabel.text = "\(Int(weatherViewModel.temperatureMin.value))°" + } } diff --git a/Weather/View/Custom/InfoView.swift b/Weather/View/Custom/InfoView.swift index d016e5b8..3fe09dbe 100644 --- a/Weather/View/Custom/InfoView.swift +++ b/Weather/View/Custom/InfoView.swift @@ -9,12 +9,35 @@ import UIKit class InfoView: UIView { + var detail1: String = "" { + didSet{ + detail1Label.text = detail1 + } + } + var detail2: String = "" { + didSet{ + detail2Label.text = detail2 + } + } @IBOutlet private weak var title1Label: UILabel! @IBOutlet private weak var detail1Label: UILabel! @IBOutlet private weak var title2Label: UILabel! @IBOutlet private weak var detail2Label: UILabel! @IBOutlet private weak var separatorView: UIView! + class func instanceFromNib() -> InfoView { + let instance = UINib(nibName: "InfoView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! InfoView + instance.translatesAutoresizingMaskIntoConstraints = false + instance.heightAnchor.constraint(equalToConstant: instance.bounds.height).isActive = true + return instance + } + + func configureInfoView(title1: String, title2: String, hasSeparator: Bool = true) { + title1Label.text = title1 + title2Label.text = title2 + separatorView.alpha = hasSeparator ? 1 : 0 + } + /* // Only override draw() if you perform custom drawing. // An empty implementation adversely affects performance during animation. diff --git a/Weather/View/WeatherViewController.swift b/Weather/View/WeatherViewController.swift index 50ad538f..aa1d9b74 100644 --- a/Weather/View/WeatherViewController.swift +++ b/Weather/View/WeatherViewController.swift @@ -9,25 +9,29 @@ import UIKit class WeatherViewController: UIViewController { - @IBOutlet weak var headerView: UIView! - @IBOutlet weak var areaLabel: UILabel! - @IBOutlet weak var weatherLabel: UILabel! - @IBOutlet weak var temperatureLabel: UILabel! - @IBOutlet weak var todayView: UIView! - @IBOutlet weak var todayForecastCollectionView: UICollectionView! - @IBOutlet weak var detailScrollView: UIScrollView! - @IBOutlet weak var dailyForecastStackView: UIStackView! - @IBOutlet weak var forecastDescriptionLabel: UILabel! - @IBOutlet weak var infoStackView: UIStackView! - - @IBOutlet weak var loadingBGView: UIView! - @IBOutlet weak var loadingView: UIActivityIndicatorView! + @IBOutlet private weak var headerView: UIView! + @IBOutlet private weak var areaLabel: UILabel! + @IBOutlet private weak var weatherLabel: UILabel! + @IBOutlet private weak var temperatureLabel: UILabel! + private var todayForecastView = DailyForecastView.instanceFromNib() + @IBOutlet private weak var todayView: UIView! + @IBOutlet private weak var todayForecastCollectionView: UICollectionView! + @IBOutlet private weak var detailScrollView: UIScrollView! + @IBOutlet private weak var dailyForecastStackView: UIStackView! + @IBOutlet private weak var forecastDescriptionLabel: UILabel! + @IBOutlet private weak var infoStackView: UIStackView! + private var sunriseSunsetView = InfoView.instanceFromNib() + private var pressureHumidityView = InfoView.instanceFromNib() + @IBOutlet private weak var loadingBGView: UIView! + @IBOutlet private weak var loadingView: UIActivityIndicatorView! private var weatherViewModel = WeatherViewModel() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. + initViewModel() + setupUI() } override var preferredStatusBarStyle : UIStatusBarStyle { @@ -43,12 +47,58 @@ class WeatherViewController: UIViewController { } + +//MARK:- ViewModel related +extension WeatherViewController { + func initViewModel() { + weatherViewModel.area.listener = { self.areaLabel.text = $0 } + weatherViewModel.weather.listener = { self.weatherLabel.text = $0 } + weatherViewModel.temperature.listener = { self.temperatureLabel.text = "\(Int($0))°" } + weatherViewModel.summary.listener = { summary in + self.forecastDescriptionLabel.text = summary + } + weatherViewModel.sunrise.listener = { self.sunriseSunsetView.detail1 = $0 } + weatherViewModel.sunset.listener = { self.sunriseSunsetView.detail2 = $0 } + weatherViewModel.pressure.listener = { self.pressureHumidityView.detail1 = $0 } + weatherViewModel.humidity.listener = { self.pressureHumidityView.detail2 = $0 } + + weatherViewModel.updateWeatherDataClosure = updateWeatherData + weatherViewModel.finishedFetchingWeatherClosure = hideLoadingView + } + + private func updateWeatherData() { + todayForecastView.configureTodayView(weatherViewModel: weatherViewModel) + } +} + //MARK:- private func extension WeatherViewController { + private func setupUI() { + self.view.insertSubview(todayForecastView, belowSubview: todayView) + sunriseSunsetView.configureInfoView(title1: "Sunrise", title2: "Sunset") + infoStackView.addArrangedSubview(sunriseSunsetView) + pressureHumidityView.configureInfoView(title1: "Pressure", title2: "Humidity", hasSeparator: false) + infoStackView.addArrangedSubview(pressureHumidityView) + + NSLayoutConstraint.activate([ + todayForecastView.widthAnchor.constraint(equalTo: view.widthAnchor), + todayForecastView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + todayForecastView.bottomAnchor.constraint(equalTo: todayView.topAnchor) + ]) + } private func showLoadingView(){ loadingBGView.backgroundColor = view.backgroundColor loadingBGView.alpha = 1 loadingView.startAnimating() } + private func hideLoadingView() { + UIView.animate(withDuration: 0.2, + animations: { + self.loadingBGView.alpha = 0 + }, + completion: { finished in + self.loadingView.stopAnimating() + }) + } } diff --git a/Weather/ViewModel/WeatherViewModel.swift b/Weather/ViewModel/WeatherViewModel.swift index 33b99434..ebf271bc 100644 --- a/Weather/ViewModel/WeatherViewModel.swift +++ b/Weather/ViewModel/WeatherViewModel.swift @@ -28,6 +28,11 @@ class WeatherViewModel: NSObject { lazy var networkManager: WeatherNetworkManager = NetworkManager(apiKey: Constants.apiKey, environment: Constants.networkEnvironment) private var locationManager = LocationManager() + var forecastListViewModel: ForecastListViewModel? + + var updateWeatherDataClosure: (()->())? + var finishedFetchingWeatherClosure: (()->())? + } @@ -49,6 +54,7 @@ extension WeatherViewModel { print("Location: \(location.coordinate.latitude) - \(location.coordinate.longitude)") self.location = Coordinate(longitude: location.coordinate.longitude, latitude: location.coordinate.latitude) self.fetchWeather() + self.forecastListViewModel?.fetchForecast(coordinate: location!) } else { // cannot get user location } @@ -72,18 +78,22 @@ extension WeatherViewModel { } private func processWeatherResponse(_ response: WeatherResponse) { - area.value = response.name - weather.value = response.weather[0].main - temperature.value = response.main.temperature - summary.value = String(format: "Today: %@ currently. It's %d°; the high today was forecast as %d°.", response.weather[0].main, Int(response.main.temperature), Int(response.main.temperatureMax)) - - dayTime.value = .night - dayString.value = "Sunday" - temperatureMin.value = response.main.temperatureMin - temperatureMax.value = response.main.temperatureMax - sunrise.value = "06:00" - sunset.value = "17:45" - pressure.value = String(format: "%d hPa", response.main.pressure) - humidity.value = String(format: "%d%", response.main.humidity) + DispatchQueue.main.async { + self.area.value = response.name + self.weather.value = response.weather[0].main + self.temperature.value = response.main.temperature + self.summary.value = String(format: "Today: %@ currently. It's %d°; the high today was forecast as %d°.", response.weather[0].main, Int(response.main.temperature), Int(response.main.temperatureMax)) + + self.dayTime.value = .night + self.dayString.value = "Sunday" + self.temperatureMin.value = response.main.temperatureMin + self.temperatureMax.value = response.main.temperatureMax + self.sunrise.value = "06:00" + self.sunset.value = "17:45" + self.pressure.value = String(format: "%d hPa", response.main.pressure) + self.humidity.value = String(format: "%d%", response.main.humidity) + self.updateWeatherDataClosure?() + self.finishedFetchingWeatherClosure?() + } } } From b0a6e1a268c9465417fbffcb99742a9e73ce03fd Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Tue, 6 Nov 2018 04:55:50 +0700 Subject: [PATCH 09/23] Add ForecastListViewModel to handle Forecast API --- Weather.xcodeproj/project.pbxproj | 32 ++++++++++++ Weather/Base.lproj/Main.storyboard | 6 +-- Weather/Model/Weather.swift | 2 +- .../Networking/Manager/NetworkManager.swift | 22 ++++++++ Weather/Utils/Extension/StringExtension.swift | 51 +++++++++++++++++++ Weather/View/Cell/HourlyForecastCell.swift | 7 +++ Weather/View/ForecastCollectionView.swift | 41 +++++++++++++++ Weather/View/WeatherViewController.swift | 5 +- Weather/ViewModel/ForecastListViewModel.swift | 41 +++++++++++++++ Weather/ViewModel/ForecastViewModel.swift | 13 +++-- Weather/ViewModel/WeatherViewModel.swift | 10 ++-- 11 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 Weather/Utils/Extension/StringExtension.swift create mode 100644 Weather/View/ForecastCollectionView.swift create mode 100644 Weather/ViewModel/ForecastListViewModel.swift diff --git a/Weather.xcodeproj/project.pbxproj b/Weather.xcodeproj/project.pbxproj index 800f40e9..a98c6ceb 100644 --- a/Weather.xcodeproj/project.pbxproj +++ b/Weather.xcodeproj/project.pbxproj @@ -25,6 +25,12 @@ AA3006B8218CE3DD00A23A36 /* WeatherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */; }; AA316FA3218DD78B00DD1794 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA316FA2218DD78B00DD1794 /* LocationManager.swift */; }; AA3685C2218F19D6002CD889 /* ForecastListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3685C1218F19D6002CD889 /* ForecastListViewModel.swift */; }; + AA44BC9B2190E2910069B282 /* ForecastCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BC9A2190E2910069B282 /* ForecastCollectionView.swift */; }; + AA44BC9D2190E4F80069B282 /* ForecastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BC9C2190E4F80069B282 /* ForecastViewModel.swift */; }; + AA44BCA02190E7D70069B282 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BC9F2190E7D70069B282 /* StringExtension.swift */; }; + AA44BCA22190F2DD0069B282 /* DailyForecastStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BCA12190F2DD0069B282 /* DailyForecastStackView.swift */; }; + AA44BCA42190F31B0069B282 /* DailyForecastListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BCA32190F31B0069B282 /* DailyForecastListViewModel.swift */; }; + AA44BCA62190F3700069B282 /* DailyForecastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BCA52190F3700069B282 /* DailyForecastViewModel.swift */; }; AA4528C5218D9EEC00EC0901 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C4218D9EEB00EC0901 /* Constants.swift */; }; AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */; }; AA4528CB218D9FE900EC0901 /* DailyForecastView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */; }; @@ -72,6 +78,12 @@ AA3006B9218CE3DD00A23A36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AA316FA2218DD78B00DD1794 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; AA3685C1218F19D6002CD889 /* ForecastListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastListViewModel.swift; sourceTree = ""; }; + AA44BC9A2190E2910069B282 /* ForecastCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastCollectionView.swift; sourceTree = ""; }; + AA44BC9C2190E4F80069B282 /* ForecastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastViewModel.swift; sourceTree = ""; }; + AA44BC9F2190E7D70069B282 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; + AA44BCA12190F2DD0069B282 /* DailyForecastStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyForecastStackView.swift; sourceTree = ""; }; + AA44BCA32190F31B0069B282 /* DailyForecastListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyForecastListViewModel.swift; sourceTree = ""; }; + AA44BCA52190F3700069B282 /* DailyForecastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyForecastViewModel.swift; sourceTree = ""; }; AA4528C4218D9EEB00EC0901 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyForecastView.swift; sourceTree = ""; }; AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DailyForecastView.xib; sourceTree = ""; }; @@ -201,6 +213,7 @@ AA3006C2218CF07F00A23A36 /* Utils */ = { isa = PBXGroup; children = ( + AA44BC9E2190E7C10069B282 /* Extension */, ); path = Utils; sourceTree = ""; @@ -232,6 +245,8 @@ AA4528C9218D9FAE00EC0901 /* Cell */, AA4528C8218D9F9C00EC0901 /* Custom */, AA3006A4218CE3D400A23A36 /* WeatherViewController.swift */, + AA44BC9A2190E2910069B282 /* ForecastCollectionView.swift */, + AA44BCA12190F2DD0069B282 /* DailyForecastStackView.swift */, ); path = View; sourceTree = ""; @@ -241,10 +256,21 @@ children = ( AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */, AA3685C1218F19D6002CD889 /* ForecastListViewModel.swift */, + AA44BC9C2190E4F80069B282 /* ForecastViewModel.swift */, + AA44BCA32190F31B0069B282 /* DailyForecastListViewModel.swift */, + AA44BCA52190F3700069B282 /* DailyForecastViewModel.swift */, ); path = ViewModel; sourceTree = ""; }; + AA44BC9E2190E7C10069B282 /* Extension */ = { + isa = PBXGroup; + children = ( + AA44BC9F2190E7D70069B282 /* StringExtension.swift */, + ); + path = Extension; + sourceTree = ""; + }; AA4528C8218D9F9C00EC0901 /* Custom */ = { isa = PBXGroup; children = ( @@ -369,8 +395,10 @@ files = ( AA0E4B05218E940500379569 /* NetworkManager.swift in Sources */, AAF4730A2190830400D09B00 /* MiscInfo.swift in Sources */, + AA44BC9B2190E2910069B282 /* ForecastCollectionView.swift in Sources */, AAFF64E2218EF84500C512B8 /* Dynamic.swift in Sources */, AA0E4B02218E940500379569 /* ParameterEncoding.swift in Sources */, + AA44BCA02190E7D70069B282 /* StringExtension.swift in Sources */, AA3685C2218F19D6002CD889 /* ForecastListViewModel.swift in Sources */, AA0E4B07218E940500379569 /* HTTPMethod.swift in Sources */, AA0E4B06218E940500379569 /* EndPointType.swift in Sources */, @@ -387,9 +415,13 @@ AA316FA3218DD78B00DD1794 /* LocationManager.swift in Sources */, AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */, AA60F075218EC7D20080F2C7 /* WeatherViewModel.swift in Sources */, + AA44BCA22190F2DD0069B282 /* DailyForecastStackView.swift in Sources */, + AA44BCA62190F3700069B282 /* DailyForecastViewModel.swift in Sources */, AA4528CF218DB18400EC0901 /* InfoView.swift in Sources */, AA4528CD218DA5FD00EC0901 /* HourlyForecastCell.swift in Sources */, + AA44BC9D2190E4F80069B282 /* ForecastViewModel.swift in Sources */, AAF473082190804000D09B00 /* Forecast.swift in Sources */, + AA44BCA42190F31B0069B282 /* DailyForecastListViewModel.swift in Sources */, AA0E4B04218E940500379569 /* WeatherEndPoint.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Weather/Base.lproj/Main.storyboard b/Weather/Base.lproj/Main.storyboard index 9a91e1db..9b881b70 100644 --- a/Weather/Base.lproj/Main.storyboard +++ b/Weather/Base.lproj/Main.storyboard @@ -60,10 +60,10 @@ - + - + @@ -259,7 +259,7 @@ - + diff --git a/Weather/Model/Weather.swift b/Weather/Model/Weather.swift index 2f6a1253..f042cabc 100644 --- a/Weather/Model/Weather.swift +++ b/Weather/Model/Weather.swift @@ -45,7 +45,7 @@ struct Weather: Codable { struct WeatherData: Codable { let temperature: Double - let pressure: Int + let pressure: Double let humidity: Int let temperatureMin: Double let temperatureMax: Double diff --git a/Weather/Service/Networking/Manager/NetworkManager.swift b/Weather/Service/Networking/Manager/NetworkManager.swift index 143dd8a4..ef4495c9 100755 --- a/Weather/Service/Networking/Manager/NetworkManager.swift +++ b/Weather/Service/Networking/Manager/NetworkManager.swift @@ -33,6 +33,8 @@ protocol WeatherNetworkManager { - Parameter completion: block to handle the fetch results */ func fetchWeather(latitude: Double, longitude: Double, completion: @escaping (_ response: WeatherResponse?,_ error: String?)->()) + + func fetchForecast(latitude: Double, longitude: Double, completion: @escaping (_ response: ForecastResponse?,_ error: String?)->()) } class NetworkManager: WeatherNetworkManager { @@ -71,6 +73,26 @@ class NetworkManager: WeatherNetworkManager { } } + func fetchForecast(latitude: Double, longitude: Double, completion: @escaping (_ response: ForecastResponse?,_ error: String?)->()) { + router.request(.forecast(latitude: Float(latitude), longitude: Float(longitude), apiKey: apiKey)) { data, response, error in + let result = self.handleNetworkResponse(data: data, response: response, error: error) + switch result{ + case .success: + do { + /*let jsonData = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) + print(jsonData)*/ + let forecastResponse = try JSONDecoder().decode(ForecastResponse.self, from: data!) + completion(forecastResponse, nil) + }catch { + print(error) + completion(nil, NetworkResponse.unableToDecode.rawValue) + } + case .failure(let error): + completion(nil, error) + } + } + } + func cancelLastRequest() { router.cancel() } diff --git a/Weather/Utils/Extension/StringExtension.swift b/Weather/Utils/Extension/StringExtension.swift new file mode 100644 index 00000000..6d81b7f5 --- /dev/null +++ b/Weather/Utils/Extension/StringExtension.swift @@ -0,0 +1,51 @@ +// +// StringExtension.swift +// Weather +// +// Created by Christian Huang on 06/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + +extension String { + subscript(value: NSRange) -> Substring { + return self[value.lowerBound..) -> Substring { + get { + return self[index(at: value.lowerBound)...index(at: value.upperBound)] + } + } + + subscript(value: CountableRange) -> Substring { + get { + return self[index(at: value.lowerBound)..) -> Substring { + get { + return self[..) -> Substring { + get { + return self[...index(at: value.upperBound)] + } + } + + subscript(value: PartialRangeFrom) -> Substring { + get { + return self[index(at: value.lowerBound)...] + } + } + + func index(at offset: Int) -> String.Index { + return index(startIndex, offsetBy: offset) + } +} diff --git a/Weather/View/Cell/HourlyForecastCell.swift b/Weather/View/Cell/HourlyForecastCell.swift index aabebe96..9c73fcb7 100644 --- a/Weather/View/Cell/HourlyForecastCell.swift +++ b/Weather/View/Cell/HourlyForecastCell.swift @@ -12,4 +12,11 @@ class HourlyForecastCell: UICollectionViewCell { @IBOutlet private weak var hourLabel: UILabel! @IBOutlet private weak var chanceLabel: UILabel! @IBOutlet private weak var temperatureLabel: UILabel! + + func configureCell(forecastViewModel: ForecastViewModel, isNow: Bool = false) { + hourLabel.text = isNow ? "Now" : forecastViewModel.time + chanceLabel.text = nil + temperatureLabel.text = forecastViewModel.temperature + } + } diff --git a/Weather/View/ForecastCollectionView.swift b/Weather/View/ForecastCollectionView.swift new file mode 100644 index 00000000..b86671c4 --- /dev/null +++ b/Weather/View/ForecastCollectionView.swift @@ -0,0 +1,41 @@ +// +// ForecastCollectionView.swift +// Weather +// +// Created by Christian Huang on 06/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class ForecastCollectionView: UICollectionView { + + var forecastListViewModel = ForecastListViewModel() + +} + +//MARK:- ViewModel related +extension ForecastCollectionView { + func initViewModel() { + self.dataSource = self + forecastListViewModel.forecastList.listener = { forecastList in + DispatchQueue.main.async { + self.reloadData() + } + } + } +} + +//MARK:- UICollectionViewDataSource related +extension ForecastCollectionView: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return forecastListViewModel.forecastCount + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "hourlyForecastCellIdentifier", for: indexPath) as! HourlyForecastCell + let forecastViewModel = forecastListViewModel.forecastList.value[indexPath.item] + cell.configureCell(forecastViewModel: forecastViewModel, isNow: indexPath.item == 0) + return cell + } +} diff --git a/Weather/View/WeatherViewController.swift b/Weather/View/WeatherViewController.swift index aa1d9b74..1f30af45 100644 --- a/Weather/View/WeatherViewController.swift +++ b/Weather/View/WeatherViewController.swift @@ -15,7 +15,7 @@ class WeatherViewController: UIViewController { @IBOutlet private weak var temperatureLabel: UILabel! private var todayForecastView = DailyForecastView.instanceFromNib() @IBOutlet private weak var todayView: UIView! - @IBOutlet private weak var todayForecastCollectionView: UICollectionView! + @IBOutlet private weak var todayForecastCollectionView: ForecastCollectionView! @IBOutlet private weak var detailScrollView: UIScrollView! @IBOutlet private weak var dailyForecastStackView: UIStackView! @IBOutlet private weak var forecastDescriptionLabel: UILabel! @@ -64,6 +64,9 @@ extension WeatherViewController { weatherViewModel.updateWeatherDataClosure = updateWeatherData weatherViewModel.finishedFetchingWeatherClosure = hideLoadingView + + todayForecastCollectionView.initViewModel() + weatherViewModel.forecastListViewModel = todayForecastCollectionView.forecastListViewModel } private func updateWeatherData() { diff --git a/Weather/ViewModel/ForecastListViewModel.swift b/Weather/ViewModel/ForecastListViewModel.swift new file mode 100644 index 00000000..e0d5b09d --- /dev/null +++ b/Weather/ViewModel/ForecastListViewModel.swift @@ -0,0 +1,41 @@ +// +// ForecastListViewModel.swift +// Weather +// +// Created by Christian Huang on 04/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class ForecastListViewModel: NSObject { + var forecastList: Dynamic<[ForecastViewModel]> = Dynamic([]) + var forecastCount: Int { + return forecastList.value.count + } + + var networkManager: WeatherNetworkManager! + + +} + +extension ForecastListViewModel { + func fetchForecast(coordinate: Coordinate) { + networkManager.fetchForecast(latitude: coordinate.latitude, longitude: coordinate.longitude) { (forecastResponse, error) in + if let error = error { + print("Network Error: \(error)") + } else { + self.processForecastResponse(forecastResponse!) + } + } + } +} + +extension ForecastListViewModel { + private func processForecastResponse(_ forecastResponse: ForecastResponse) { + forecastList.value = forecastResponse.list.map({ forecast -> ForecastViewModel in + return ForecastViewModel(forecast: forecast) + }) + + } +} diff --git a/Weather/ViewModel/ForecastViewModel.swift b/Weather/ViewModel/ForecastViewModel.swift index 236cf874..42ca5bd9 100644 --- a/Weather/ViewModel/ForecastViewModel.swift +++ b/Weather/ViewModel/ForecastViewModel.swift @@ -2,14 +2,19 @@ // ForecastViewModel.swift // Weather // -// Created by Christian Huang on 04/11/18. +// Created by Christian Huang on 06/11/18. // Copyright © 2018 Christian Huang. All rights reserved. // -import UIKit +import Foundation + class ForecastViewModel: NSObject { - var networkManager: WeatherNetworkManager! - + var time: String + var temperature: String + init(forecast: Forecast) { + time = String(forecast.dayTimeText[11...12]) + temperature = "\(Int(forecast.main.temperature))°" + } } diff --git a/Weather/ViewModel/WeatherViewModel.swift b/Weather/ViewModel/WeatherViewModel.swift index ebf271bc..f3bfbdd0 100644 --- a/Weather/ViewModel/WeatherViewModel.swift +++ b/Weather/ViewModel/WeatherViewModel.swift @@ -28,7 +28,11 @@ class WeatherViewModel: NSObject { lazy var networkManager: WeatherNetworkManager = NetworkManager(apiKey: Constants.apiKey, environment: Constants.networkEnvironment) private var locationManager = LocationManager() - var forecastListViewModel: ForecastListViewModel? + var forecastListViewModel: ForecastListViewModel? { + didSet { + forecastListViewModel?.networkManager = networkManager + } + } var updateWeatherDataClosure: (()->())? var finishedFetchingWeatherClosure: (()->())? @@ -54,7 +58,7 @@ extension WeatherViewModel { print("Location: \(location.coordinate.latitude) - \(location.coordinate.longitude)") self.location = Coordinate(longitude: location.coordinate.longitude, latitude: location.coordinate.latitude) self.fetchWeather() - self.forecastListViewModel?.fetchForecast(coordinate: location!) + self.forecastListViewModel?.fetchForecast(coordinate: self.location!) } else { // cannot get user location } @@ -90,7 +94,7 @@ extension WeatherViewModel { self.temperatureMax.value = response.main.temperatureMax self.sunrise.value = "06:00" self.sunset.value = "17:45" - self.pressure.value = String(format: "%d hPa", response.main.pressure) + self.pressure.value = String(format: "%.2f hPa", response.main.pressure) self.humidity.value = String(format: "%d%", response.main.humidity) self.updateWeatherDataClosure?() self.finishedFetchingWeatherClosure?() From f4cef0c60ccf5273068dd027bd2eb9726f89c550 Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Thu, 8 Nov 2018 04:15:20 +0700 Subject: [PATCH 10/23] Add ForecastDailyListViewModel to handle forecast daily feature --- Weather.xcodeproj/project.pbxproj | 28 +++++---- Weather/Base.lproj/Main.storyboard | 27 +++++---- Weather/Constants.swift | 3 +- Weather/Model/ForecastDaily.swift | 2 +- .../Networking/Manager/NetworkManager.swift | 22 +++++++ Weather/Utils/DateConverter.swift | 58 +++++++++++++++++++ Weather/View/Custom/DailyForecastView.swift | 8 +++ Weather/View/ForecastCollectionView.swift | 2 +- Weather/View/ForecastDailyStackView.swift | 49 ++++++++++++++++ Weather/View/WeatherViewController.swift | 13 ++++- .../ForecastDailyListViewModel.swift | 44 ++++++++++++++ .../ViewModel/ForecastDailyViewModel.swift | 22 +++++++ Weather/ViewModel/ForecastListViewModel.swift | 4 ++ Weather/ViewModel/WeatherViewModel.swift | 8 ++- 14 files changed, 259 insertions(+), 31 deletions(-) create mode 100644 Weather/Utils/DateConverter.swift create mode 100644 Weather/View/ForecastDailyStackView.swift create mode 100644 Weather/ViewModel/ForecastDailyListViewModel.swift create mode 100644 Weather/ViewModel/ForecastDailyViewModel.swift diff --git a/Weather.xcodeproj/project.pbxproj b/Weather.xcodeproj/project.pbxproj index a98c6ceb..2861758d 100644 --- a/Weather.xcodeproj/project.pbxproj +++ b/Weather.xcodeproj/project.pbxproj @@ -28,9 +28,9 @@ AA44BC9B2190E2910069B282 /* ForecastCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BC9A2190E2910069B282 /* ForecastCollectionView.swift */; }; AA44BC9D2190E4F80069B282 /* ForecastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BC9C2190E4F80069B282 /* ForecastViewModel.swift */; }; AA44BCA02190E7D70069B282 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BC9F2190E7D70069B282 /* StringExtension.swift */; }; - AA44BCA22190F2DD0069B282 /* DailyForecastStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BCA12190F2DD0069B282 /* DailyForecastStackView.swift */; }; - AA44BCA42190F31B0069B282 /* DailyForecastListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BCA32190F31B0069B282 /* DailyForecastListViewModel.swift */; }; - AA44BCA62190F3700069B282 /* DailyForecastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BCA52190F3700069B282 /* DailyForecastViewModel.swift */; }; + AA44BCA22190F2DD0069B282 /* ForecastDailyStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BCA12190F2DD0069B282 /* ForecastDailyStackView.swift */; }; + AA44BCA42190F31B0069B282 /* ForecastDailyListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BCA32190F31B0069B282 /* ForecastDailyListViewModel.swift */; }; + AA44BCA62190F3700069B282 /* ForecastDailyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BCA52190F3700069B282 /* ForecastDailyViewModel.swift */; }; AA4528C5218D9EEC00EC0901 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C4218D9EEB00EC0901 /* Constants.swift */; }; AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */; }; AA4528CB218D9FE900EC0901 /* DailyForecastView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */; }; @@ -39,6 +39,7 @@ AA4528D1218DB19700EC0901 /* InfoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA4528D0218DB19700EC0901 /* InfoView.xib */; }; AA60F075218EC7D20080F2C7 /* WeatherViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */; }; AAC62752218EAF7600A221C0 /* Weather.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC62751218EAF7600A221C0 /* Weather.swift */; }; + AAE4A44721923DF70068D8F1 /* DateConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE4A44621923DF70068D8F1 /* DateConverter.swift */; }; AAF473082190804000D09B00 /* Forecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF473072190804000D09B00 /* Forecast.swift */; }; AAF4730A2190830400D09B00 /* MiscInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF473092190830400D09B00 /* MiscInfo.swift */; }; AAF4730C2190851300D09B00 /* ForecastDaily.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF4730B2190851300D09B00 /* ForecastDaily.swift */; }; @@ -81,9 +82,9 @@ AA44BC9A2190E2910069B282 /* ForecastCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastCollectionView.swift; sourceTree = ""; }; AA44BC9C2190E4F80069B282 /* ForecastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastViewModel.swift; sourceTree = ""; }; AA44BC9F2190E7D70069B282 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; - AA44BCA12190F2DD0069B282 /* DailyForecastStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyForecastStackView.swift; sourceTree = ""; }; - AA44BCA32190F31B0069B282 /* DailyForecastListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyForecastListViewModel.swift; sourceTree = ""; }; - AA44BCA52190F3700069B282 /* DailyForecastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyForecastViewModel.swift; sourceTree = ""; }; + AA44BCA12190F2DD0069B282 /* ForecastDailyStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDailyStackView.swift; sourceTree = ""; }; + AA44BCA32190F31B0069B282 /* ForecastDailyListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDailyListViewModel.swift; sourceTree = ""; }; + AA44BCA52190F3700069B282 /* ForecastDailyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDailyViewModel.swift; sourceTree = ""; }; AA4528C4218D9EEB00EC0901 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; AA4528C6218D9F9000EC0901 /* DailyForecastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyForecastView.swift; sourceTree = ""; }; AA4528CA218D9FE800EC0901 /* DailyForecastView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DailyForecastView.xib; sourceTree = ""; }; @@ -92,6 +93,7 @@ AA4528D0218DB19700EC0901 /* InfoView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InfoView.xib; sourceTree = ""; }; AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModel.swift; sourceTree = ""; }; AAC62751218EAF7600A221C0 /* Weather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weather.swift; sourceTree = ""; }; + AAE4A44621923DF70068D8F1 /* DateConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateConverter.swift; sourceTree = ""; }; AAF473072190804000D09B00 /* Forecast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Forecast.swift; sourceTree = ""; }; AAF473092190830400D09B00 /* MiscInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiscInfo.swift; sourceTree = ""; }; AAF4730B2190851300D09B00 /* ForecastDaily.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDaily.swift; sourceTree = ""; }; @@ -214,6 +216,7 @@ isa = PBXGroup; children = ( AA44BC9E2190E7C10069B282 /* Extension */, + AAE4A44621923DF70068D8F1 /* DateConverter.swift */, ); path = Utils; sourceTree = ""; @@ -246,7 +249,7 @@ AA4528C8218D9F9C00EC0901 /* Custom */, AA3006A4218CE3D400A23A36 /* WeatherViewController.swift */, AA44BC9A2190E2910069B282 /* ForecastCollectionView.swift */, - AA44BCA12190F2DD0069B282 /* DailyForecastStackView.swift */, + AA44BCA12190F2DD0069B282 /* ForecastDailyStackView.swift */, ); path = View; sourceTree = ""; @@ -257,8 +260,8 @@ AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */, AA3685C1218F19D6002CD889 /* ForecastListViewModel.swift */, AA44BC9C2190E4F80069B282 /* ForecastViewModel.swift */, - AA44BCA32190F31B0069B282 /* DailyForecastListViewModel.swift */, - AA44BCA52190F3700069B282 /* DailyForecastViewModel.swift */, + AA44BCA32190F31B0069B282 /* ForecastDailyListViewModel.swift */, + AA44BCA52190F3700069B282 /* ForecastDailyViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -413,15 +416,16 @@ AA4528C5218D9EEC00EC0901 /* Constants.swift in Sources */, AAF4730C2190851300D09B00 /* ForecastDaily.swift in Sources */, AA316FA3218DD78B00DD1794 /* LocationManager.swift in Sources */, + AAE4A44721923DF70068D8F1 /* DateConverter.swift in Sources */, AA4528C7218D9F9000EC0901 /* DailyForecastView.swift in Sources */, AA60F075218EC7D20080F2C7 /* WeatherViewModel.swift in Sources */, - AA44BCA22190F2DD0069B282 /* DailyForecastStackView.swift in Sources */, - AA44BCA62190F3700069B282 /* DailyForecastViewModel.swift in Sources */, + AA44BCA22190F2DD0069B282 /* ForecastDailyStackView.swift in Sources */, + AA44BCA62190F3700069B282 /* ForecastDailyViewModel.swift in Sources */, AA4528CF218DB18400EC0901 /* InfoView.swift in Sources */, AA4528CD218DA5FD00EC0901 /* HourlyForecastCell.swift in Sources */, AA44BC9D2190E4F80069B282 /* ForecastViewModel.swift in Sources */, AAF473082190804000D09B00 /* Forecast.swift in Sources */, - AA44BCA42190F31B0069B282 /* DailyForecastListViewModel.swift in Sources */, + AA44BCA42190F31B0069B282 /* ForecastDailyListViewModel.swift in Sources */, AA0E4B04218E940500379569 /* WeatherEndPoint.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Weather/Base.lproj/Main.storyboard b/Weather/Base.lproj/Main.storyboard index 9b881b70..db7cd1bb 100644 --- a/Weather/Base.lproj/Main.storyboard +++ b/Weather/Base.lproj/Main.storyboard @@ -143,23 +143,24 @@ - + - + + - + - - + + - - + + - + - - - - @@ -251,16 +248,18 @@ - + + + diff --git a/Weather/Constants.swift b/Weather/Constants.swift index de026431..95ba7148 100644 --- a/Weather/Constants.swift +++ b/Weather/Constants.swift @@ -10,5 +10,6 @@ import Foundation struct Constants { - static let apiKey = "" + static let apiKey = "62fc4256-8f8c-11e5-8994-feff819cdc9f" + static let networkEnvironment: NetworkEnvironment = .production } diff --git a/Weather/Model/ForecastDaily.swift b/Weather/Model/ForecastDaily.swift index 40887a91..cfe54e78 100644 --- a/Weather/Model/ForecastDaily.swift +++ b/Weather/Model/ForecastDaily.swift @@ -23,7 +23,7 @@ struct ForecastDaily: Codable { let speed: Double let degree: Double let clouds: Double - let rain: Double + let rain: Double? enum CodingKeys: String, CodingKey { case dayTime = "dt" diff --git a/Weather/Service/Networking/Manager/NetworkManager.swift b/Weather/Service/Networking/Manager/NetworkManager.swift index ef4495c9..72e31298 100755 --- a/Weather/Service/Networking/Manager/NetworkManager.swift +++ b/Weather/Service/Networking/Manager/NetworkManager.swift @@ -35,6 +35,8 @@ protocol WeatherNetworkManager { func fetchWeather(latitude: Double, longitude: Double, completion: @escaping (_ response: WeatherResponse?,_ error: String?)->()) func fetchForecast(latitude: Double, longitude: Double, completion: @escaping (_ response: ForecastResponse?,_ error: String?)->()) + + func fetchForecastDaily(latitude: Double, longitude: Double, completion: @escaping (_ response: ForecastDailyResponse?,_ error: String?)->()) } class NetworkManager: WeatherNetworkManager { @@ -93,6 +95,26 @@ class NetworkManager: WeatherNetworkManager { } } + func fetchForecastDaily(latitude: Double, longitude: Double, completion: @escaping (_ response: ForecastDailyResponse?,_ error: String?)->()) { + router.request(.forecastDaily(latitude: Float(latitude), longitude: Float(longitude), apiKey: apiKey)) { data, response, error in + let result = self.handleNetworkResponse(data: data, response: response, error: error) + switch result{ + case .success: + do { + /*let jsonData = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) + print(jsonData)*/ + let forecastDailyResponse = try JSONDecoder().decode(ForecastDailyResponse.self, from: data!) + completion(forecastDailyResponse, nil) + }catch { + print(error) + completion(nil, NetworkResponse.unableToDecode.rawValue) + } + case .failure(let error): + completion(nil, error) + } + } + } + func cancelLastRequest() { router.cancel() } diff --git a/Weather/Utils/DateConverter.swift b/Weather/Utils/DateConverter.swift new file mode 100644 index 00000000..56505201 --- /dev/null +++ b/Weather/Utils/DateConverter.swift @@ -0,0 +1,58 @@ +// +// DateConverter.swift +// Weather +// +// Created by Christian Huang on 07/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +class DateConverter { + + static func timeIntervalToDayString(_ time: TimeInterval) -> String{ + let date = Date(timeIntervalSince1970: time) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEEE" + return dateFormatter.string(from: date) + } + + static func timeIntervalToHourString(_ time: TimeInterval) -> String{ + let date = Date(timeIntervalSince1970: time) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH" + return dateFormatter.string(from: date) + } + + static func timeIntervalToHourMinuteString(_ time: TimeInterval) -> String{ + let date = Date(timeIntervalSince1970: time) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH':'mm" + return dateFormatter.string(from: date) + } + + static func stringToDate(dateString: String, timezone: String = "CEST") -> Date? { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZZZ" + dateFormatter.timeZone = TimeZone(abbreviation: timezone) + let date = dateFormatter.date(from: dateString) + return date + } + + static func dateToDateString(date: Date, timezone: String = "CEST") -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "E. dd.MM" + dateFormatter.timeZone = TimeZone(abbreviation: timezone) + + return dateFormatter.string(from:date) + } + + static func dateToTimeString(date: Date, timezone: String = "CEST") -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "hh.mm" + dateFormatter.timeZone = TimeZone(abbreviation: timezone) + + return dateFormatter.string(from:date) + } +} diff --git a/Weather/View/Custom/DailyForecastView.swift b/Weather/View/Custom/DailyForecastView.swift index 1db29373..1131f076 100644 --- a/Weather/View/Custom/DailyForecastView.swift +++ b/Weather/View/Custom/DailyForecastView.swift @@ -36,5 +36,13 @@ class DailyForecastView: UIView { maxLabel.text = "\(Int(weatherViewModel.temperatureMax.value))°" minLabel.text = "\(Int(weatherViewModel.temperatureMin.value))°" } + + func configureTodayView(forecastDailyViewModel: ForecastDailyViewModel) { + + dayLabel.text = forecastDailyViewModel.dayString + todayLabel.alpha = 0 + maxLabel.text = "\(Int(forecastDailyViewModel.temperatureMax))°" + minLabel.text = "\(Int(forecastDailyViewModel.temperatureMin))°" + } } diff --git a/Weather/View/ForecastCollectionView.swift b/Weather/View/ForecastCollectionView.swift index b86671c4..b0cf617a 100644 --- a/Weather/View/ForecastCollectionView.swift +++ b/Weather/View/ForecastCollectionView.swift @@ -34,7 +34,7 @@ extension ForecastCollectionView: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "hourlyForecastCellIdentifier", for: indexPath) as! HourlyForecastCell - let forecastViewModel = forecastListViewModel.forecastList.value[indexPath.item] + let forecastViewModel = forecastListViewModel.getForecastViewModel(indexPath: indexPath) cell.configureCell(forecastViewModel: forecastViewModel, isNow: indexPath.item == 0) return cell } diff --git a/Weather/View/ForecastDailyStackView.swift b/Weather/View/ForecastDailyStackView.swift new file mode 100644 index 00000000..891fa955 --- /dev/null +++ b/Weather/View/ForecastDailyStackView.swift @@ -0,0 +1,49 @@ +// +// ForecastDailyStackView.swift +// Weather +// +// Created by Christian Huang on 06/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class ForecastDailyStackView: UIStackView { + + var forecastDailyListViewModel = ForecastDailyListViewModel() + +} + +//MARK:- ViewModel related +extension ForecastDailyStackView { + func initViewModel() { + forecastDailyListViewModel.forecastDailyList.listener = { forecastDailyList in + DispatchQueue.main.async { + self.reloadData() + } + } + } +} + +extension ForecastDailyStackView { + private func reloadData() { + let count = forecastDailyListViewModel.forecastDailyCount + + if count > self.arrangedSubviews.count { + for _ in self.arrangedSubviews.count.. + } +} + diff --git a/Weather/ViewModel/ForecastDailyListViewModel.swift b/Weather/ViewModel/ForecastDailyListViewModel.swift new file mode 100644 index 00000000..be50bf62 --- /dev/null +++ b/Weather/ViewModel/ForecastDailyListViewModel.swift @@ -0,0 +1,44 @@ +// +// ForecastDailyListViewModel.swift +// Weather +// +// Created by Christian Huang on 06/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +class ForecastDailyListViewModel: NSObject { + var forecastDailyList: Dynamic<[ForecastDailyViewModel]> = Dynamic([]) + var forecastDailyCount: Int { + return forecastDailyList.value.count + } + + var networkManager: WeatherNetworkManager! +} + +extension ForecastDailyListViewModel { + func fetchForecastDaily(coordinate: Coordinate) { + networkManager.fetchForecastDaily(latitude: coordinate.latitude, longitude: coordinate.longitude) { (forecastDailyResponse, error) in + if let error = error { + print("Network Error: \(error)") + } else { + self.processForecastDailyResponse(forecastDailyResponse!) + } + } + } + + func getForecastDailyViewModel(index: Int) -> ForecastDailyViewModel { + return forecastDailyList.value[index] + } +} + +extension ForecastDailyListViewModel { + private func processForecastDailyResponse(_ forecastDailyResponse: ForecastDailyResponse) { + forecastDailyList.value = forecastDailyResponse.list.map({ forecastDaily -> ForecastDailyViewModel in + return ForecastDailyViewModel(forecastDaily: forecastDaily) + }) + + } +} diff --git a/Weather/ViewModel/ForecastDailyViewModel.swift b/Weather/ViewModel/ForecastDailyViewModel.swift new file mode 100644 index 00000000..f6a4cbbd --- /dev/null +++ b/Weather/ViewModel/ForecastDailyViewModel.swift @@ -0,0 +1,22 @@ +// +// ForecastDailyViewModel.swift +// Weather +// +// Created by Christian Huang on 06/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import Foundation + + +class ForecastDailyViewModel: NSObject { + let dayString: String + let temperatureMax: Double + let temperatureMin: Double + + init(forecastDaily: ForecastDaily) { + dayString = DateConverter.timeIntervalToDayString(forecastDaily.dayTime) + temperatureMax = forecastDaily.temperature.max + temperatureMin = forecastDaily.temperature.min + } +} diff --git a/Weather/ViewModel/ForecastListViewModel.swift b/Weather/ViewModel/ForecastListViewModel.swift index e0d5b09d..d0e08b72 100644 --- a/Weather/ViewModel/ForecastListViewModel.swift +++ b/Weather/ViewModel/ForecastListViewModel.swift @@ -29,6 +29,10 @@ extension ForecastListViewModel { } } } + + func getForecastViewModel(indexPath: IndexPath) -> ForecastViewModel { + return forecastList.value[indexPath.row] + } } extension ForecastListViewModel { diff --git a/Weather/ViewModel/WeatherViewModel.swift b/Weather/ViewModel/WeatherViewModel.swift index f3bfbdd0..bc6c52d4 100644 --- a/Weather/ViewModel/WeatherViewModel.swift +++ b/Weather/ViewModel/WeatherViewModel.swift @@ -28,11 +28,16 @@ class WeatherViewModel: NSObject { lazy var networkManager: WeatherNetworkManager = NetworkManager(apiKey: Constants.apiKey, environment: Constants.networkEnvironment) private var locationManager = LocationManager() - var forecastListViewModel: ForecastListViewModel? { + weak var forecastListViewModel: ForecastListViewModel? { didSet { forecastListViewModel?.networkManager = networkManager } } + weak var forecastDailyListViewModel: ForecastDailyListViewModel? { + didSet { + forecastDailyListViewModel?.networkManager = networkManager + } + } var updateWeatherDataClosure: (()->())? var finishedFetchingWeatherClosure: (()->())? @@ -59,6 +64,7 @@ extension WeatherViewModel { self.location = Coordinate(longitude: location.coordinate.longitude, latitude: location.coordinate.latitude) self.fetchWeather() self.forecastListViewModel?.fetchForecast(coordinate: self.location!) + self.forecastDailyListViewModel?.fetchForecastDaily(coordinate: self.location!) } else { // cannot get user location } From 7982b06bd4c3d2f1d4c29ae735c652847af1eab1 Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Thu, 8 Nov 2018 05:01:01 +0700 Subject: [PATCH 11/23] Add scroll effect feature --- Weather/Base.lproj/Main.storyboard | 3 +++ Weather/View/WeatherViewController.swift | 34 +++++++++++++++++++++++- Weather/ViewModel/WeatherViewModel.swift | 6 ++--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Weather/Base.lproj/Main.storyboard b/Weather/Base.lproj/Main.storyboard index db7cd1bb..87e2a323 100644 --- a/Weather/Base.lproj/Main.storyboard +++ b/Weather/Base.lproj/Main.storyboard @@ -187,6 +187,9 @@ + + + diff --git a/Weather/View/WeatherViewController.swift b/Weather/View/WeatherViewController.swift index 41ff0dab..bf12a9e8 100644 --- a/Weather/View/WeatherViewController.swift +++ b/Weather/View/WeatherViewController.swift @@ -28,6 +28,12 @@ class WeatherViewController: UIViewController { @IBOutlet private weak var loadingView: UIActivityIndicatorView! private var weatherViewModel = WeatherViewModel() + private var headerViewTopDefault: CGFloat = 0 + private var todayViewTopDefault: CGFloat = 0 + private var totalTopShift: CGFloat = 0 + private var headerViewTopRatio: CGFloat = 0 + private var todayViewTopRatio: CGFloat = 0 + private var scrollViewOffset = CGPoint.zero override func viewDidLoad() { super.viewDidLoad() @@ -81,6 +87,7 @@ extension WeatherViewController { //MARK:- private func extension WeatherViewController { private func setupUI() { + self.view.layoutIfNeeded() self.view.insertSubview(todayForecastView, belowSubview: todayView) sunriseSunsetView.configureInfoView(title1: "Sunrise", title2: "Sunset") infoStackView.addArrangedSubview(sunriseSunsetView) @@ -92,6 +99,12 @@ extension WeatherViewController { todayForecastView.centerXAnchor.constraint(equalTo: view.centerXAnchor), todayForecastView.bottomAnchor.constraint(equalTo: todayView.topAnchor) ]) + + headerViewTopDefault = headerViewTopConstraint.constant + todayViewTopDefault = todayViewTopConstraint.constant + totalTopShift = headerViewTopDefault + todayViewTopDefault + headerViewTopRatio = headerViewTopDefault / totalTopShift + todayViewTopRatio = todayViewTopDefault / totalTopShift } private func showLoadingView(){ loadingBGView.backgroundColor = view.backgroundColor @@ -112,7 +125,26 @@ extension WeatherViewController { //MARK:- ScrollView Delegate extension WeatherViewController: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { - <#code#> + var offset = scrollView.contentOffset + if (offset.y > 0 && todayViewTopConstraint.constant > 0) || + (offset.y < 0 && todayViewTopConstraint.constant < todayViewTopDefault) { + let shift = offset.y + headerViewTopConstraint.constant -= shift * headerViewTopRatio + todayViewTopConstraint.constant -= shift * todayViewTopRatio + headerViewTopConstraint.constant = max(0, min(headerViewTopDefault, headerViewTopConstraint.constant)) + todayViewTopConstraint.constant = max(0, min(todayViewTopDefault, todayViewTopConstraint.constant)) + + self.view.layoutIfNeeded() + + let maxValue = (todayViewTopDefault / 2) + var alpha = (todayViewTopConstraint.constant - maxValue) / maxValue + alpha = max(0, alpha) + temperatureLabel.alpha = alpha + todayForecastView.alpha = alpha + + offset.y = 0 + scrollView.contentOffset = offset + } } } diff --git a/Weather/ViewModel/WeatherViewModel.swift b/Weather/ViewModel/WeatherViewModel.swift index bc6c52d4..f259bbc1 100644 --- a/Weather/ViewModel/WeatherViewModel.swift +++ b/Weather/ViewModel/WeatherViewModel.swift @@ -95,11 +95,11 @@ extension WeatherViewModel { self.summary.value = String(format: "Today: %@ currently. It's %d°; the high today was forecast as %d°.", response.weather[0].main, Int(response.main.temperature), Int(response.main.temperatureMax)) self.dayTime.value = .night - self.dayString.value = "Sunday" + self.dayString.value = DateConverter.timeIntervalToDayString(response.dayTime) self.temperatureMin.value = response.main.temperatureMin self.temperatureMax.value = response.main.temperatureMax - self.sunrise.value = "06:00" - self.sunset.value = "17:45" + self.sunrise.value = DateConverter.timeIntervalToHourMinuteString(response.system.sunrise) + self.sunset.value = DateConverter.timeIntervalToHourMinuteString(response.system.sunset) self.pressure.value = String(format: "%.2f hPa", response.main.pressure) self.humidity.value = String(format: "%d%", response.main.humidity) self.updateWeatherDataClosure?() From 485dbb1dd8c37feda790a952bee3bdee7c661f90 Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Sat, 10 Nov 2018 15:09:14 +0700 Subject: [PATCH 12/23] Add cocoapod for Kingfisher --- Podfile | 16 ++ Weather.xcodeproj/project.pbxproj | 210 ++++++++++++++++-- .../xcschemes/xcschememanagement.plist | 2 +- Weather.xcworkspace/contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + Weather/Base.lproj/Main.storyboard | 14 ++ Weather/Constants.swift | 2 +- Weather/Model/ForecastDaily.swift | 2 +- Weather/ViewModel/ForecastViewModel.swift | 2 + Weather/ViewModel/WeatherViewModel.swift | 2 + 10 files changed, 249 insertions(+), 19 deletions(-) create mode 100644 Podfile create mode 100644 Weather.xcworkspace/contents.xcworkspacedata create mode 100644 Weather.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Podfile b/Podfile new file mode 100644 index 00000000..d01acf46 --- /dev/null +++ b/Podfile @@ -0,0 +1,16 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'Weather' do + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + use_frameworks! + + # Pods for Weather + pod 'Kingfisher', '~> 4.0' + + target 'WeatherTests' do + inherit! :search_paths + # Pods for testing + end + +end diff --git a/Weather.xcodeproj/project.pbxproj b/Weather.xcodeproj/project.pbxproj index 2861758d..19338ad1 100644 --- a/Weather.xcodeproj/project.pbxproj +++ b/Weather.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 0590753E29507F710CF3CED8 /* Pods_WeatherTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD1A25CF54C2CDB0077D7291 /* Pods_WeatherTests.framework */; }; + 158601774FA4161333DFB17B /* Pods_Weather.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9E3500245BE8843B5CF5268 /* Pods_Weather.framework */; }; AA0E4B01218E940500379569 /* JSONParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF4218E940500379569 /* JSONParameterEncoder.swift */; }; AA0E4B02218E940500379569 /* ParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF5218E940500379569 /* ParameterEncoding.swift */; }; AA0E4B03218E940500379569 /* URLParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF6218E940500379569 /* URLParameterEncoding.swift */; }; @@ -25,6 +27,12 @@ AA3006B8218CE3DD00A23A36 /* WeatherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */; }; AA316FA3218DD78B00DD1794 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA316FA2218DD78B00DD1794 /* LocationManager.swift */; }; AA3685C2218F19D6002CD889 /* ForecastListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3685C1218F19D6002CD889 /* ForecastListViewModel.swift */; }; + AA4243BF2194DF140030A3B3 /* CoreImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA4243B92194DF130030A3B3 /* CoreImage.framework */; }; + AA4243C02194DF140030A3B3 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = AA4243BA2194DF130030A3B3 /* libxml2.tbd */; }; + AA4243C12194DF140030A3B3 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA4243BB2194DF130030A3B3 /* QuartzCore.framework */; }; + AA4243C22194DF140030A3B3 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA4243BC2194DF130030A3B3 /* UIKit.framework */; }; + AA4243C32194DF140030A3B3 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA4243BD2194DF140030A3B3 /* CoreGraphics.framework */; }; + AA4243C42194DF140030A3B3 /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA4243BE2194DF140030A3B3 /* CoreText.framework */; }; AA44BC9B2190E2910069B282 /* ForecastCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BC9A2190E2910069B282 /* ForecastCollectionView.swift */; }; AA44BC9D2190E4F80069B282 /* ForecastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BC9C2190E4F80069B282 /* ForecastViewModel.swift */; }; AA44BCA02190E7D70069B282 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44BC9F2190E7D70069B282 /* StringExtension.swift */; }; @@ -56,7 +64,23 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + AA4243D02194DFF10030A3B3 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ + 8EFD89BC805FB41F6EE88D57 /* Pods-Weather.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Weather.release.xcconfig"; path = "Pods/Target Support Files/Pods-Weather/Pods-Weather.release.xcconfig"; sourceTree = ""; }; + 9089C580DD3FE849E3D674CC /* Pods-WeatherTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WeatherTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-WeatherTests/Pods-WeatherTests.debug.xcconfig"; sourceTree = ""; }; + 9C41227BF3B0CE586CCEA861 /* Pods-WeatherTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WeatherTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-WeatherTests/Pods-WeatherTests.release.xcconfig"; sourceTree = ""; }; AA0E4AF4218E940500379569 /* JSONParameterEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONParameterEncoder.swift; sourceTree = ""; }; AA0E4AF5218E940500379569 /* ParameterEncoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParameterEncoding.swift; sourceTree = ""; }; AA0E4AF6218E940500379569 /* URLParameterEncoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLParameterEncoding.swift; sourceTree = ""; }; @@ -79,6 +103,12 @@ AA3006B9218CE3DD00A23A36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AA316FA2218DD78B00DD1794 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; AA3685C1218F19D6002CD889 /* ForecastListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastListViewModel.swift; sourceTree = ""; }; + AA4243B92194DF130030A3B3 /* CoreImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreImage.framework; path = System/Library/Frameworks/CoreImage.framework; sourceTree = SDKROOT; }; + AA4243BA2194DF130030A3B3 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; }; + AA4243BB2194DF130030A3B3 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + AA4243BC2194DF130030A3B3 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + AA4243BD2194DF140030A3B3 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + AA4243BE2194DF140030A3B3 /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; AA44BC9A2190E2910069B282 /* ForecastCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastCollectionView.swift; sourceTree = ""; }; AA44BC9C2190E4F80069B282 /* ForecastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastViewModel.swift; sourceTree = ""; }; AA44BC9F2190E7D70069B282 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; @@ -98,6 +128,9 @@ AAF473092190830400D09B00 /* MiscInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiscInfo.swift; sourceTree = ""; }; AAF4730B2190851300D09B00 /* ForecastDaily.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDaily.swift; sourceTree = ""; }; AAFF64E1218EF84500C512B8 /* Dynamic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dynamic.swift; sourceTree = ""; }; + B9E3500245BE8843B5CF5268 /* Pods_Weather.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Weather.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DD1A25CF54C2CDB0077D7291 /* Pods_WeatherTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WeatherTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FB3BD15C15224C07D1446635 /* Pods-Weather.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Weather.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Weather/Pods-Weather.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -105,6 +138,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + AA4243BF2194DF140030A3B3 /* CoreImage.framework in Frameworks */, + AA4243C02194DF140030A3B3 /* libxml2.tbd in Frameworks */, + AA4243C12194DF140030A3B3 /* QuartzCore.framework in Frameworks */, + AA4243C22194DF140030A3B3 /* UIKit.framework in Frameworks */, + AA4243C32194DF140030A3B3 /* CoreGraphics.framework in Frameworks */, + AA4243C42194DF140030A3B3 /* CoreText.framework in Frameworks */, + 158601774FA4161333DFB17B /* Pods_Weather.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -112,12 +152,24 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 0590753E29507F710CF3CED8 /* Pods_WeatherTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 789780D72854A0B8E4D49467 /* Pods */ = { + isa = PBXGroup; + children = ( + FB3BD15C15224C07D1446635 /* Pods-Weather.debug.xcconfig */, + 8EFD89BC805FB41F6EE88D57 /* Pods-Weather.release.xcconfig */, + 9089C580DD3FE849E3D674CC /* Pods-WeatherTests.debug.xcconfig */, + 9C41227BF3B0CE586CCEA861 /* Pods-WeatherTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; AA0E4AF2218E940500379569 /* Networking */ = { isa = PBXGroup; children = ( @@ -173,6 +225,8 @@ AA3006A1218CE3D400A23A36 /* Weather */, AA3006B6218CE3DD00A23A36 /* WeatherTests */, AA3006A0218CE3D400A23A36 /* Products */, + AA4243B82194DF130030A3B3 /* Frameworks */, + 789780D72854A0B8E4D49467 /* Pods */, ); sourceTree = ""; }; @@ -266,6 +320,21 @@ path = ViewModel; sourceTree = ""; }; + AA4243B82194DF130030A3B3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + AA4243BD2194DF140030A3B3 /* CoreGraphics.framework */, + AA4243B92194DF130030A3B3 /* CoreImage.framework */, + AA4243BE2194DF140030A3B3 /* CoreText.framework */, + AA4243BA2194DF130030A3B3 /* libxml2.tbd */, + AA4243BB2194DF130030A3B3 /* QuartzCore.framework */, + AA4243BC2194DF130030A3B3 /* UIKit.framework */, + B9E3500245BE8843B5CF5268 /* Pods_Weather.framework */, + DD1A25CF54C2CDB0077D7291 /* Pods_WeatherTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; AA44BC9E2190E7C10069B282 /* Extension */ = { isa = PBXGroup; children = ( @@ -300,9 +369,13 @@ isa = PBXNativeTarget; buildConfigurationList = AA3006BC218CE3DD00A23A36 /* Build configuration list for PBXNativeTarget "Weather" */; buildPhases = ( + C59C8281D6BBDBB7CC7893B8 /* [CP] Check Pods Manifest.lock */, AA30069B218CE3D400A23A36 /* Sources */, AA30069C218CE3D400A23A36 /* Frameworks */, AA30069D218CE3D400A23A36 /* Resources */, + AA4243D02194DFF10030A3B3 /* Embed Frameworks */, + AD01839882209B46949BE2F3 /* [CP] Embed Pods Frameworks */, + 2EC49454CADDC3B0895C0223 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -317,9 +390,12 @@ isa = PBXNativeTarget; buildConfigurationList = AA3006BF218CE3DD00A23A36 /* Build configuration list for PBXNativeTarget "WeatherTests" */; buildPhases = ( + EE1C1927C10FAB21B0C98122 /* [CP] Check Pods Manifest.lock */, AA3006AF218CE3DD00A23A36 /* Sources */, AA3006B0218CE3DD00A23A36 /* Frameworks */, AA3006B1218CE3DD00A23A36 /* Resources */, + 0D4E8826DC9F48538C7E9A22 /* [CP] Embed Pods Frameworks */, + 17BD24C2EA7815C78DC25A5F /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -391,6 +467,108 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 0D4E8826DC9F48538C7E9A22 /* [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-WeatherTests/Pods-WeatherTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 17BD24C2EA7815C78DC25A5F /* [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-WeatherTests/Pods-WeatherTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 2EC49454CADDC3B0895C0223 /* [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-Weather/Pods-Weather-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + AD01839882209B46949BE2F3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Weather/Pods-Weather-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Weather/Pods-Weather-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + C59C8281D6BBDBB7CC7893B8 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Weather-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + EE1C1927C10FAB21B0C98122 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-WeatherTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ AA30069B218CE3D400A23A36 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -586,15 +764,18 @@ }; AA3006BD218CE3DD00A23A36 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = FB3BD15C15224C07D1446635 /* Pods-Weather.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7X5UG5NGMS; - INFOPLIST_FILE = Weather/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( + FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "@executable_path/Frameworks", + "$(PROJECT_DIR)$(LOCAL_LIBRARY_DIR)", + "$(PROJECT_DIR)$(LOCAL_LIBRARY_DIR)/CocoaLumberjack", ); + INFOPLIST_FILE = Weather/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = net.inovidea.Weather; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.2; @@ -604,15 +785,18 @@ }; AA3006BE218CE3DD00A23A36 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 8EFD89BC805FB41F6EE88D57 /* Pods-Weather.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7X5UG5NGMS; - INFOPLIST_FILE = Weather/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( + FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "@executable_path/Frameworks", + "$(PROJECT_DIR)$(LOCAL_LIBRARY_DIR)", + "$(PROJECT_DIR)$(LOCAL_LIBRARY_DIR)/CocoaLumberjack", ); + INFOPLIST_FILE = Weather/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = net.inovidea.Weather; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.2; @@ -622,17 +806,14 @@ }; AA3006C0218CE3DD00A23A36 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9089C580DD3FE849E3D674CC /* Pods-WeatherTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7X5UG5NGMS; INFOPLIST_FILE = WeatherTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = net.inovidea.WeatherTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.2; @@ -643,17 +824,14 @@ }; AA3006C1218CE3DD00A23A36 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9C41227BF3B0CE586CCEA861 /* Pods-WeatherTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7X5UG5NGMS; INFOPLIST_FILE = WeatherTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = net.inovidea.WeatherTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.2; diff --git a/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist b/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist index 51757c96..5ab3c8a6 100644 --- a/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Weather.xcodeproj/xcuserdata/christian.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ Weather.xcscheme_^#shared#^_ orderHint - 0 + 3 diff --git a/Weather.xcworkspace/contents.xcworkspacedata b/Weather.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..71d6a2f2 --- /dev/null +++ b/Weather.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Weather.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Weather.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Weather.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Weather/Base.lproj/Main.storyboard b/Weather/Base.lproj/Main.storyboard index 87e2a323..5569e6f0 100644 --- a/Weather/Base.lproj/Main.storyboard +++ b/Weather/Base.lproj/Main.storyboard @@ -269,5 +269,19 @@ + + + + + + + + + + + + + + diff --git a/Weather/Constants.swift b/Weather/Constants.swift index 95ba7148..31262d28 100644 --- a/Weather/Constants.swift +++ b/Weather/Constants.swift @@ -10,6 +10,6 @@ import Foundation struct Constants { - static let apiKey = "62fc4256-8f8c-11e5-8994-feff819cdc9f" + static let apiKey = "" static let networkEnvironment: NetworkEnvironment = .production } diff --git a/Weather/Model/ForecastDaily.swift b/Weather/Model/ForecastDaily.swift index cfe54e78..dc8ddfb3 100644 --- a/Weather/Model/ForecastDaily.swift +++ b/Weather/Model/ForecastDaily.swift @@ -18,7 +18,7 @@ struct ForecastDaily: Codable { let dayTime: TimeInterval let temperature: Temperature let pressure: Double - let humidity: Double + let humidity: Int let weather: [Weather] let speed: Double let degree: Double diff --git a/Weather/ViewModel/ForecastViewModel.swift b/Weather/ViewModel/ForecastViewModel.swift index 42ca5bd9..2ac9acf9 100644 --- a/Weather/ViewModel/ForecastViewModel.swift +++ b/Weather/ViewModel/ForecastViewModel.swift @@ -12,9 +12,11 @@ import Foundation class ForecastViewModel: NSObject { var time: String var temperature: String + var icon: String init(forecast: Forecast) { time = String(forecast.dayTimeText[11...12]) temperature = "\(Int(forecast.main.temperature))°" + icon = "http://openweathermap.org/img/w/\(forecast.weather[0].icon).png" } } diff --git a/Weather/ViewModel/WeatherViewModel.swift b/Weather/ViewModel/WeatherViewModel.swift index f259bbc1..44357fa4 100644 --- a/Weather/ViewModel/WeatherViewModel.swift +++ b/Weather/ViewModel/WeatherViewModel.swift @@ -23,6 +23,7 @@ class WeatherViewModel: NSObject { var sunset = Dynamic("00:00") var pressure = Dynamic("") var humidity = Dynamic("") + var icon = Dynamic("") lazy var networkManager: WeatherNetworkManager = NetworkManager(apiKey: Constants.apiKey, environment: Constants.networkEnvironment) @@ -102,6 +103,7 @@ extension WeatherViewModel { self.sunset.value = DateConverter.timeIntervalToHourMinuteString(response.system.sunset) self.pressure.value = String(format: "%.2f hPa", response.main.pressure) self.humidity.value = String(format: "%d%", response.main.humidity) + self.icon.value = "http://openweathermap.org/img/w/\(response.weather[0].icon).png" self.updateWeatherDataClosure?() self.finishedFetchingWeatherClosure?() } From f29d84e999f539c63d40b6a434bb4c2f1db54202 Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Sat, 10 Nov 2018 15:43:25 +0700 Subject: [PATCH 13/23] Add weather icons --- Weather/Base.lproj/Main.storyboard | 10 ++++++++++ Weather/View/Cell/HourlyForecastCell.swift | 3 +++ Weather/View/Custom/DailyForecastView.swift | 3 +++ Weather/View/Custom/DailyForecastView.xib | 12 ++++++++++-- Weather/ViewModel/ForecastDailyViewModel.swift | 2 ++ 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Weather/Base.lproj/Main.storyboard b/Weather/Base.lproj/Main.storyboard index 5569e6f0..a64c225f 100644 --- a/Weather/Base.lproj/Main.storyboard +++ b/Weather/Base.lproj/Main.storyboard @@ -89,6 +89,13 @@ + + + + + + + + + + diff --git a/Weather/View/Cell/HourlyForecastCell.swift b/Weather/View/Cell/ForecastCell.swift similarity index 67% rename from Weather/View/Cell/HourlyForecastCell.swift rename to Weather/View/Cell/ForecastCell.swift index 63eb2fde..655680e7 100644 --- a/Weather/View/Cell/HourlyForecastCell.swift +++ b/Weather/View/Cell/ForecastCell.swift @@ -1,5 +1,5 @@ // -// HourlyForecastCell.swift +// ForecastCell.swift // Weather // // Created by Christian Huang on 03/11/18. @@ -9,14 +9,19 @@ import UIKit import Kingfisher -class HourlyForecastCell: UICollectionViewCell { - @IBOutlet private weak var hourLabel: UILabel! +class ForecastCell: UICollectionViewCell { + @IBOutlet private weak var timeLabel: UILabel! @IBOutlet private weak var chanceLabel: UILabel! @IBOutlet private weak var weatherImageView: UIImageView! @IBOutlet private weak var temperatureLabel: UILabel! + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + func configureCell(forecastViewModel: ForecastViewModel, isNow: Bool = false) { - hourLabel.text = isNow ? "Now" : forecastViewModel.time + timeLabel.text = isNow ? "Now" : forecastViewModel.time chanceLabel.text = nil weatherImageView.kf.setImage(with: URL(string: forecastViewModel.icon)) temperatureLabel.text = forecastViewModel.temperature diff --git a/Weather/View/Cell/ForecastCell.xib b/Weather/View/Cell/ForecastCell.xib new file mode 100644 index 00000000..5c98074c --- /dev/null +++ b/Weather/View/Cell/ForecastCell.xib @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Weather/View/ForecastCollectionView.swift b/Weather/View/ForecastCollectionView.swift index b0cf617a..37032b5b 100644 --- a/Weather/View/ForecastCollectionView.swift +++ b/Weather/View/ForecastCollectionView.swift @@ -11,6 +11,11 @@ import UIKit class ForecastCollectionView: UICollectionView { var forecastListViewModel = ForecastListViewModel() + + override func awakeFromNib() { + super.awakeFromNib() + self.register(UINib(nibName: "ForecastCell", bundle: nil), forCellWithReuseIdentifier: "forecastCellIdentifier") + } } @@ -33,7 +38,7 @@ extension ForecastCollectionView: UICollectionViewDataSource { } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "hourlyForecastCellIdentifier", for: indexPath) as! HourlyForecastCell + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "forecastCellIdentifier", for: indexPath) as! ForecastCell let forecastViewModel = forecastListViewModel.getForecastViewModel(indexPath: indexPath) cell.configureCell(forecastViewModel: forecastViewModel, isNow: indexPath.item == 0) return cell diff --git a/Weather/View/MyWeatherViewController.swift b/Weather/View/MyWeatherViewController.swift new file mode 100644 index 00000000..23f444dc --- /dev/null +++ b/Weather/View/MyWeatherViewController.swift @@ -0,0 +1,30 @@ +// +// MyWeatherViewController.swift +// Weather +// +// Created by Christian Huang on 10/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +class MyWeatherViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} From fd18db4df27aec8253db1fcfcad89dc380fc7cd2 Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Sun, 11 Nov 2018 16:08:03 +0700 Subject: [PATCH 15/23] Link MyWeatherViewController with WeatherViewModel --- Weather.xcodeproj/project.pbxproj | 12 + Weather/Base.lproj/Main.storyboard | 248 ++++++++++++++---- Weather/Utils/DateConverter.swift | 6 + Weather/Utils/Extension/UIViewExtension.swift | 19 ++ Weather/View/Cell/ForecastCell.swift | 7 + Weather/View/Cell/ForecastCell.xib | 2 +- Weather/View/ForecastCollectionView.swift | 12 +- .../View/ForecastDailyCollectionView.swift | 53 ++++ Weather/View/MyWeatherViewController.swift | 90 ++++++- Weather/View/WeatherViewController.swift | 22 +- .../ForecastDailyListViewModel.swift | 7 + .../ViewModel/ForecastDailyViewModel.swift | 2 + Weather/ViewModel/ForecastListViewModel.swift | 12 +- Weather/ViewModel/ForecastViewModel.swift | 2 +- Weather/ViewModel/WeatherDetailProtocol.swift | 14 + Weather/ViewModel/WeatherViewModel.swift | 56 +++- 16 files changed, 491 insertions(+), 73 deletions(-) create mode 100644 Weather/Utils/Extension/UIViewExtension.swift create mode 100644 Weather/View/ForecastDailyCollectionView.swift create mode 100644 Weather/ViewModel/WeatherDetailProtocol.swift diff --git a/Weather.xcodeproj/project.pbxproj b/Weather.xcodeproj/project.pbxproj index 9c0e23bb..7314d707 100644 --- a/Weather.xcodeproj/project.pbxproj +++ b/Weather.xcodeproj/project.pbxproj @@ -50,6 +50,9 @@ AA60F075218EC7D20080F2C7 /* WeatherViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */; }; AAC62752218EAF7600A221C0 /* Weather.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC62751218EAF7600A221C0 /* Weather.swift */; }; AAE4A44721923DF70068D8F1 /* DateConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE4A44621923DF70068D8F1 /* DateConverter.swift */; }; + AAE8A3A22197E84700DF123F /* ForecastDailyCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8A3A12197E84700DF123F /* ForecastDailyCollectionView.swift */; }; + AAE8A3A42197EFCF00DF123F /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8A3A32197EFCF00DF123F /* UIViewExtension.swift */; }; + AAEF7DA1219808D400C5FDF8 /* WeatherDetailProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEF7DA0219808D400C5FDF8 /* WeatherDetailProtocol.swift */; }; AAF473082190804000D09B00 /* Forecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF473072190804000D09B00 /* Forecast.swift */; }; AAF4730A2190830400D09B00 /* MiscInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF473092190830400D09B00 /* MiscInfo.swift */; }; AAF4730C2190851300D09B00 /* ForecastDaily.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF4730B2190851300D09B00 /* ForecastDaily.swift */; }; @@ -128,6 +131,9 @@ AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModel.swift; sourceTree = ""; }; AAC62751218EAF7600A221C0 /* Weather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weather.swift; sourceTree = ""; }; AAE4A44621923DF70068D8F1 /* DateConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateConverter.swift; sourceTree = ""; }; + AAE8A3A12197E84700DF123F /* ForecastDailyCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDailyCollectionView.swift; sourceTree = ""; }; + AAE8A3A32197EFCF00DF123F /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; + AAEF7DA0219808D400C5FDF8 /* WeatherDetailProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherDetailProtocol.swift; sourceTree = ""; }; AAF473072190804000D09B00 /* Forecast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Forecast.swift; sourceTree = ""; }; AAF473092190830400D09B00 /* MiscInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiscInfo.swift; sourceTree = ""; }; AAF4730B2190851300D09B00 /* ForecastDaily.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDaily.swift; sourceTree = ""; }; @@ -307,6 +313,7 @@ AA4528C8218D9F9C00EC0901 /* Custom */, AA3006A4218CE3D400A23A36 /* WeatherViewController.swift */, AA44BC9A2190E2910069B282 /* ForecastCollectionView.swift */, + AAE8A3A12197E84700DF123F /* ForecastDailyCollectionView.swift */, AA44BCA12190F2DD0069B282 /* ForecastDailyStackView.swift */, AA38D29E2196D44900FAFFE3 /* MyWeatherViewController.swift */, ); @@ -316,6 +323,7 @@ AA3006C6218CF0BC00A23A36 /* ViewModel */ = { isa = PBXGroup; children = ( + AAEF7DA0219808D400C5FDF8 /* WeatherDetailProtocol.swift */, AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */, AA3685C1218F19D6002CD889 /* ForecastListViewModel.swift */, AA44BC9C2190E4F80069B282 /* ForecastViewModel.swift */, @@ -344,6 +352,7 @@ isa = PBXGroup; children = ( AA44BC9F2190E7D70069B282 /* StringExtension.swift */, + AAE8A3A32197EFCF00DF123F /* UIViewExtension.swift */, ); path = Extension; sourceTree = ""; @@ -586,8 +595,11 @@ AA44BC9B2190E2910069B282 /* ForecastCollectionView.swift in Sources */, AAFF64E2218EF84500C512B8 /* Dynamic.swift in Sources */, AA0E4B02218E940500379569 /* ParameterEncoding.swift in Sources */, + AAE8A3A22197E84700DF123F /* ForecastDailyCollectionView.swift in Sources */, AA44BCA02190E7D70069B282 /* StringExtension.swift in Sources */, + AAEF7DA1219808D400C5FDF8 /* WeatherDetailProtocol.swift in Sources */, AA3685C2218F19D6002CD889 /* ForecastListViewModel.swift in Sources */, + AAE8A3A42197EFCF00DF123F /* UIViewExtension.swift in Sources */, AA0E4B07218E940500379569 /* HTTPMethod.swift in Sources */, AA0E4B06218E940500379569 /* EndPointType.swift in Sources */, AAC62752218EAF7600A221C0 /* Weather.swift in Sources */, diff --git a/Weather/Base.lproj/Main.storyboard b/Weather/Base.lproj/Main.storyboard index c79b8e40..1044a9a0 100644 --- a/Weather/Base.lproj/Main.storyboard +++ b/Weather/Base.lproj/Main.storyboard @@ -165,12 +165,24 @@ + + + + + @@ -242,14 +254,14 @@ - + - + - + - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -273,8 +372,11 @@ - - + @@ -318,14 +420,14 @@ @@ -340,17 +442,17 @@ - + - + @@ -362,26 +464,27 @@ - - + + @@ -390,32 +493,32 @@ - + - + - + - + + + @@ -457,8 +566,20 @@ + + + + + + + + + + + + @@ -466,18 +587,20 @@ + + - + @@ -494,7 +617,7 @@ - + @@ -514,7 +637,7 @@ - + @@ -527,11 +650,11 @@ - + @@ -542,13 +665,13 @@ - + - + @@ -570,7 +693,7 @@ - + @@ -583,7 +706,7 @@ - + @@ -605,6 +728,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Weather/Utils/DateConverter.swift b/Weather/Utils/DateConverter.swift index 56505201..b1bced70 100644 --- a/Weather/Utils/DateConverter.swift +++ b/Weather/Utils/DateConverter.swift @@ -17,6 +17,12 @@ class DateConverter { dateFormatter.dateFormat = "EEEE" return dateFormatter.string(from: date) } + static func timeIntervalToDayShortString(_ time: TimeInterval) -> String{ + let date = Date(timeIntervalSince1970: time) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EE" + return dateFormatter.string(from: date) + } static func timeIntervalToHourString(_ time: TimeInterval) -> String{ let date = Date(timeIntervalSince1970: time) diff --git a/Weather/Utils/Extension/UIViewExtension.swift b/Weather/Utils/Extension/UIViewExtension.swift new file mode 100644 index 00000000..f6457796 --- /dev/null +++ b/Weather/Utils/Extension/UIViewExtension.swift @@ -0,0 +1,19 @@ +// +// UIViewExtension.swift +// Weather +// +// Created by Christian Huang on 11/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + + +//MARK:- layer related +extension UIView { + func setFullRoundedCorner(borderWidth: CGFloat = 0, borderColor: CGColor = UIColor.clear.cgColor) { + self.layer.cornerRadius = self.bounds.height / 2 + self.layer.borderWidth = borderWidth + self.layer.borderColor = borderColor + } +} diff --git a/Weather/View/Cell/ForecastCell.swift b/Weather/View/Cell/ForecastCell.swift index 655680e7..628d4693 100644 --- a/Weather/View/Cell/ForecastCell.swift +++ b/Weather/View/Cell/ForecastCell.swift @@ -27,4 +27,11 @@ class ForecastCell: UICollectionViewCell { temperatureLabel.text = forecastViewModel.temperature } + func configureCell(forecastDailyViewModel: ForecastDailyViewModel, isNow: Bool = false) { + timeLabel.text = isNow ? "Now" : forecastDailyViewModel.dayShortString + chanceLabel.text = nil + weatherImageView.kf.setImage(with: URL(string: forecastDailyViewModel.icon)) + temperatureLabel.text = "\(Int(forecastDailyViewModel.temperatureMin))°-\(Int(forecastDailyViewModel.temperatureMax))°" + } + } diff --git a/Weather/View/Cell/ForecastCell.xib b/Weather/View/Cell/ForecastCell.xib index 5c98074c..5dab3158 100644 --- a/Weather/View/Cell/ForecastCell.xib +++ b/Weather/View/Cell/ForecastCell.xib @@ -36,7 +36,7 @@ - - + diff --git a/Weather/Model/MiscInfo.swift b/Weather/Model/MiscInfo.swift index ebd12edf..a34e268f 100644 --- a/Weather/Model/MiscInfo.swift +++ b/Weather/Model/MiscInfo.swift @@ -9,13 +9,6 @@ import Foundation -enum DayTime: Int { - case morning - case day - case evening - case night -} - struct Coordinate: Codable { let longitude: Double let latitude: Double diff --git a/Weather/Utils/DateConverter.swift b/Weather/Utils/DateConverter.swift index b1bced70..063d9dfb 100644 --- a/Weather/Utils/DateConverter.swift +++ b/Weather/Utils/DateConverter.swift @@ -38,6 +38,13 @@ class DateConverter { return dateFormatter.string(from: date) } + static func timeIntervalToDayTimeInterval(_ time: TimeInterval) -> TimeInterval{ + let date = Date(timeIntervalSince1970: time) + let calendar = Calendar.current + let components = calendar.dateComponents([.hour, .minute], from: date) + return TimeInterval(((components.hour! * 60) + components.minute!) * 60) + } + static func stringToDate(dateString: String, timezone: String = "CEST") -> Date? { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZZZ" diff --git a/Weather/Utils/DayTimeColor.swift b/Weather/Utils/DayTimeColor.swift new file mode 100644 index 00000000..b0b41284 --- /dev/null +++ b/Weather/Utils/DayTimeColor.swift @@ -0,0 +1,32 @@ +// +// DayTimeColor.swift +// Weather +// +// Created by Christian Huang on 11/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + + +enum DayTime: Int { + case morning + case day + case evening + case night +} + +class DayTimeColor { + static func colorFor(_ dayTime: DayTime) -> UIColor { + switch dayTime { + case .morning: + return UIColor(hexString: "D9781F") + case .day: + return UIColor(hexString: "499BC4") + case .evening: + return UIColor(hexString: "C43900") + default: + return UIColor.black + } + } +} diff --git a/Weather/Utils/Extension/UIColorExtension.swift b/Weather/Utils/Extension/UIColorExtension.swift new file mode 100644 index 00000000..e5b0c63e --- /dev/null +++ b/Weather/Utils/Extension/UIColorExtension.swift @@ -0,0 +1,39 @@ +// +// UIColorExtension.swift +// Weather +// +// Created by Christian Huang on 11/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import UIKit + +extension UIColor { + convenience init(hexString: String, alpha: CGFloat = 1.0) { + let hexString: String = hexString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + let scanner = Scanner(string: hexString) + if (hexString.hasPrefix("#")) { + scanner.scanLocation = 1 + } + var color: UInt32 = 0 + scanner.scanHexInt32(&color) + let mask = 0x000000FF + let r = Int(color >> 16) & mask + let g = Int(color >> 8) & mask + let b = Int(color) & mask + let red = CGFloat(r) / 255.0 + let green = CGFloat(g) / 255.0 + let blue = CGFloat(b) / 255.0 + self.init(red:red, green:green, blue:blue, alpha:alpha) + } + + func toHexString() -> String { + var r:CGFloat = 0 + var g:CGFloat = 0 + var b:CGFloat = 0 + var a:CGFloat = 0 + getRed(&r, green: &g, blue: &b, alpha: &a) + let rgb:Int = (Int)(r*255)<<16 | (Int)(g*255)<<8 | (Int)(b*255)<<0 + return String(format:"#%06x", rgb) + } +} diff --git a/Weather/View/MyWeatherViewController.swift b/Weather/View/MyWeatherViewController.swift index 7c93c11b..f3480474 100644 --- a/Weather/View/MyWeatherViewController.swift +++ b/Weather/View/MyWeatherViewController.swift @@ -67,6 +67,7 @@ extension MyWeatherViewController { weatherViewModel.temperature.listener = { self.temperatureLabel.text = "\(Int($0))" } weatherViewModel.timeString.listener = { self.timeLabel.text = $0 } weatherViewModel.dayShortString.listener = { self.dayLabel.text = $0 } + weatherViewModel.dayTime.listener = { self.view.backgroundColor = DayTimeColor.colorFor($0) } weatherViewModel.temperatureMin.listener = { self.minTemperatureLabel.text = "Min \(Int($0))°" } weatherViewModel.temperatureMax.listener = { self.maxTemperatureLabel.text = "Max \(Int($0))°" } weatherViewModel.windSpeed.listener = { self.windLabel.text = "Wind \($0)" } diff --git a/Weather/View/WeatherViewController.swift b/Weather/View/WeatherViewController.swift index 48ba109c..4d2835a5 100644 --- a/Weather/View/WeatherViewController.swift +++ b/Weather/View/WeatherViewController.swift @@ -77,6 +77,7 @@ extension WeatherViewController { } weatherViewModel.sunrise.listener = { self.sunriseSunsetView.detail1 = $0 } weatherViewModel.sunset.listener = { self.sunriseSunsetView.detail2 = $0 } + weatherViewModel.dayTime.listener = { self.view.backgroundColor = DayTimeColor.colorFor($0) } weatherViewModel.pressure.listener = { self.pressureHumidityView.detail1 = $0 } weatherViewModel.humidity.listener = { self.pressureHumidityView.detail2 = $0 } diff --git a/Weather/ViewModel/WeatherViewModel.swift b/Weather/ViewModel/WeatherViewModel.swift index 3c28db9f..02d91487 100644 --- a/Weather/ViewModel/WeatherViewModel.swift +++ b/Weather/ViewModel/WeatherViewModel.swift @@ -23,7 +23,9 @@ class WeatherViewModel: NSObject { var temperatureMin: Dynamic = Dynamic(0) var temperatureMax: Dynamic = Dynamic(0) var sunrise = Dynamic("00:00") + var sunriseTime: Dynamic = Dynamic(0) var sunset = Dynamic("00:00") + var sunsetTime: Dynamic = Dynamic(0) var windSpeed = Dynamic("") var pressure = Dynamic("") var humidity = Dynamic("") @@ -74,6 +76,7 @@ extension WeatherViewModel: WeatherDetailProtocol { self.timeString.value = DateConverter.timeIntervalToHourMinuteString(forecast.dayTime) self.dayString.value = DateConverter.timeIntervalToDayString(forecast.dayTime) self.dayShortString.value = DateConverter.timeIntervalToDayShortString(forecast.dayTime) + self.dayTime.value = self.calcDayTime(timeInterval: forecast.dayTime) self.temperatureMin.value = forecast.main.temperatureMin self.temperatureMax.value = forecast.main.temperatureMax self.windSpeed.value = "\(forecast.wind.speed) mps" @@ -91,6 +94,7 @@ extension WeatherViewModel: WeatherDetailProtocol { self.timeString.value = DateConverter.timeIntervalToHourMinuteString(forecastDaily.dayTime) self.dayString.value = DateConverter.timeIntervalToDayString(forecastDaily.dayTime) self.dayShortString.value = DateConverter.timeIntervalToDayShortString(forecastDaily.dayTime) + self.dayTime.value = self.calcDayTime(timeInterval: forecastDaily.dayTime) self.temperatureMin.value = forecastDaily.temperature.min self.temperatureMax.value = forecastDaily.temperature.max self.windSpeed.value = "\(forecastDaily.speed) mps" @@ -142,14 +146,16 @@ extension WeatherViewModel { self.temperature.value = response.main.temperature self.summary.value = String(format: "Today: %@ currently. It's %d°; the high today was forecast as %d°.", response.weather[0].main, Int(response.main.temperature), Int(response.main.temperatureMax)) - self.dayTime.value = .night self.timeString.value = DateConverter.timeIntervalToHourMinuteString(response.dayTime) self.dayString.value = DateConverter.timeIntervalToDayString(response.dayTime) self.dayShortString.value = DateConverter.timeIntervalToDayShortString(response.dayTime) self.temperatureMin.value = response.main.temperatureMin self.temperatureMax.value = response.main.temperatureMax self.sunrise.value = DateConverter.timeIntervalToHourMinuteString(response.system.sunrise) + self.sunriseTime.value = DateConverter.timeIntervalToDayTimeInterval(response.system.sunrise) self.sunset.value = DateConverter.timeIntervalToHourMinuteString(response.system.sunset) + self.sunsetTime.value = DateConverter.timeIntervalToDayTimeInterval(response.system.sunset) + self.dayTime.value = self.calcDayTime(timeInterval: response.dayTime) self.windSpeed.value = "\(response.wind.speed) mps" self.pressure.value = String(format: "%d hPa", Int(response.main.pressure)) self.humidity.value = String(format: "%d %%", response.main.humidity) @@ -158,4 +164,20 @@ extension WeatherViewModel { self.finishedFetchingWeatherClosure?() } } + + private func calcDayTime(timeInterval: TimeInterval)-> DayTime { + let time = DateConverter.timeIntervalToDayTimeInterval(timeInterval) + switch time { + case 0.. Date: Mon, 12 Nov 2018 04:20:15 +0700 Subject: [PATCH 17/23] Add some unit tests --- Weather.xcodeproj/project.pbxproj | 56 +++++ Weather/View/Custom/DailyForecastView.swift | 6 +- .../View/ForecastDailyCollectionView.swift | 3 +- Weather/View/MyWeatherViewController.swift | 34 ++-- Weather/View/WeatherViewController.swift | 48 +++-- .../ForecastDailyListViewModel.swift | 3 + Weather/ViewModel/ForecastListViewModel.swift | 3 + Weather/ViewModel/WeatherViewModel.swift | 192 +++++++++--------- WeatherTests/DateConverterTests.swift | 77 +++++++ WeatherTests/ForecastListViewModelTests.swift | 56 +++++ .../MockService/MockNetworkManager.swift | 36 ++++ WeatherTests/Resource/forecast.json | 1 + WeatherTests/Resource/forecastDaily.json | 1 + WeatherTests/Resource/weather.json | 1 + WeatherTests/WeatherTests.swift | 7 - WeatherTests/WeatherViewModelTests.swift | 120 +++++++++++ 16 files changed, 509 insertions(+), 135 deletions(-) create mode 100644 WeatherTests/DateConverterTests.swift create mode 100644 WeatherTests/ForecastListViewModelTests.swift create mode 100644 WeatherTests/MockService/MockNetworkManager.swift create mode 100644 WeatherTests/Resource/forecast.json create mode 100644 WeatherTests/Resource/forecastDaily.json create mode 100644 WeatherTests/Resource/weather.json create mode 100644 WeatherTests/WeatherViewModelTests.swift diff --git a/Weather.xcodeproj/project.pbxproj b/Weather.xcodeproj/project.pbxproj index ff9635e5..5b1abcea 100644 --- a/Weather.xcodeproj/project.pbxproj +++ b/Weather.xcodeproj/project.pbxproj @@ -10,6 +10,12 @@ 0590753E29507F710CF3CED8 /* Pods_WeatherTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD1A25CF54C2CDB0077D7291 /* Pods_WeatherTests.framework */; }; 158601774FA4161333DFB17B /* Pods_Weather.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9E3500245BE8843B5CF5268 /* Pods_Weather.framework */; }; AA0BF2C421984CAF0093D62F /* UIColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0BF2C321984CAF0093D62F /* UIColorExtension.swift */; }; + AA0BF2C8219854370093D62F /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0BF2C7219854370093D62F /* XCTestCase.swift */; }; + AA0BF2CA219854640093D62F /* weather.json in Resources */ = {isa = PBXBuildFile; fileRef = AA0BF2C9219854640093D62F /* weather.json */; }; + AA0BF2CC2198547D0093D62F /* forecast.json in Resources */ = {isa = PBXBuildFile; fileRef = AA0BF2CB2198547D0093D62F /* forecast.json */; }; + AA0BF2CE2198548F0093D62F /* forecastDaily.json in Resources */ = {isa = PBXBuildFile; fileRef = AA0BF2CD2198548F0093D62F /* forecastDaily.json */; }; + AA0BF2D0219855040093D62F /* WeatherViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0BF2CF219855040093D62F /* WeatherViewModelTests.swift */; }; + AA0BF2D7219859130093D62F /* MockNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0BF2D6219859130093D62F /* MockNetworkManager.swift */; }; AA0E4B01218E940500379569 /* JSONParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF4218E940500379569 /* JSONParameterEncoder.swift */; }; AA0E4B02218E940500379569 /* ParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF5218E940500379569 /* ParameterEncoding.swift */; }; AA0E4B03218E940500379569 /* URLParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0E4AF6218E940500379569 /* URLParameterEncoding.swift */; }; @@ -49,6 +55,8 @@ AA4528CF218DB18400EC0901 /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4528CE218DB18400EC0901 /* InfoView.swift */; }; AA4528D1218DB19700EC0901 /* InfoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA4528D0218DB19700EC0901 /* InfoView.xib */; }; AA60F075218EC7D20080F2C7 /* WeatherViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */; }; + AA7F66352198C5E40061DFE8 /* ForecastListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7F66342198C5E40061DFE8 /* ForecastListViewModelTests.swift */; }; + AA7F66372198CB960061DFE8 /* DateConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7F66362198CB960061DFE8 /* DateConverterTests.swift */; }; AAC62752218EAF7600A221C0 /* Weather.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC62751218EAF7600A221C0 /* Weather.swift */; }; AAE4A44721923DF70068D8F1 /* DateConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE4A44621923DF70068D8F1 /* DateConverter.swift */; }; AAE8A3A22197E84700DF123F /* ForecastDailyCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8A3A12197E84700DF123F /* ForecastDailyCollectionView.swift */; }; @@ -89,6 +97,12 @@ 9089C580DD3FE849E3D674CC /* Pods-WeatherTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WeatherTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-WeatherTests/Pods-WeatherTests.debug.xcconfig"; sourceTree = ""; }; 9C41227BF3B0CE586CCEA861 /* Pods-WeatherTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WeatherTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-WeatherTests/Pods-WeatherTests.release.xcconfig"; sourceTree = ""; }; AA0BF2C321984CAF0093D62F /* UIColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColorExtension.swift; sourceTree = ""; }; + AA0BF2C7219854370093D62F /* XCTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestCase.swift; sourceTree = ""; }; + AA0BF2C9219854640093D62F /* weather.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = weather.json; sourceTree = ""; }; + AA0BF2CB2198547D0093D62F /* forecast.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = forecast.json; sourceTree = ""; }; + AA0BF2CD2198548F0093D62F /* forecastDaily.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = forecastDaily.json; sourceTree = ""; }; + AA0BF2CF219855040093D62F /* WeatherViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModelTests.swift; sourceTree = ""; }; + AA0BF2D6219859130093D62F /* MockNetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkManager.swift; sourceTree = ""; }; AA0E4AF4218E940500379569 /* JSONParameterEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONParameterEncoder.swift; sourceTree = ""; }; AA0E4AF5218E940500379569 /* ParameterEncoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParameterEncoding.swift; sourceTree = ""; }; AA0E4AF6218E940500379569 /* URLParameterEncoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLParameterEncoding.swift; sourceTree = ""; }; @@ -132,6 +146,8 @@ AA4528CE218DB18400EC0901 /* InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = ""; }; AA4528D0218DB19700EC0901 /* InfoView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InfoView.xib; sourceTree = ""; }; AA60F074218EC7D20080F2C7 /* WeatherViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModel.swift; sourceTree = ""; }; + AA7F66342198C5E40061DFE8 /* ForecastListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastListViewModelTests.swift; sourceTree = ""; }; + AA7F66362198CB960061DFE8 /* DateConverterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateConverterTests.swift; sourceTree = ""; }; AAC62751218EAF7600A221C0 /* Weather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weather.swift; sourceTree = ""; }; AAE4A44621923DF70068D8F1 /* DateConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateConverter.swift; sourceTree = ""; }; AAE8A3A12197E84700DF123F /* ForecastDailyCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDailyCollectionView.swift; sourceTree = ""; }; @@ -184,6 +200,32 @@ name = Pods; sourceTree = ""; }; + AA0BF2C5219853EA0093D62F /* Resource */ = { + isa = PBXGroup; + children = ( + AA0BF2C9219854640093D62F /* weather.json */, + AA0BF2CB2198547D0093D62F /* forecast.json */, + AA0BF2CD2198548F0093D62F /* forecastDaily.json */, + ); + path = Resource; + sourceTree = ""; + }; + AA0BF2C6219853FC0093D62F /* Extension */ = { + isa = PBXGroup; + children = ( + AA0BF2C7219854370093D62F /* XCTestCase.swift */, + ); + path = Extension; + sourceTree = ""; + }; + AA0BF2D5219858F40093D62F /* MockService */ = { + isa = PBXGroup; + children = ( + AA0BF2D6219859130093D62F /* MockNetworkManager.swift */, + ); + path = MockService; + sourceTree = ""; + }; AA0E4AF2218E940500379569 /* Networking */ = { isa = PBXGroup; children = ( @@ -274,8 +316,14 @@ AA3006B6218CE3DD00A23A36 /* WeatherTests */ = { isa = PBXGroup; children = ( + AA0BF2C5219853EA0093D62F /* Resource */, + AA0BF2C6219853FC0093D62F /* Extension */, + AA0BF2D5219858F40093D62F /* MockService */, AA3006B7218CE3DD00A23A36 /* WeatherTests.swift */, AA3006B9218CE3DD00A23A36 /* Info.plist */, + AA0BF2CF219855040093D62F /* WeatherViewModelTests.swift */, + AA7F66342198C5E40061DFE8 /* ForecastListViewModelTests.swift */, + AA7F66362198CB960061DFE8 /* DateConverterTests.swift */, ); path = WeatherTests; sourceTree = ""; @@ -484,6 +532,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + AA0BF2CC2198547D0093D62F /* forecast.json in Resources */, + AA0BF2CA219854640093D62F /* weather.json in Resources */, + AA0BF2CE2198548F0093D62F /* forecastDaily.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -640,7 +691,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AA7F66372198CB960061DFE8 /* DateConverterTests.swift in Sources */, + AA0BF2C8219854370093D62F /* XCTestCase.swift in Sources */, AA3006B8218CE3DD00A23A36 /* WeatherTests.swift in Sources */, + AA0BF2D7219859130093D62F /* MockNetworkManager.swift in Sources */, + AA7F66352198C5E40061DFE8 /* ForecastListViewModelTests.swift in Sources */, + AA0BF2D0219855040093D62F /* WeatherViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Weather/View/Custom/DailyForecastView.swift b/Weather/View/Custom/DailyForecastView.swift index 9b4f9d9e..f20c1845 100644 --- a/Weather/View/Custom/DailyForecastView.swift +++ b/Weather/View/Custom/DailyForecastView.swift @@ -32,11 +32,11 @@ class DailyForecastView: UIView { */ func configureTodayView(weatherViewModel: WeatherViewModel) { - dayLabel.text = weatherViewModel.dayString.value + dayLabel.text = weatherViewModel.dayString todayLabel.alpha = 1 weatherImageView.image = nil - maxLabel.text = "\(Int(weatherViewModel.temperatureMax.value))°" - minLabel.text = "\(Int(weatherViewModel.temperatureMin.value))°" + maxLabel.text = "\(Int(weatherViewModel.temperatureMax))°" + minLabel.text = "\(Int(weatherViewModel.temperatureMin))°" } func configureTodayView(forecastDailyViewModel: ForecastDailyViewModel) { diff --git a/Weather/View/ForecastDailyCollectionView.swift b/Weather/View/ForecastDailyCollectionView.swift index d5907dd6..cfc5ce20 100644 --- a/Weather/View/ForecastDailyCollectionView.swift +++ b/Weather/View/ForecastDailyCollectionView.swift @@ -15,6 +15,8 @@ class ForecastDailyCollectionView: UICollectionView { override func awakeFromNib() { super.awakeFromNib() self.register(UINib(nibName: "ForecastCell", bundle: nil), forCellWithReuseIdentifier: "forecastCellIdentifier") + self.dataSource = self + self.delegate = self } } @@ -22,7 +24,6 @@ class ForecastDailyCollectionView: UICollectionView { //MARK:- ViewModel related extension ForecastDailyCollectionView { func initViewModel() { - self.dataSource = self forecastDailyListViewModel.forecastDailyList.listener = { forecastList in DispatchQueue.main.async { self.reloadData() diff --git a/Weather/View/MyWeatherViewController.swift b/Weather/View/MyWeatherViewController.swift index f3480474..acb96391 100644 --- a/Weather/View/MyWeatherViewController.swift +++ b/Weather/View/MyWeatherViewController.swift @@ -61,21 +61,14 @@ class MyWeatherViewController: UIViewController { //MARK:- ViewModel related extension MyWeatherViewController { func initViewModel() { - weatherViewModel.area.listener = { self.locationLabel.text = "\($0), \(self.weatherViewModel.country.value)" } - weatherViewModel.country.listener = { self.locationLabel.text = "\(self.weatherViewModel.area.value), \($0)" } - weatherViewModel.weather.listener = { self.weatherLabel.text = $0 } - weatherViewModel.temperature.listener = { self.temperatureLabel.text = "\(Int($0))" } - weatherViewModel.timeString.listener = { self.timeLabel.text = $0 } - weatherViewModel.dayShortString.listener = { self.dayLabel.text = $0 } - weatherViewModel.dayTime.listener = { self.view.backgroundColor = DayTimeColor.colorFor($0) } - weatherViewModel.temperatureMin.listener = { self.minTemperatureLabel.text = "Min \(Int($0))°" } - weatherViewModel.temperatureMax.listener = { self.maxTemperatureLabel.text = "Max \(Int($0))°" } - weatherViewModel.windSpeed.listener = { self.windLabel.text = "Wind \($0)" } - weatherViewModel.pressure.listener = { self.pressureLabel.text = $0 } - weatherViewModel.humidity.listener = { self.humidityLabel.text = $0 } - weatherViewModel.icon.listener = { self.weatherImageView.kf.setImage(with: URL(string: $0)) } + weatherViewModel.startFetchingWeatherClosure = showLoadingView + weatherViewModel.updateWeatherDataClosure = { + DispatchQueue.main.async { + self.updateWeatherData() + } + } weatherViewModel.finishedFetchingWeatherClosure = hideLoadingView shortTermForecastCollectionView.initViewModel() @@ -94,6 +87,21 @@ extension MyWeatherViewController { weatherCenterView.setFullRoundedCorner(borderWidth: 1, borderColor: UIColor.lightGray.cgColor) } + private func updateWeatherData() { + locationLabel.text = "\(weatherViewModel.area), \(weatherViewModel.country)" + weatherLabel.text = weatherViewModel.weather + temperatureLabel.text = "\(Int(weatherViewModel.temperature))" + timeLabel.text = weatherViewModel.timeString + dayLabel.text = weatherViewModel.dayShortString + self.view.backgroundColor = DayTimeColor.colorFor(weatherViewModel.dayTime) + minTemperatureLabel.text = "Min \(Int(weatherViewModel.temperatureMin))°" + maxTemperatureLabel.text = "Max \(Int(weatherViewModel.temperatureMax))°" + windLabel.text = "Wind \(weatherViewModel.windSpeed)" + pressureLabel.text = weatherViewModel.pressure + humidityLabel.text = weatherViewModel.humidity + weatherImageView.kf.setImage(with: URL(string: weatherViewModel.icon)) + } + private func showLoadingView(){ DispatchQueue.main.async { self.loadingView.startAnimating() diff --git a/Weather/View/WeatherViewController.swift b/Weather/View/WeatherViewController.swift index 4d2835a5..889daaad 100644 --- a/Weather/View/WeatherViewController.swift +++ b/Weather/View/WeatherViewController.swift @@ -69,21 +69,22 @@ class WeatherViewController: UIViewController { //MARK:- ViewModel related extension WeatherViewController { func initViewModel() { - weatherViewModel.area.listener = { self.areaLabel.text = $0 } - weatherViewModel.weather.listener = { self.weatherLabel.text = $0 } - weatherViewModel.temperature.listener = { self.temperatureLabel.text = "\(Int($0))°" } - weatherViewModel.summary.listener = { summary in - self.forecastDescriptionLabel.text = summary - } - weatherViewModel.sunrise.listener = { self.sunriseSunsetView.detail1 = $0 } - weatherViewModel.sunset.listener = { self.sunriseSunsetView.detail2 = $0 } - weatherViewModel.dayTime.listener = { self.view.backgroundColor = DayTimeColor.colorFor($0) } - weatherViewModel.pressure.listener = { self.pressureHumidityView.detail1 = $0 } - weatherViewModel.humidity.listener = { self.pressureHumidityView.detail2 = $0 } - weatherViewModel.startFetchingWeatherClosure = showLoadingView - weatherViewModel.updateWeatherDataClosure = updateWeatherData - weatherViewModel.finishedFetchingWeatherClosure = hideLoadingView + weatherViewModel.startFetchingWeatherClosure = { + DispatchQueue.main.async { + self.showLoadingView() + } + } + weatherViewModel.updateWeatherDataClosure = { + DispatchQueue.main.async { + self.updateWeatherData() + } + } + weatherViewModel.finishedFetchingWeatherClosure = { + DispatchQueue.main.async { + self.hideLoadingView() + } + } todayForecastCollectionView.initViewModel() forecastDailyStackView.initViewModel() @@ -91,9 +92,6 @@ extension WeatherViewController { weatherViewModel.forecastDailyListViewModel = forecastDailyStackView.forecastDailyListViewModel } - private func updateWeatherData() { - todayForecastView.configureTodayView(weatherViewModel: weatherViewModel) - } } //MARK:- private func @@ -118,6 +116,21 @@ extension WeatherViewController { headerViewTopRatio = headerViewTopDefault / totalTopShift todayViewTopRatio = todayViewTopDefault / totalTopShift } + + private func updateWeatherData() { + areaLabel.text = weatherViewModel.area + weatherLabel.text = weatherViewModel.weather + temperatureLabel.text = "\(Int(weatherViewModel.temperature))°" + forecastDescriptionLabel.text = weatherViewModel.summary + sunriseSunsetView.detail1 = weatherViewModel.sunrise + sunriseSunsetView.detail2 = weatherViewModel.sunset + view.backgroundColor = DayTimeColor.colorFor(weatherViewModel.dayTime) + pressureHumidityView.detail1 = weatherViewModel.pressure + pressureHumidityView.detail2 = weatherViewModel.humidity + + todayForecastView.configureTodayView(weatherViewModel: weatherViewModel) + } + private func showLoadingView(){ if isFirstLoad { isFirstLoad = false @@ -126,6 +139,7 @@ extension WeatherViewController { loadingView.startAnimating() } } + private func hideLoadingView() { UIView.animate(withDuration: 0.2, animations: { diff --git a/Weather/ViewModel/ForecastDailyListViewModel.swift b/Weather/ViewModel/ForecastDailyListViewModel.swift index 68c0ae7c..94ba7040 100644 --- a/Weather/ViewModel/ForecastDailyListViewModel.swift +++ b/Weather/ViewModel/ForecastDailyListViewModel.swift @@ -15,6 +15,7 @@ class ForecastDailyListViewModel: NSObject { var forecastDailyCount: Int { return forecastDailyList.value.count } + var status: String? = nil var networkManager: WeatherNetworkManager! var weatherDetail: WeatherDetailProtocol? @@ -25,7 +26,9 @@ extension ForecastDailyListViewModel { networkManager.fetchForecastDaily(latitude: coordinate.latitude, longitude: coordinate.longitude) { (forecastDailyResponse, error) in if let error = error { print("Network Error: \(error)") + self.status = error } else { + self.status = nil self.processForecastDailyResponse(forecastDailyResponse!) } } diff --git a/Weather/ViewModel/ForecastListViewModel.swift b/Weather/ViewModel/ForecastListViewModel.swift index 0e4a602c..0d2fb03f 100644 --- a/Weather/ViewModel/ForecastListViewModel.swift +++ b/Weather/ViewModel/ForecastListViewModel.swift @@ -14,6 +14,7 @@ class ForecastListViewModel: NSObject { var forecastCount: Int { return forecastList.value.count } + var status: String? = nil var networkManager: WeatherNetworkManager! var weatherDetail: WeatherDetailProtocol? @@ -25,7 +26,9 @@ extension ForecastListViewModel { networkManager.fetchForecast(latitude: coordinate.latitude, longitude: coordinate.longitude) { (forecastResponse, error) in if let error = error { print("Network Error: \(error)") + self.status = error } else { + self.status = nil self.processForecastResponse(forecastResponse!) } } diff --git a/Weather/ViewModel/WeatherViewModel.swift b/Weather/ViewModel/WeatherViewModel.swift index 02d91487..e41eca86 100644 --- a/Weather/ViewModel/WeatherViewModel.swift +++ b/Weather/ViewModel/WeatherViewModel.swift @@ -10,29 +10,30 @@ import UIKit import CoreLocation class WeatherViewModel: NSObject { - private var location: Coordinate? - var area = Dynamic("") - var country = Dynamic("") - var weather = Dynamic("") - var temperature: Dynamic = Dynamic(0) - var summary = Dynamic("") - var dayTime: Dynamic = Dynamic(DayTime.morning) - var timeString = Dynamic("00:00") - var dayString = Dynamic("") - var dayShortString = Dynamic("") - var temperatureMin: Dynamic = Dynamic(0) - var temperatureMax: Dynamic = Dynamic(0) - var sunrise = Dynamic("00:00") - var sunriseTime: Dynamic = Dynamic(0) - var sunset = Dynamic("00:00") - var sunsetTime: Dynamic = Dynamic(0) - var windSpeed = Dynamic("") - var pressure = Dynamic("") - var humidity = Dynamic("") - var icon = Dynamic("") + var location: Coordinate? + var area = "" + var country = "" + var weather = "" + var temperature: Double = 0 + var summary = "" + var dayTime: DayTime = .morning + var timeString = "00:00" + var dayString = "" + var dayShortString = "" + var temperatureMin: Double = 0 + var temperatureMax: Double = 0 + var sunrise = "00:00" + var sunriseTime: TimeInterval = 0 + var sunset = "00:00" + var sunsetTime: TimeInterval = 0 + var windSpeed = "" + var pressure = "" + var humidity = "" + var icon = "" + var status: String? = nil - lazy var networkManager: WeatherNetworkManager = NetworkManager(apiKey: Constants.apiKey, environment: Constants.networkEnvironment) + var networkManager: WeatherNetworkManager = NetworkManager(apiKey: Constants.apiKey, environment: Constants.networkEnvironment) private var locationManager = LocationManager() weak var forecastListViewModel: ForecastListViewModel? { @@ -63,45 +64,64 @@ extension WeatherViewModel { locationManager.requestLocation() startFetchingWeatherClosure?() } + + func fetchWeather() { + guard let location = location else { + print("Location is not avaliable") + status = "Location is not avaliable" + return + } + + networkManager.fetchWeather(latitude: location.latitude, longitude: location.longitude) { (weatherResponse, error) in + if let error = error { + print("Network Error: \(error)") + self.status = error + } else { + self.status = nil + self.processWeatherResponse(weatherResponse!) + } + } + } } //MARK: - WeatherDetailProtocol extension WeatherViewModel: WeatherDetailProtocol { func updateWeatherData(forecast: Forecast) { - DispatchQueue.main.async { - self.weather.value = forecast.weather[0].main - self.temperature.value = forecast.main.temperature - self.summary.value = "" - self.timeString.value = DateConverter.timeIntervalToHourMinuteString(forecast.dayTime) - self.dayString.value = DateConverter.timeIntervalToDayString(forecast.dayTime) - self.dayShortString.value = DateConverter.timeIntervalToDayShortString(forecast.dayTime) - self.dayTime.value = self.calcDayTime(timeInterval: forecast.dayTime) - self.temperatureMin.value = forecast.main.temperatureMin - self.temperatureMax.value = forecast.main.temperatureMax - self.windSpeed.value = "\(forecast.wind.speed) mps" - self.pressure.value = String(format: "%d hPa", Int(forecast.main.pressure)) - self.humidity.value = String(format: "%d%", forecast.main.humidity) - self.icon.value = "http://openweathermap.org/img/w/\(forecast.weather[0].icon).png" - } + + weather = forecast.weather[0].main + temperature = forecast.main.temperature + summary = "" + timeString = DateConverter.timeIntervalToHourMinuteString(forecast.dayTime) + dayString = DateConverter.timeIntervalToDayString(forecast.dayTime) + dayShortString = DateConverter.timeIntervalToDayShortString(forecast.dayTime) + dayTime = self.calcDayTime(timeInterval: forecast.dayTime) + temperatureMin = forecast.main.temperatureMin + temperatureMax = forecast.main.temperatureMax + windSpeed = "\(forecast.wind.speed) mps" + pressure = String(format: "%d hPa", Int(forecast.main.pressure)) + humidity = String(format: "%d %%", forecast.main.humidity) + icon = "http://openweathermap.org/img/w/\(forecast.weather[0].icon).png" + + updateWeatherDataClosure?() } func updateWeatherData(forecastDaily: ForecastDaily) { - DispatchQueue.main.async { - self.weather.value = forecastDaily.weather[0].main - self.temperature.value = forecastDaily.temperature.day - self.summary.value = "" - self.timeString.value = DateConverter.timeIntervalToHourMinuteString(forecastDaily.dayTime) - self.dayString.value = DateConverter.timeIntervalToDayString(forecastDaily.dayTime) - self.dayShortString.value = DateConverter.timeIntervalToDayShortString(forecastDaily.dayTime) - self.dayTime.value = self.calcDayTime(timeInterval: forecastDaily.dayTime) - self.temperatureMin.value = forecastDaily.temperature.min - self.temperatureMax.value = forecastDaily.temperature.max - self.windSpeed.value = "\(forecastDaily.speed) mps" - self.pressure.value = String(format: "%d hPa", Int(forecastDaily.pressure)) - self.humidity.value = String(format: "%d%", forecastDaily.humidity) - self.icon.value = "http://openweathermap.org/img/w/\(forecastDaily.weather[0].icon).png" - } + weather = forecastDaily.weather[0].main + temperature = forecastDaily.temperature.day + summary = "" + timeString = DateConverter.timeIntervalToHourMinuteString(forecastDaily.dayTime) + dayString = DateConverter.timeIntervalToDayString(forecastDaily.dayTime) + dayShortString = DateConverter.timeIntervalToDayShortString(forecastDaily.dayTime) + dayTime = self.calcDayTime(timeInterval: forecastDaily.dayTime) + temperatureMin = forecastDaily.temperature.min + temperatureMax = forecastDaily.temperature.max + windSpeed = "\(forecastDaily.speed) mps" + pressure = String(format: "%d hPa", Int(forecastDaily.pressure)) + humidity = String(format: "%d %%", forecastDaily.humidity) + icon = "http://openweathermap.org/img/w/\(forecastDaily.weather[0].icon).png" + + updateWeatherDataClosure?() } } @@ -123,58 +143,42 @@ extension WeatherViewModel { } } - private func fetchWeather() { - guard let location = location else { - print("Location is not avaliable") - return - } - - networkManager.fetchWeather(latitude: location.latitude, longitude: location.longitude) { (weatherResponse, error) in - if let error = error { - print("Network Error: \(error)") - } else { - self.processWeatherResponse(weatherResponse!) - } - } - } - private func processWeatherResponse(_ response: WeatherResponse) { - DispatchQueue.main.async { - self.area.value = response.name - self.country.value = response.system.country - self.weather.value = response.weather[0].main - self.temperature.value = response.main.temperature - self.summary.value = String(format: "Today: %@ currently. It's %d°; the high today was forecast as %d°.", response.weather[0].main, Int(response.main.temperature), Int(response.main.temperatureMax)) - - self.timeString.value = DateConverter.timeIntervalToHourMinuteString(response.dayTime) - self.dayString.value = DateConverter.timeIntervalToDayString(response.dayTime) - self.dayShortString.value = DateConverter.timeIntervalToDayShortString(response.dayTime) - self.temperatureMin.value = response.main.temperatureMin - self.temperatureMax.value = response.main.temperatureMax - self.sunrise.value = DateConverter.timeIntervalToHourMinuteString(response.system.sunrise) - self.sunriseTime.value = DateConverter.timeIntervalToDayTimeInterval(response.system.sunrise) - self.sunset.value = DateConverter.timeIntervalToHourMinuteString(response.system.sunset) - self.sunsetTime.value = DateConverter.timeIntervalToDayTimeInterval(response.system.sunset) - self.dayTime.value = self.calcDayTime(timeInterval: response.dayTime) - self.windSpeed.value = "\(response.wind.speed) mps" - self.pressure.value = String(format: "%d hPa", Int(response.main.pressure)) - self.humidity.value = String(format: "%d %%", response.main.humidity) - self.icon.value = "http://openweathermap.org/img/w/\(response.weather[0].icon).png" - self.updateWeatherDataClosure?() - self.finishedFetchingWeatherClosure?() - } + area = response.name + country = response.system.country + weather = response.weather[0].main + temperature = response.main.temperature + summary = String(format: "Today: %@ currently. It's %d°; The high today was forecast as %d°.", response.weather[0].main, Int(response.main.temperature), Int(response.main.temperatureMax)) + + timeString = DateConverter.timeIntervalToHourMinuteString(response.dayTime) + dayString = DateConverter.timeIntervalToDayString(response.dayTime) + dayShortString = DateConverter.timeIntervalToDayShortString(response.dayTime) + temperatureMin = response.main.temperatureMin + temperatureMax = response.main.temperatureMax + sunrise = DateConverter.timeIntervalToHourMinuteString(response.system.sunrise) + sunriseTime = DateConverter.timeIntervalToDayTimeInterval(response.system.sunrise) + sunset = DateConverter.timeIntervalToHourMinuteString(response.system.sunset) + sunsetTime = DateConverter.timeIntervalToDayTimeInterval(response.system.sunset) + dayTime = self.calcDayTime(timeInterval: response.dayTime) + windSpeed = "\(response.wind.speed) mps" + pressure = String(format: "%d hPa", Int(response.main.pressure)) + humidity = String(format: "%d %%", response.main.humidity) + icon = "http://openweathermap.org/img/w/\(response.weather[0].icon).png" + + updateWeatherDataClosure?() + finishedFetchingWeatherClosure?() } private func calcDayTime(timeInterval: TimeInterval)-> DayTime { let time = DateConverter.timeIntervalToDayTimeInterval(timeInterval) switch time { - case 0.. ()) { + completion(mockWeatherResponse, mockError) + } + + func fetchForecast(latitude: Double, longitude: Double, completion: @escaping (ForecastResponse?, String?) -> ()) { + completion(mockForecastResponse, mockError) + } + + func fetchForecastDaily(latitude: Double, longitude: Double, completion: @escaping (ForecastDailyResponse?, String?) -> ()) { + completion(mockForecastDailyResponse, mockError) + } + + +} diff --git a/WeatherTests/Resource/forecast.json b/WeatherTests/Resource/forecast.json new file mode 100644 index 00000000..3776d253 --- /dev/null +++ b/WeatherTests/Resource/forecast.json @@ -0,0 +1 @@ +{"cod":"200","message":0.005,"cnt":40,"list":[{"dt":1541430000,"main":{"temp":27.54,"temp_min":27.54,"temp_max":27.62,"pressure":1022.93,"sea_level":1023.49,"grnd_level":1022.93,"humidity":99,"temp_kf":-0.08},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":32},"wind":{"speed":1.92,"deg":209.504},"rain":{"3h":1.4625},"sys":{"pod":"n"},"dt_txt":"2018-11-05 15:00:00"},{"dt":1541440800,"main":{"temp":27.12,"temp_min":27.12,"temp_max":27.18,"pressure":1021.52,"sea_level":1022.15,"grnd_level":1021.52,"humidity":100,"temp_kf":-0.06},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":20},"wind":{"speed":2.37,"deg":197.504},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-05 18:00:00"},{"dt":1541451600,"main":{"temp":26.71,"temp_min":26.71,"temp_max":26.75,"pressure":1021.17,"sea_level":1021.83,"grnd_level":1021.17,"humidity":100,"temp_kf":-0.04},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"clouds":{"all":32},"wind":{"speed":2.64,"deg":186.001},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-05 21:00:00"},{"dt":1541462400,"main":{"temp":26.87,"temp_min":26.87,"temp_max":26.89,"pressure":1022.96,"sea_level":1023.48,"grnd_level":1022.96,"humidity":100,"temp_kf":-0.02},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"clouds":{"all":32},"wind":{"speed":3.19,"deg":155.5},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-06 00:00:00"},{"dt":1541473200,"main":{"temp":30.11,"temp_min":30.11,"temp_max":30.11,"pressure":1022.75,"sea_level":1023.31,"grnd_level":1022.75,"humidity":90,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"02d"}],"clouds":{"all":8},"wind":{"speed":1.95,"deg":108.516},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-06 03:00:00"},{"dt":1541484000,"main":{"temp":31.08,"temp_min":31.08,"temp_max":31.08,"pressure":1020.01,"sea_level":1020.64,"grnd_level":1020.01,"humidity":83,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":0},"wind":{"speed":3.56,"deg":10.5087},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-06 06:00:00"},{"dt":1541494800,"main":{"temp":29.78,"temp_min":29.78,"temp_max":29.78,"pressure":1019.54,"sea_level":1020.1,"grnd_level":1019.54,"humidity":90,"temp_kf":0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"clouds":{"all":20},"wind":{"speed":2.17,"deg":17.5022},"rain":{"3h":3.17},"sys":{"pod":"d"},"dt_txt":"2018-11-06 09:00:00"},{"dt":1541505600,"main":{"temp":27.62,"temp_min":27.62,"temp_max":27.62,"pressure":1021.46,"sea_level":1022,"grnd_level":1021.46,"humidity":99,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":0},"wind":{"speed":1.26,"deg":176.501},"rain":{"3h":2.32},"sys":{"pod":"n"},"dt_txt":"2018-11-06 12:00:00"},{"dt":1541516400,"main":{"temp":27.35,"temp_min":27.35,"temp_max":27.35,"pressure":1022.87,"sea_level":1023.39,"grnd_level":1022.87,"humidity":99,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":24},"wind":{"speed":1.89,"deg":158.502},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-06 15:00:00"},{"dt":1541527200,"main":{"temp":26.79,"temp_min":26.79,"temp_max":26.79,"pressure":1021.5,"sea_level":1022.14,"grnd_level":1021.5,"humidity":100,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":20},"wind":{"speed":1.76,"deg":170},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-06 18:00:00"},{"dt":1541538000,"main":{"temp":26.63,"temp_min":26.63,"temp_max":26.63,"pressure":1020.93,"sea_level":1021.53,"grnd_level":1020.93,"humidity":100,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"clouds":{"all":64},"wind":{"speed":1.72,"deg":164.003},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-06 21:00:00"},{"dt":1541548800,"main":{"temp":27,"temp_min":27,"temp_max":27,"pressure":1022.2,"sea_level":1022.75,"grnd_level":1022.2,"humidity":100,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"clouds":{"all":48},"wind":{"speed":1.06,"deg":91.501},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-07 00:00:00"},{"dt":1541559600,"main":{"temp":29.07,"temp_min":29.07,"temp_max":29.07,"pressure":1022.54,"sea_level":1023.01,"grnd_level":1022.54,"humidity":97,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"clouds":{"all":12},"wind":{"speed":1.72,"deg":18.5053},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-07 03:00:00"},{"dt":1541570400,"main":{"temp":29.7,"temp_min":29.7,"temp_max":29.7,"pressure":1020.29,"sea_level":1020.93,"grnd_level":1020.29,"humidity":92,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":0},"wind":{"speed":4.61,"deg":348.007},"rain":{"3h":0.46},"sys":{"pod":"d"},"dt_txt":"2018-11-07 06:00:00"},{"dt":1541581200,"main":{"temp":29.59,"temp_min":29.59,"temp_max":29.59,"pressure":1019.9,"sea_level":1020.47,"grnd_level":1019.9,"humidity":90,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":8},"wind":{"speed":2.77,"deg":321.501},"rain":{"3h":1.805},"sys":{"pod":"d"},"dt_txt":"2018-11-07 09:00:00"},{"dt":1541592000,"main":{"temp":28.22,"temp_min":28.22,"temp_max":28.22,"pressure":1021.63,"sea_level":1022.19,"grnd_level":1021.63,"humidity":93,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":36},"wind":{"speed":1.52,"deg":247.002},"rain":{"3h":1.235},"sys":{"pod":"n"},"dt_txt":"2018-11-07 12:00:00"},{"dt":1541602800,"main":{"temp":27.31,"temp_min":27.31,"temp_max":27.31,"pressure":1023.05,"sea_level":1023.54,"grnd_level":1023.05,"humidity":99,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":20},"wind":{"speed":1.66,"deg":209.503},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-07 15:00:00"},{"dt":1541613600,"main":{"temp":26.88,"temp_min":26.88,"temp_max":26.88,"pressure":1021.78,"sea_level":1022.41,"grnd_level":1021.78,"humidity":100,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":20},"wind":{"speed":1.86,"deg":203.5},"rain":{"3h":0.004999999999999},"sys":{"pod":"n"},"dt_txt":"2018-11-07 18:00:00"},{"dt":1541624400,"main":{"temp":26.42,"temp_min":26.42,"temp_max":26.42,"pressure":1021.13,"sea_level":1021.63,"grnd_level":1021.13,"humidity":100,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"clouds":{"all":32},"wind":{"speed":1.95,"deg":198.502},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-07 21:00:00"},{"dt":1541635200,"main":{"temp":27.01,"temp_min":27.01,"temp_max":27.01,"pressure":1022.36,"sea_level":1022.88,"grnd_level":1022.36,"humidity":100,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"clouds":{"all":36},"wind":{"speed":2.07,"deg":171.001},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-08 00:00:00"},{"dt":1541646000,"main":{"temp":29.29,"temp_min":29.29,"temp_max":29.29,"pressure":1022.63,"sea_level":1023.13,"grnd_level":1022.63,"humidity":94,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":{"all":68},"wind":{"speed":0.55,"deg":99.5026},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-08 03:00:00"},{"dt":1541656800,"main":{"temp":30.08,"temp_min":30.08,"temp_max":30.08,"pressure":1020.64,"sea_level":1021.17,"grnd_level":1020.64,"humidity":88,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":{"all":64},"wind":{"speed":3.01,"deg":13.5023},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-08 06:00:00"},{"dt":1541667600,"main":{"temp":29.73,"temp_min":29.73,"temp_max":29.73,"pressure":1019.76,"sea_level":1020.38,"grnd_level":1019.76,"humidity":89,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":36},"wind":{"speed":2.21,"deg":1.0033},"rain":{"3h":0.91},"sys":{"pod":"d"},"dt_txt":"2018-11-08 09:00:00"},{"dt":1541678400,"main":{"temp":26.8,"temp_min":26.8,"temp_max":26.8,"pressure":1021.49,"sea_level":1021.94,"grnd_level":1021.49,"humidity":100,"temp_kf":0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10n"}],"clouds":{"all":56},"wind":{"speed":1.61,"deg":119.502},"rain":{"3h":3.5625},"sys":{"pod":"n"},"dt_txt":"2018-11-08 12:00:00"},{"dt":1541689200,"main":{"temp":26.86,"temp_min":26.86,"temp_max":26.86,"pressure":1022.58,"sea_level":1023.19,"grnd_level":1022.58,"humidity":100,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"clouds":{"all":76},"wind":{"speed":1.06,"deg":149.002},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-08 15:00:00"},{"dt":1541700000,"main":{"temp":27.04,"temp_min":27.04,"temp_max":27.04,"pressure":1021.53,"sea_level":1022.03,"grnd_level":1021.53,"humidity":100,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"clouds":{"all":68},"wind":{"speed":1.31,"deg":152.505},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-08 18:00:00"},{"dt":1541710800,"main":{"temp":26.79,"temp_min":26.79,"temp_max":26.79,"pressure":1020.43,"sea_level":1020.91,"grnd_level":1020.43,"humidity":100,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":20},"wind":{"speed":2.27,"deg":122.003},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-08 21:00:00"},{"dt":1541721600,"main":{"temp":27.68,"temp_min":27.68,"temp_max":27.68,"pressure":1021.55,"sea_level":1022.11,"grnd_level":1021.55,"humidity":100,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"clouds":{"all":24},"wind":{"speed":2.66,"deg":120.006},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-09 00:00:00"},{"dt":1541732400,"main":{"temp":29.82,"temp_min":29.82,"temp_max":29.82,"pressure":1021.95,"sea_level":1022.39,"grnd_level":1021.95,"humidity":93,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":{"all":68},"wind":{"speed":2.06,"deg":87.0087},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-09 03:00:00"},{"dt":1541743200,"main":{"temp":30.48,"temp_min":30.48,"temp_max":30.48,"pressure":1019.51,"sea_level":1020.11,"grnd_level":1019.51,"humidity":88,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"clouds":{"all":48},"wind":{"speed":3.36,"deg":35.0016},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-09 06:00:00"},{"dt":1541754000,"main":{"temp":30.05,"temp_min":30.05,"temp_max":30.05,"pressure":1018.86,"sea_level":1019.55,"grnd_level":1018.86,"humidity":90,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":24},"wind":{"speed":2.48,"deg":56.0006},"rain":{"3h":2.8875},"sys":{"pod":"d"},"dt_txt":"2018-11-09 09:00:00"},{"dt":1541764800,"main":{"temp":26.49,"temp_min":26.49,"temp_max":26.49,"pressure":1021.02,"sea_level":1021.44,"grnd_level":1021.02,"humidity":100,"temp_kf":0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10n"}],"clouds":{"all":44},"wind":{"speed":2.97,"deg":151.501},"rain":{"3h":8.2375},"sys":{"pod":"n"},"dt_txt":"2018-11-09 12:00:00"},{"dt":1541775600,"main":{"temp":27.01,"temp_min":27.01,"temp_max":27.01,"pressure":1022.4,"sea_level":1022.96,"grnd_level":1022.4,"humidity":100,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"clouds":{"all":48},"wind":{"speed":2.02,"deg":121.5},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-09 15:00:00"},{"dt":1541786400,"main":{"temp":26.82,"temp_min":26.82,"temp_max":26.82,"pressure":1021.15,"sea_level":1021.7,"grnd_level":1021.15,"humidity":100,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"clouds":{"all":36},"wind":{"speed":2.17,"deg":128.5},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-09 18:00:00"},{"dt":1541797200,"main":{"temp":26.44,"temp_min":26.44,"temp_max":26.44,"pressure":1020.56,"sea_level":1021.16,"grnd_level":1020.56,"humidity":100,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"clouds":{"all":32},"wind":{"speed":2.43,"deg":140.005},"rain":{},"sys":{"pod":"n"},"dt_txt":"2018-11-09 21:00:00"},{"dt":1541808000,"main":{"temp":26.86,"temp_min":26.86,"temp_max":26.86,"pressure":1022.34,"sea_level":1022.87,"grnd_level":1022.34,"humidity":100,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":88},"wind":{"speed":2.6,"deg":116.014},"rain":{"3h":0.037500000000001},"sys":{"pod":"d"},"dt_txt":"2018-11-10 00:00:00"},{"dt":1541818800,"main":{"temp":28.66,"temp_min":28.66,"temp_max":28.66,"pressure":1022.61,"sea_level":1023.19,"grnd_level":1022.61,"humidity":100,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":{"all":56},"wind":{"speed":2.11,"deg":91.5005},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-10 03:00:00"},{"dt":1541829600,"main":{"temp":29.62,"temp_min":29.62,"temp_max":29.62,"pressure":1020.59,"sea_level":1021.09,"grnd_level":1020.59,"humidity":96,"temp_kf":0},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"clouds":{"all":32},"wind":{"speed":3.83,"deg":41.0005},"rain":{},"sys":{"pod":"d"},"dt_txt":"2018-11-10 06:00:00"},{"dt":1541840400,"main":{"temp":27.24,"temp_min":27.24,"temp_max":27.24,"pressure":1020.09,"sea_level":1020.41,"grnd_level":1020.09,"humidity":100,"temp_kf":0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"clouds":{"all":44},"wind":{"speed":2.56,"deg":93.0006},"rain":{"3h":6.575},"sys":{"pod":"d"},"dt_txt":"2018-11-10 09:00:00"},{"dt":1541851200,"main":{"temp":27.01,"temp_min":27.01,"temp_max":27.01,"pressure":1021.26,"sea_level":1021.85,"grnd_level":1021.26,"humidity":100,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}],"clouds":{"all":56},"wind":{"speed":1.76,"deg":148.006},"rain":{"3h":0.049999999999997},"sys":{"pod":"n"},"dt_txt":"2018-11-10 12:00:00"}],"city":{"id":1993378,"name":"Cideng","coord":{"lat":-6.1674,"lon":106.8062},"country":"ID"}} diff --git a/WeatherTests/Resource/forecastDaily.json b/WeatherTests/Resource/forecastDaily.json new file mode 100644 index 00000000..3dca4940 --- /dev/null +++ b/WeatherTests/Resource/forecastDaily.json @@ -0,0 +1 @@ +{"city":{"id":1993378,"name":"Cideng","coord":{"lon":106.8062,"lat":-6.1674},"country":"ID","population":0},"cod":"200","message":10.2448179,"cnt":7,"list":[{"dt":1541390400,"temp":{"day":29,"min":27.54,"max":29,"night":27.54,"eve":29,"morn":29},"pressure":1021.44,"humidity":88,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":1.06,"deg":224,"clouds":64,"rain":2.48},{"dt":1541476800,"temp":{"day":30.11,"min":26.71,"max":31.08,"night":27.35,"eve":29.78,"morn":26.71},"pressure":1022.75,"humidity":90,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"speed":1.95,"deg":109,"clouds":8,"rain":5.49},{"dt":1541563200,"temp":{"day":29.07,"min":26.63,"max":29.7,"night":27.31,"eve":29.59,"morn":26.63},"pressure":1022.54,"humidity":97,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"speed":1.72,"deg":19,"clouds":12,"rain":3.5},{"dt":1541649600,"temp":{"day":29.29,"min":26.42,"max":30.08,"night":26.86,"eve":29.73,"morn":26.42},"pressure":1022.63,"humidity":94,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"speed":0.55,"deg":100,"clouds":68,"rain":4.47},{"dt":1541736000,"temp":{"day":28.61,"min":26.42,"max":28.61,"night":26.42,"eve":27.37,"morn":26.9},"pressure":1015.86,"humidity":0,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"speed":3.95,"deg":58,"clouds":16,"rain":3.09},{"dt":1541822400,"temp":{"day":29.6,"min":26.43,"max":29.6,"night":26.43,"eve":27.71,"morn":26.84},"pressure":1015.36,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":3.12,"deg":53,"clouds":17,"rain":2.12},{"dt":1541908800,"temp":{"day":29.47,"min":26.16,"max":29.47,"night":26.16,"eve":27.76,"morn":27.13},"pressure":1015.6,"humidity":0,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"speed":3.05,"deg":34,"clouds":57,"rain":7.01}]} diff --git a/WeatherTests/Resource/weather.json b/WeatherTests/Resource/weather.json new file mode 100644 index 00000000..96b2411a --- /dev/null +++ b/WeatherTests/Resource/weather.json @@ -0,0 +1 @@ +{"coord":{"lon":106.79,"lat":-6.18},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"base":"stations","main":{"temp":30.22,"pressure":1008,"humidity":78,"temp_min":28,"temp_max":32},"visibility":4000,"wind":{"speed":4.1,"deg":320},"clouds":{"all":40},"dt":1541916000,"sys":{"type":1,"id":8038,"message":0.0036,"country":"ID","sunrise":1541888729,"sunset":1541933292},"id":1993378,"name":"Cideng","cod":200} diff --git a/WeatherTests/WeatherTests.swift b/WeatherTests/WeatherTests.swift index bb35d1f2..40928eea 100644 --- a/WeatherTests/WeatherTests.swift +++ b/WeatherTests/WeatherTests.swift @@ -24,11 +24,4 @@ class WeatherTests: XCTestCase { // Use XCTAssert and related functions to verify your tests produce the correct results. } - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - } diff --git a/WeatherTests/WeatherViewModelTests.swift b/WeatherTests/WeatherViewModelTests.swift new file mode 100644 index 00000000..fc6f741f --- /dev/null +++ b/WeatherTests/WeatherViewModelTests.swift @@ -0,0 +1,120 @@ +// +// WeatherViewModelTests.swift +// WeatherTests +// +// Created by Christian Huang on 11/11/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import XCTest +@testable import Weather + +class WeatherViewModelTests: XCTestCase { + var weatherViewModel = WeatherViewModel() + var networkManager = MockNetworkManager(apiKey: "testAPIKey", environment: .qa) + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + weatherViewModel.networkManager = networkManager + weatherViewModel.location = Coordinate(longitude: 1, latitude: 2) + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testFetchWeatherError() { + networkManager.mockWeatherResponse = nil + networkManager.mockError = "This is an error" + + weatherViewModel.fetchWeather() + + XCTAssertEqual(weatherViewModel.status, networkManager.mockError) + } + + func testFetchWeather() { + let data = loadDataFromBundle(withName: "weather", extension: "json") + let weatherResponse = try! JSONDecoder().decode(WeatherResponse.self, from: data) + + networkManager.mockWeatherResponse = weatherResponse + networkManager.mockError = nil + + weatherViewModel.fetchWeather() + + XCTAssertNil(weatherViewModel.status) + + XCTAssertEqual(weatherViewModel.area, "Cideng") + XCTAssertEqual(weatherViewModel.weather, "Rain") + XCTAssertEqual(weatherViewModel.temperature, 30.22) + XCTAssertEqual(weatherViewModel.summary, "Today: Rain currently. It's 30°; The high today was forecast as 32°.") + XCTAssertEqual(weatherViewModel.timeString, "13:00") + XCTAssertEqual(weatherViewModel.dayString, "Sunday") + XCTAssertEqual(weatherViewModel.dayShortString, "Sun") + XCTAssertEqual(weatherViewModel.temperatureMin, 28) + XCTAssertEqual(weatherViewModel.temperatureMax, 32) + XCTAssertEqual(weatherViewModel.sunrise, "05:25") + XCTAssertEqual(weatherViewModel.sunriseTime, 19500) + XCTAssertEqual(weatherViewModel.sunset, "17:48") + XCTAssertEqual(weatherViewModel.sunsetTime, 64080) + XCTAssertEqual(weatherViewModel.dayTime, .day) + XCTAssertEqual(weatherViewModel.windSpeed, "4.1 mps") + XCTAssertEqual(weatherViewModel.pressure, "1008 hPa") + XCTAssertEqual(weatherViewModel.humidity, "78 %") + XCTAssertEqual(weatherViewModel.icon, "http://openweathermap.org/img/w/10d.png") + + } + + func testUpdateWeatherWithForecast() { + let data = loadDataFromBundle(withName: "forecast", extension: "json") + let forecastResponse = try! JSONDecoder().decode(ForecastResponse.self, from: data) + + weatherViewModel.sunriseTime = 19500 + weatherViewModel.sunsetTime = 64080 + + weatherViewModel.updateWeatherData(forecast: forecastResponse.list[2]) + + XCTAssertEqual(weatherViewModel.weather, "Clouds") + XCTAssertEqual(weatherViewModel.temperature, 26.71) + XCTAssertEqual(weatherViewModel.timeString, "04:00") + XCTAssertEqual(weatherViewModel.dayString, "Tuesday") + XCTAssertEqual(weatherViewModel.dayShortString, "Tue") + XCTAssertEqual(weatherViewModel.dayTime, .night) + XCTAssertEqual(weatherViewModel.temperatureMin, 26.71) + XCTAssertEqual(weatherViewModel.temperatureMax, 26.75) + XCTAssertEqual(weatherViewModel.windSpeed, "2.64 mps") + XCTAssertEqual(weatherViewModel.pressure, "1021 hPa") + XCTAssertEqual(weatherViewModel.humidity, "100 %") + XCTAssertEqual(weatherViewModel.icon, "http://openweathermap.org/img/w/03n.png") + } + + func testUpdateWeatherWithForecastDaily() { + let data = loadDataFromBundle(withName: "forecastDaily", extension: "json") + let forecastDailyResponse = try! JSONDecoder().decode(ForecastDailyResponse.self, from: data) + + weatherViewModel.sunriseTime = 19500 + weatherViewModel.sunsetTime = 64080 + + weatherViewModel.updateWeatherData(forecastDaily: forecastDailyResponse.list[3]) + + XCTAssertEqual(weatherViewModel.weather, "Rain") + XCTAssertEqual(weatherViewModel.temperature, 29.29) + XCTAssertEqual(weatherViewModel.timeString, "11:00") + XCTAssertEqual(weatherViewModel.dayString, "Thursday") + XCTAssertEqual(weatherViewModel.dayShortString, "Thu") + XCTAssertEqual(weatherViewModel.dayTime, .day) + XCTAssertEqual(weatherViewModel.temperatureMin, 26.42) + XCTAssertEqual(weatherViewModel.temperatureMax, 30.08) + XCTAssertEqual(weatherViewModel.windSpeed, "0.55 mps") + XCTAssertEqual(weatherViewModel.pressure, "1022 hPa") + XCTAssertEqual(weatherViewModel.humidity, "94 %") + XCTAssertEqual(weatherViewModel.icon, "http://openweathermap.org/img/w/10d.png") + } + + + +} From d12c3f7fed47f841b76aa5b844969fe87aae37df Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Mon, 12 Nov 2018 04:52:21 +0700 Subject: [PATCH 18/23] Add WindDegree feature --- Weather/Base.lproj/Main.storyboard | 23 +++++++++++++++++++++- Weather/View/MyWeatherViewController.swift | 12 +++++++++++ Weather/ViewModel/WeatherViewModel.swift | 4 ++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Weather/Base.lproj/Main.storyboard b/Weather/Base.lproj/Main.storyboard index 37d98c24..8c624f98 100644 --- a/Weather/Base.lproj/Main.storyboard +++ b/Weather/Base.lproj/Main.storyboard @@ -285,6 +285,22 @@ + + + + + + + + + + + @@ -375,7 +391,7 @@ @@ -747,6 +767,7 @@ + diff --git a/Weather/View/MyWeatherViewController.swift b/Weather/View/MyWeatherViewController.swift index acb96391..0a3da0a6 100644 --- a/Weather/View/MyWeatherViewController.swift +++ b/Weather/View/MyWeatherViewController.swift @@ -16,6 +16,7 @@ class MyWeatherViewController: UIViewController { @IBOutlet private weak var weatherBaseView: UIView! @IBOutlet private weak var weatherCenterView: UIView! @IBOutlet private weak var compassBaseView: UIView! + @IBOutlet private weak var windDirectionView: UIView! @IBOutlet private weak var weatherImageView: UIImageView! @IBOutlet private weak var loadingView: UIActivityIndicatorView! @IBOutlet private weak var temperatureLabel: UILabel! @@ -84,10 +85,13 @@ extension MyWeatherViewController { closeButton.setFullRoundedCorner() weatherBaseView.setFullRoundedCorner() compassBaseView.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 4) + windDirectionView.alpha = 0 weatherCenterView.setFullRoundedCorner(borderWidth: 1, borderColor: UIColor.lightGray.cgColor) } private func updateWeatherData() { + let degreeToRad: CGFloat = CGFloat.pi / 180 + locationLabel.text = "\(weatherViewModel.area), \(weatherViewModel.country)" weatherLabel.text = weatherViewModel.weather temperatureLabel.text = "\(Int(weatherViewModel.temperature))" @@ -97,6 +101,14 @@ extension MyWeatherViewController { minTemperatureLabel.text = "Min \(Int(weatherViewModel.temperatureMin))°" maxTemperatureLabel.text = "Max \(Int(weatherViewModel.temperatureMax))°" windLabel.text = "Wind \(weatherViewModel.windSpeed)" + if let windDegree = weatherViewModel.windDegree { + UIView.animate(withDuration: 0.3) { + self.windDirectionView.transform = CGAffineTransform(rotationAngle: CGFloat(windDegree) * degreeToRad) + self.windDirectionView.alpha = 1 + } + } else { + windDirectionView.alpha = 0 + } pressureLabel.text = weatherViewModel.pressure humidityLabel.text = weatherViewModel.humidity weatherImageView.kf.setImage(with: URL(string: weatherViewModel.icon)) diff --git a/Weather/ViewModel/WeatherViewModel.swift b/Weather/ViewModel/WeatherViewModel.swift index e41eca86..05a48e35 100644 --- a/Weather/ViewModel/WeatherViewModel.swift +++ b/Weather/ViewModel/WeatherViewModel.swift @@ -27,6 +27,7 @@ class WeatherViewModel: NSObject { var sunset = "00:00" var sunsetTime: TimeInterval = 0 var windSpeed = "" + var windDegree: Double? = 0 var pressure = "" var humidity = "" var icon = "" @@ -99,6 +100,7 @@ extension WeatherViewModel: WeatherDetailProtocol { temperatureMin = forecast.main.temperatureMin temperatureMax = forecast.main.temperatureMax windSpeed = "\(forecast.wind.speed) mps" + windDegree = forecast.wind.degree pressure = String(format: "%d hPa", Int(forecast.main.pressure)) humidity = String(format: "%d %%", forecast.main.humidity) icon = "http://openweathermap.org/img/w/\(forecast.weather[0].icon).png" @@ -117,6 +119,7 @@ extension WeatherViewModel: WeatherDetailProtocol { temperatureMin = forecastDaily.temperature.min temperatureMax = forecastDaily.temperature.max windSpeed = "\(forecastDaily.speed) mps" + windDegree = forecastDaily.degree pressure = String(format: "%d hPa", Int(forecastDaily.pressure)) humidity = String(format: "%d %%", forecastDaily.humidity) icon = "http://openweathermap.org/img/w/\(forecastDaily.weather[0].icon).png" @@ -161,6 +164,7 @@ extension WeatherViewModel { sunsetTime = DateConverter.timeIntervalToDayTimeInterval(response.system.sunset) dayTime = self.calcDayTime(timeInterval: response.dayTime) windSpeed = "\(response.wind.speed) mps" + windDegree = response.wind.degree pressure = String(format: "%d hPa", Int(response.main.pressure)) humidity = String(format: "%d %%", response.main.humidity) icon = "http://openweathermap.org/img/w/\(response.weather[0].icon).png" From d2b2d9ecd2a54453bd7b81af26de88054c6e6da8 Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Mon, 12 Nov 2018 05:33:07 +0700 Subject: [PATCH 19/23] Add simple retry fetching weather data --- README.md | 46 +++++++++++++++++++ .../ForecastDailyListViewModel.swift | 6 +++ Weather/ViewModel/ForecastListViewModel.swift | 6 +++ Weather/ViewModel/WeatherViewModel.swift | 6 +++ WeatherTests/Extension/XCTestCase.swift | 44 ++++++++++++++++++ 5 files changed, 108 insertions(+) create mode 100755 WeatherTests/Extension/XCTestCase.swift diff --git a/README.md b/README.md index b3fe3b4f..755fe1b5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,49 @@ +Weather +====== + +This repository contains a sample Weather app for requesting Weather's API. + + +--- +# Installation + +To install the dependencies +* Open a terminal and cd to the directory containing the Podfile +* Run the `pod install` command + +(For further details regarding cocoapod installation see https://cocoapods.org/) + + +--- +# Existing Functionalities + +The app is currently able to load Weather data from Weather's API, and show it in 2 UI designs (list form and dashboard) + +* When the app loads, it will load the Weather data from Weather's API, and show them in list form ( like iOS default "Weather" app) +* Upon selecting "Other Design", it will open a view with Dashboard like presentation of Weather data + +--- +# Development Steps + +1. Create new project based on single view app +2. Create folders for MVVM pattern +3. Add WeatherViewController and Design the UI layout to Weather data +4. Add LocationManager to detect user current location +5. Add Networking Layer to handle the Weather's API +6. Add ViewModel and Model, that will show the Weather data at WeatherViewController +7. Add Forecast and ForecastDaily features +8. Add scroll effect feature +9. Add pods: Kingfisher +10. Add MyWeatherViewController and Design the Dashboard UI layout +11. Implement existing ViewModels to MyWeatherViewController +10. Add Unit & UI Test to test the process + + + + +================================================================== + + # Work sample - Application developer ## Assignment diff --git a/Weather/ViewModel/ForecastDailyListViewModel.swift b/Weather/ViewModel/ForecastDailyListViewModel.swift index 94ba7040..2d8d6860 100644 --- a/Weather/ViewModel/ForecastDailyListViewModel.swift +++ b/Weather/ViewModel/ForecastDailyListViewModel.swift @@ -27,6 +27,12 @@ extension ForecastDailyListViewModel { if let error = error { print("Network Error: \(error)") self.status = error + + DispatchQueue.main.async { + let _ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in + self.fetchForecastDaily(coordinate: coordinate) + }) + } } else { self.status = nil self.processForecastDailyResponse(forecastDailyResponse!) diff --git a/Weather/ViewModel/ForecastListViewModel.swift b/Weather/ViewModel/ForecastListViewModel.swift index 0d2fb03f..5aefd29b 100644 --- a/Weather/ViewModel/ForecastListViewModel.swift +++ b/Weather/ViewModel/ForecastListViewModel.swift @@ -27,6 +27,12 @@ extension ForecastListViewModel { if let error = error { print("Network Error: \(error)") self.status = error + + DispatchQueue.main.async { + let _ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in + self.fetchForecast(coordinate: coordinate) + }) + } } else { self.status = nil self.processForecastResponse(forecastResponse!) diff --git a/Weather/ViewModel/WeatherViewModel.swift b/Weather/ViewModel/WeatherViewModel.swift index 05a48e35..b8d12d1a 100644 --- a/Weather/ViewModel/WeatherViewModel.swift +++ b/Weather/ViewModel/WeatherViewModel.swift @@ -77,6 +77,12 @@ extension WeatherViewModel { if let error = error { print("Network Error: \(error)") self.status = error + + DispatchQueue.main.async { + let _ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in + self.fetchWeather() + }) + } } else { self.status = nil self.processWeatherResponse(weatherResponse!) diff --git a/WeatherTests/Extension/XCTestCase.swift b/WeatherTests/Extension/XCTestCase.swift new file mode 100755 index 00000000..0672f18d --- /dev/null +++ b/WeatherTests/Extension/XCTestCase.swift @@ -0,0 +1,44 @@ +// +// XCTestCase.swift +// UnitTests +// +// Created by Christian Huang on 01/09/18. +// Copyright © 2018 Christian Huang. All rights reserved. +// + +import XCTest + +extension XCTestCase { + + // MARK: - Helper Methods + + func loadDataFromBundle(withName name: String, extension: String) -> Data { + let bundle = Bundle(for: classForCoder) + let url = bundle.url(forResource: name, withExtension: `extension`) + + let data = try! Data(contentsOf: url!) + return data + } + + func loadJSONDictionaryFromBundle(withName name: String, extension: String) -> Dictionary { + let bundle = Bundle(for: classForCoder) + let url = bundle.url(forResource: name, withExtension: `extension`) + + let data = try! Data(contentsOf: url!) + let jsonResult = try! JSONSerialization.jsonObject(with: data, options: []) + let jsonDic = jsonResult as! Dictionary + return jsonDic + } + + func loadJSONArrayFromBundle(withName name: String, extension: String) -> [Dictionary] { + let bundle = Bundle(for: classForCoder) + let url = bundle.url(forResource: name, withExtension: `extension`) + + let data = try! Data(contentsOf: url!) + let jsonResult = try! JSONSerialization.jsonObject(with: data, options: []) + let jsonDicArray = jsonResult as! [Dictionary] + return jsonDicArray + } + + +} From 8a75e7ffe0dd36de4c48a2130787c1bce6471ca6 Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Mon, 12 Nov 2018 05:40:41 +0700 Subject: [PATCH 20/23] Modify README with Instructions and Development steps --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 755fe1b5..00f9eb27 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ The app is currently able to load Weather data from Weather's API, and show it i 9. Add pods: Kingfisher 10. Add MyWeatherViewController and Design the Dashboard UI layout 11. Implement existing ViewModels to MyWeatherViewController -10. Add Unit & UI Test to test the process +10. Add Unit Test to test the process From f84f481f83462d30b40f022c29d23e639e6fc595 Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Mon, 12 Nov 2018 05:51:33 +0700 Subject: [PATCH 21/23] Minor update --- Weather/Service/Networking/EndPoint/WeatherEndPoint.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Weather/Service/Networking/EndPoint/WeatherEndPoint.swift b/Weather/Service/Networking/EndPoint/WeatherEndPoint.swift index 683ea290..6a93f78a 100755 --- a/Weather/Service/Networking/EndPoint/WeatherEndPoint.swift +++ b/Weather/Service/Networking/EndPoint/WeatherEndPoint.swift @@ -1,5 +1,5 @@ // -// WeatherPoint.swift +// WeatherEndPoint.swift // NetworkLayer // // @@ -45,7 +45,7 @@ extension WeatherAPI: EndPointType { } } - /** generate task based on requested Auto1's API. */ + /** generate task based on requested Weather's API. */ var task: HTTPTask { switch self { case .weather(let latitude, let longitude, let apiKey): From b2fcf02db636f8aea81f1553b7438d6b5901d329 Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Mon, 12 Nov 2018 07:48:27 +0700 Subject: [PATCH 22/23] Documentation adjustment --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 00f9eb27..cd2981a5 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ To install the dependencies (For further details regarding cocoapod installation see https://cocoapods.org/) +Notes: +Don't forget to change the apiKey for the Weather's API in "Constants.swift" --- # Existing Functionalities @@ -22,6 +24,7 @@ The app is currently able to load Weather data from Weather's API, and show it i * When the app loads, it will load the Weather data from Weather's API, and show them in list form ( like iOS default "Weather" app) * Upon selecting "Other Design", it will open a view with Dashboard like presentation of Weather data + --- # Development Steps From 5a0975f0f39f97ddac107b7c216f8c08443b069f Mon Sep 17 00:00:00 2001 From: Christian Huang Date: Sat, 5 Jan 2019 04:44:54 +0700 Subject: [PATCH 23/23] Properties adjustment --- Weather/ViewModel/ForecastDailyListViewModel.swift | 2 +- Weather/ViewModel/ForecastListViewModel.swift | 2 +- Weather/ViewModel/WeatherViewModel.swift | 10 +++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Weather/ViewModel/ForecastDailyListViewModel.swift b/Weather/ViewModel/ForecastDailyListViewModel.swift index 2d8d6860..55d4f22e 100644 --- a/Weather/ViewModel/ForecastDailyListViewModel.swift +++ b/Weather/ViewModel/ForecastDailyListViewModel.swift @@ -11,7 +11,7 @@ import Foundation class ForecastDailyListViewModel: NSObject { private var forecastDailyRawList = [ForecastDaily]() - var forecastDailyList: Dynamic<[ForecastDailyViewModel]> = Dynamic([]) + let forecastDailyList: Dynamic<[ForecastDailyViewModel]> = Dynamic([]) var forecastDailyCount: Int { return forecastDailyList.value.count } diff --git a/Weather/ViewModel/ForecastListViewModel.swift b/Weather/ViewModel/ForecastListViewModel.swift index 5aefd29b..313d838b 100644 --- a/Weather/ViewModel/ForecastListViewModel.swift +++ b/Weather/ViewModel/ForecastListViewModel.swift @@ -10,7 +10,7 @@ import UIKit class ForecastListViewModel: NSObject { private var forecastRawList = [Forecast]() - var forecastList: Dynamic<[ForecastViewModel]> = Dynamic([]) + let forecastList: Dynamic<[ForecastViewModel]> = Dynamic([]) var forecastCount: Int { return forecastList.value.count } diff --git a/Weather/ViewModel/WeatherViewModel.swift b/Weather/ViewModel/WeatherViewModel.swift index b8d12d1a..e951c73c 100644 --- a/Weather/ViewModel/WeatherViewModel.swift +++ b/Weather/ViewModel/WeatherViewModel.swift @@ -181,14 +181,18 @@ extension WeatherViewModel { private func calcDayTime(timeInterval: TimeInterval)-> DayTime { let time = DateConverter.timeIntervalToDayTimeInterval(timeInterval) + var adjustment: TimeInterval = 7200 + if sunriseTime + adjustment > sunsetTime - adjustment { + adjustment = (sunsetTime - sunriseTime) / 4 + } switch time { case 0..