From 26d670ea01cadc9e1818f5c9115e57a8f7b2690c Mon Sep 17 00:00:00 2001 From: Craig Holliday Date: Wed, 1 Nov 2017 15:18:56 -0500 Subject: [PATCH] v1.2 App Store Release (#39) * Refactor To MVVM (#29) * Reworking classes and adding repositories * Update repository * ViewModelController and Repos are working * Lots of additions and reworking of views - Fixes for repositories - 60fps scrolling - Fixed html encodings - Started switching out some view controllers for cleaner versions * Fixed FilterObject * Fix recommened api request * Added remoteCommandManager toggle change playback position Fix for this issue: https://github.com/SoftwareEngineeringDaily/se-daily-iOS/issues/25 * Build for beta and fixed CollectionView Layout scaling * fixed audio view label * Clean up clean up Fixed User Model Fixed repos and search controller Deleted a lot of old code * Removed and cleaned up code/files Also fixed search controller since there was an issue with calling the api too many times after we returned 0 results * Reworking classes and adding repositories * Update repository * ViewModelController and Repos are working * Lots of additions and reworking of views - Fixes for repositories - 60fps scrolling - Fixed html encodings - Started switching out some view controllers for cleaner versions * Fixed FilterObject * Fix recommened api request * Added remoteCommandManager toggle change playback position Fix for this issue: https://github.com/SoftwareEngineeringDaily/se-daily-iOS/issues/25 * Build for beta and fixed CollectionView Layout scaling * fixed audio view label * Clean up clean up Fixed User Model Fixed repos and search controller Deleted a lot of old code * Removed and cleaned up code/files Also fixed search controller since there was an issue with calling the api too many times after we returned 0 results * After rebase with a few fixes * Remove comment * Review Fixes * Removed Kingfisher in favor for SDWebImage This looks to fix a memory issue with Kingfisher caching all the images and using a lot of memory. * Fixed models and voting on header view * Forgot to push this pod update * Fixed tab bar not selecting first index on load. Fixed Image caching * Fix skeletonCollectionView stopping when leaving a view * 1.2 beta build 11 * v1.2 build 12 --- Constants/L10nEnum.swift | 2 + Podfile | 38 +- Podfile.lock | 30 +- SEDaily-IOS.xcodeproj/project.pbxproj | 244 +++++++------ SEDaily-IOS/API.swift | 280 +++++---------- SEDaily-IOS/AnswersTracker.swift | 14 +- SEDaily-IOS/AppDelegate.swift | 18 - SEDaily-IOS/AudioView.swift | 64 ++-- SEDaily-IOS/AudioViewManager.swift | 34 +- SEDaily-IOS/Base.lproj/Localizable.strings | 1 + SEDaily-IOS/CollectionReusableView.swift | 37 -- SEDaily-IOS/ContainerViewController.swift | 6 +- SEDaily-IOS/CustomTabViewController.swift | 65 +--- SEDaily-IOS/DetailTableViewCell.swift | 211 ----------- SEDaily-IOS/Extensions.swift | 57 --- SEDaily-IOS/FilterObject.swift | 38 ++ .../GeneralCollectionViewController.swift | 337 ++++++------------ SEDaily-IOS/HeaderView.swift | 166 ++++----- SEDaily-IOS/Helpers.swift | 42 ++- SEDaily-IOS/Info.plist | 2 +- .../JustForYouCollectionViewController.swift | 157 -------- SEDaily-IOS/LoginViewController.swift | 92 ++--- SEDaily-IOS/NotificationCenterExtension.swift | 13 - SEDaily-IOS/ObjectExtensions.swift | 86 ----- SEDaily-IOS/Podcast.swift | 121 +++++++ SEDaily-IOS/PodcastCollectionViewCell.swift | 114 ++---- SEDaily-IOS/PodcastDataSource.swift | 130 +++++++ SEDaily-IOS/PodcastDescriptionView.swift | 48 +++ SEDaily-IOS/PodcastDetailViewController.swift | 39 ++ SEDaily-IOS/PodcastModel.swift | 206 ----------- SEDaily-IOS/PodcastPageViewController.swift | 86 ++--- SEDaily-IOS/PodcastRepository.swift | 129 +++++++ SEDaily-IOS/PodcastTableViewCell.swift | 25 +- SEDaily-IOS/PodcastViewModel.swift | 121 +++++++ SEDaily-IOS/PodcastViewModelController.swift | 126 +++++++ .../PostDetailTableViewController.swift | 89 ----- SEDaily-IOS/SearchTableViewController.swift | 93 ++--- SEDaily-IOS/SingleLabelTableViewCell.swift | 58 --- SEDaily-IOS/SkeletonCollectionView.swift | 10 +- SEDaily-IOS/TempObjectMapper.swift | 214 ----------- SEDaily-IOS/TopCollectionViewController.swift | 149 -------- SEDaily-IOS/UserModel.swift | 161 +++++---- SEDaily-IOS/fr.lproj/Localizable.strings | 2 +- SEDaily-IOSTests/Info.plist | 2 +- SEDaily-IOSTests/SEDaily_IOSTests.swift | 2 + fastlane/Fastfile | 2 +- fastlane/README.md | 2 +- 47 files changed, 1547 insertions(+), 2416 deletions(-) delete mode 100644 SEDaily-IOS/CollectionReusableView.swift delete mode 100644 SEDaily-IOS/DetailTableViewCell.swift delete mode 100644 SEDaily-IOS/Extensions.swift create mode 100644 SEDaily-IOS/FilterObject.swift delete mode 100644 SEDaily-IOS/JustForYouCollectionViewController.swift delete mode 100644 SEDaily-IOS/NotificationCenterExtension.swift delete mode 100644 SEDaily-IOS/ObjectExtensions.swift create mode 100644 SEDaily-IOS/Podcast.swift create mode 100644 SEDaily-IOS/PodcastDataSource.swift create mode 100644 SEDaily-IOS/PodcastDescriptionView.swift create mode 100644 SEDaily-IOS/PodcastDetailViewController.swift delete mode 100644 SEDaily-IOS/PodcastModel.swift create mode 100644 SEDaily-IOS/PodcastRepository.swift create mode 100644 SEDaily-IOS/PodcastViewModel.swift create mode 100644 SEDaily-IOS/PodcastViewModelController.swift delete mode 100644 SEDaily-IOS/PostDetailTableViewController.swift delete mode 100644 SEDaily-IOS/SingleLabelTableViewCell.swift delete mode 100644 SEDaily-IOS/TempObjectMapper.swift delete mode 100644 SEDaily-IOS/TopCollectionViewController.swift diff --git a/Constants/L10nEnum.swift b/Constants/L10nEnum.swift index 05e0c94..2f8a005 100644 --- a/Constants/L10nEnum.swift +++ b/Constants/L10nEnum.swift @@ -60,6 +60,8 @@ enum L10n { static let logoutTitle = L10n.tr("Localizable", "LogoutTitle") /// Password static let passwordPlaceholder = L10n.tr("Localizable", "PasswordPlaceholder") + /// Play + static let play = L10n.tr("Localizable", "Play") /// Sign Up static let signUpButtonTitle = L10n.tr("Localizable", "SignUpButtonTitle") /// Just For You diff --git a/Podfile b/Podfile index 9da14b7..b699266 100644 --- a/Podfile +++ b/Podfile @@ -7,29 +7,29 @@ target 'SEDaily-IOS' do # Pods for SEDaily-IOS pod 'Alamofire' - pod 'ObjectMapper' - pod 'RealmSwift' - pod 'SwiftyJSON' - pod 'SwifterSwift' - pod 'SnapKit' - pod 'Reusable' - pod 'SwiftyBeaver' - pod 'KoalaTeaFlowLayout' - pod 'UIFontComplete' - pod 'SwiftIcons', :git => 'https://github.com/themisterholliday/SwiftIcons.git', :branch => 'swift-4' - pod 'IQKeyboardManagerSwift' - pod 'Eureka' - pod 'SideMenu' pod 'ActiveLabel', :git => 'https://github.com/optonaut/ActiveLabel.swift.git' - pod 'Kingfisher' - pod 'Fabric' pod 'Crashlytics' - pod 'KTResponsiveUI' + pod 'Disk' + pod 'Eureka' + pod 'Fabric' + pod 'IQKeyboardManagerSwift' + pod 'KoalaTeaFlowLayout' pod 'KoalaTeaPlayer' - pod 'Tabman' - pod 'SwiftGen' + pod 'KTResponsiveUI' + pod 'Reusable' + pod 'SDWebImage' + pod 'SideMenu' pod 'Skeleton' - + pod 'SnapKit' + pod 'SwiftyBeaver' + pod 'SwifterSwift' + pod 'SwiftIcons', :git => 'https://github.com/themisterholliday/SwiftIcons.git', :branch => 'swift-4' + pod 'SwiftGen' + pod 'SwiftSoup' + pod 'SwiftyJSON' + pod 'Tabman' + pod 'UIFontComplete' + target 'SEDaily-IOSTests' do inherit! :search_paths # Pods for testing diff --git a/Podfile.lock b/Podfile.lock index 2a32c9e..ff40ab0 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -3,27 +3,24 @@ PODS: - Alamofire (4.5.1) - Crashlytics (3.9.0): - Fabric (~> 1.7.0) + - Disk (0.3.1) - Eureka (4.0.1) - Fabric (1.7.0) - IQKeyboardManagerSwift (5.0.3) - - Kingfisher (4.1.0) - KoalaTeaFlowLayout (0.3.1) - KoalaTeaPlayer (0.1.6) - KTResponsiveUI (0.2.3): - SwiftIcons - - ObjectMapper (3.0.0) - Pageboy (2.0.0) - PureLayout (3.0.2) - - Realm (2.10.2): - - Realm/Headers (= 2.10.2) - - Realm/Headers (2.10.2) - - RealmSwift (2.10.2): - - Realm (= 2.10.2) - Reusable (4.0.0): - Reusable/Storyboard (= 4.0.0) - Reusable/View (= 4.0.0) - Reusable/Storyboard (4.0.0) - Reusable/View (4.0.0) + - SDWebImage (4.1.2): + - SDWebImage/Core (= 4.1.2) + - SDWebImage/Core (4.1.2) - SideMenu (3.1.1) - Skeleton (0.1.0) - SnapKit (4.0.0) @@ -40,7 +37,9 @@ PODS: - SwifterSwift/Foundation (4.0.1) - SwifterSwift/SwiftStdlib (4.0.1) - SwifterSwift/UIKit (4.0.1) + - SwiftGen (5.1.2) - SwiftIcons (1.5.1) + - SwiftSoup (1.5.4) - SwiftyBeaver (1.4.2) - SwiftyJSON (3.1.4) - Tabman (1.0.3): @@ -52,22 +51,22 @@ DEPENDENCIES: - ActiveLabel (from `https://github.com/optonaut/ActiveLabel.swift.git`) - Alamofire - Crashlytics + - Disk - Eureka - Fabric - IQKeyboardManagerSwift - - Kingfisher - KoalaTeaFlowLayout - KoalaTeaPlayer - KTResponsiveUI - - ObjectMapper - - RealmSwift - Reusable + - SDWebImage - SideMenu - Skeleton - SnapKit - SwifterSwift - SwiftGen - SwiftIcons (from `https://github.com/themisterholliday/SwiftIcons.git`, branch `swift-4`) + - SwiftSoup - SwiftyBeaver - SwiftyJSON - Tabman @@ -92,30 +91,29 @@ SPEC CHECKSUMS: ActiveLabel: faa96b5f50507770536a3e48a4cf291ee88fb7db Alamofire: 2d95912bf4c34f164fdfc335872e8c312acaea4a Crashlytics: 64aad5dd97249dd3ff94b979fea140144590cdd3 + Disk: 64f0f95a4163314aba6f09f010f17de964762140 Eureka: c8bd5cc07143b6f66268c208d28a246c99b41955 Fabric: e6be012366472553807dada21243c5ab8d904151 IQKeyboardManagerSwift: e257e513744cf32123eca7061272acea0246fc83 - Kingfisher: f14df8cbe576bf55f211fa589e9869bceb4ea87d KoalaTeaFlowLayout: 219c45709d0873d405b2de1b27080a9c411246b7 KoalaTeaPlayer: f160b122c1c3f877682ba12518b0e9c2e41f887f KTResponsiveUI: 6e17f26edb8ccefd935b14de8775e0d97e7ef380 - ObjectMapper: 92230db59bf8f341a5c3a3cf0b9fbdde3cf0d87f Pageboy: a43a4e34fad98ebfa415a5d33cd0389e5005a101 PureLayout: 4d550abe49a94f24c2808b9b95db9131685fe4cd - Realm: 0ef72b837fb67e9f4b098bac771ddd72c7fdbb69 - RealmSwift: 07a9ae0505091eda6b2ee7c190c3786d6e90a7b0 Reusable: 98e5fff1e0e2e00872199699b276dde08ee56c07 + SDWebImage: cb6f9f266a9977741efcbc21e618e8be3734c774 SideMenu: 1bf32e90afaa2dc0e0ab68e938a0ff85014c4217 Skeleton: b76249b7d6a73a9913e164c8030647a3e005c700 SnapKit: a42d492c16e80209130a3379f73596c3454b7694 - SwiftGen: dd2892e7fb008151f30e6ce0cf465b8b8d89e80e SwifterSwift: 5f406a5f831343312d6d5b34282b57ef3f52c40b + SwiftGen: dd2892e7fb008151f30e6ce0cf465b8b8d89e80e SwiftIcons: b20641adce6b71fcb2e72389b4b5f2e87252df99 + SwiftSoup: f8e924cd65a1d74eb7ba1b6f0a9a61feacaff690 SwiftyBeaver: 91057725648ee4980308f1650af077d04b3654a0 SwiftyJSON: c2842d878f95482ffceec5709abc3d05680c0220 Tabman: f922ae6412f482234b58213f51caa5f0549a5949 UIFontComplete: 7e3ce7f0a12d2529fb07f537e262aabfa87df280 -PODFILE CHECKSUM: c5fe254633620bed66f10278ada8111c9c4aaaac +PODFILE CHECKSUM: 649ceef84ecfcef810f531508569460e85464ae8 COCOAPODS: 1.3.1 diff --git a/SEDaily-IOS.xcodeproj/project.pbxproj b/SEDaily-IOS.xcodeproj/project.pbxproj index 47badfe..db3f6f3 100644 --- a/SEDaily-IOS.xcodeproj/project.pbxproj +++ b/SEDaily-IOS.xcodeproj/project.pbxproj @@ -10,10 +10,10 @@ 01BB1D6D1F29999E004A912E /* PodcastPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01BB1D6C1F29999E004A912E /* PodcastPageViewController.swift */; }; 161F3DE61F61F73D00A8F825 /* SearchTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 161F3DE51F61F73D00A8F825 /* SearchTableViewController.swift */; }; 161F3DEA1F62703100A8F825 /* PodcastTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 161F3DE91F62703100A8F825 /* PodcastTableViewCell.swift */; }; + 16307FCA1F8FDD02001783CB /* Podcast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16307FC91F8FDD02001783CB /* Podcast.swift */; }; + 16307FCC1F8FEE3A001783CB /* PodcastViewModelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16307FCB1F8FEE3A001783CB /* PodcastViewModelController.swift */; }; 1649A78A1F204EC6005C4A6E /* ContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1649A7891F204EC6005C4A6E /* ContainerViewController.swift */; }; 164C71051F021AC8003803BC /* CustomTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164C71041F021AC8003803BC /* CustomTabViewController.swift */; }; - 164FE9D71F02CD99009419CA /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164FE9D61F02CD99009419CA /* Extensions.swift */; }; - 164FE9D91F02D611009419CA /* CollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164FE9D81F02D611009419CA /* CollectionReusableView.swift */; }; 164FE9DC1F02DAB5009419CA /* PodcastCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164FE9DB1F02DAB5009419CA /* PodcastCollectionViewCell.swift */; }; 164FE9E11F02EE67009419CA /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164FE9E01F02EE67009419CA /* LoginViewController.swift */; }; 164FE9E51F02EE83009419CA /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164FE9E41F02EE83009419CA /* API.swift */; }; @@ -22,29 +22,26 @@ 164FE9EB1F02F340009419CA /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164FE9EA1F02F340009419CA /* Helpers.swift */; }; 164FE9ED1F02F7E2009419CA /* UIButtonExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164FE9EC1F02F7E2009419CA /* UIButtonExtension.swift */; }; 164FE9EF1F03065C009419CA /* NavigationControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164FE9EE1F03065C009419CA /* NavigationControllerExtension.swift */; }; - 164FE9F11F030A2E009419CA /* PodcastModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164FE9F01F030A2E009419CA /* PodcastModel.swift */; }; - 165A5B681F7C32FD00FDE80B /* TempObjectMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165A5B671F7C32FD00FDE80B /* TempObjectMapper.swift */; }; + 165484551F902D3F005AEA23 /* GeneralCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165484541F902D3F005AEA23 /* GeneralCollectionViewController.swift */; }; 166036211F266FF300A22B7B /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 166036201F266FF300A22B7B /* Notifications.swift */; }; - 1668124A1F350737007F288B /* GeneralCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 166812491F350737007F288B /* GeneralCollectionViewController.swift */; }; - 1672FAC41F06C141008445B1 /* NotificationCenterExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1672FAC31F06C141008445B1 /* NotificationCenterExtension.swift */; }; - 1673B8FF1F27E56100923BB8 /* TopCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1673B8FE1F27E56100923BB8 /* TopCollectionViewController.swift */; }; - 1673B9011F27E70200923BB8 /* JustForYouCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1673B9001F27E70200923BB8 /* JustForYouCollectionViewController.swift */; }; - 167AFAB21F043CF600A1332F /* PostDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167AFAB11F043CF600A1332F /* PostDetailTableViewController.swift */; }; - 167AFAB51F043ED500A1332F /* DetailTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167AFAB41F043ED500A1332F /* DetailTableViewCell.swift */; }; 167AFAB71F043F1100A1332F /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167AFAB61F043F1100A1332F /* HeaderView.swift */; }; - 167AFAB91F04624E00A1332F /* SingleLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167AFAB81F04624E00A1332F /* SingleLabelTableViewCell.swift */; }; 1686FC011F009EC00088A6C1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1686FC001F009EC00088A6C1 /* AppDelegate.swift */; }; 1686FC061F009EC00088A6C1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1686FC041F009EC00088A6C1 /* Main.storyboard */; }; 1686FC081F009EC00088A6C1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1686FC071F009EC00088A6C1 /* Assets.xcassets */; }; 1686FC0B1F009EC00088A6C1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1686FC091F009EC00088A6C1 /* LaunchScreen.storyboard */; }; 1686FC161F009EC00088A6C1 /* SEDaily_IOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1686FC151F009EC00088A6C1 /* SEDaily_IOSTests.swift */; }; 169806A61F8EF3970075D8AD /* L10nEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 169806A51F8EF08F0075D8AD /* L10nEnum.swift */; }; + 16AD3E0A1F9B138D0084C545 /* PodcastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16AD3E091F9B138D0084C545 /* PodcastViewModel.swift */; }; + 16AD3E0C1F9B13EE0084C545 /* FilterObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16AD3E0B1F9B13EE0084C545 /* FilterObject.swift */; }; + 16AD3E0E1F9B14130084C545 /* PodcastDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16AD3E0D1F9B14130084C545 /* PodcastDataSource.swift */; }; 16B147B11F16BF9C00433A42 /* AudioViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16B147B01F16BF9C00433A42 /* AudioViewManager.swift */; }; + 16CE698F1F98029E0057BAC3 /* PodcastDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16CE698E1F98029E0057BAC3 /* PodcastDetailViewController.swift */; }; + 16CE69911F9807CF0057BAC3 /* PodcastDescriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16CE69901F9807CF0057BAC3 /* PodcastDescriptionView.swift */; }; 16D67C4A1F33AC620065E838 /* AnswersTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D67C491F33AC620065E838 /* AnswersTracker.swift */; }; - 16D67C4D1F33AE370065E838 /* ObjectExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D67C4C1F33AE370065E838 /* ObjectExtensions.swift */; }; 16D766BA1F06B4850066C143 /* AudioView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D766B91F06B4850066C143 /* AudioView.swift */; }; - 1E44AF031F87B08D00221B22 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1E44AF051F87B08D00221B22 /* Localizable.strings */; }; + 16F3A1BA1F90918D00364709 /* PodcastRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16F3A1B91F90918D00364709 /* PodcastRepository.swift */; }; 16FA84031F8D323700A45D9B /* SkeletonCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16FA84021F8D323700A45D9B /* SkeletonCollectionView.swift */; }; + 1E44AF031F87B08D00221B22 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1E44AF051F87B08D00221B22 /* Localizable.strings */; }; 238E99F4E37C7365658113B8 /* Pods_SEDaily_IOSTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF0C9F051C6C2F8638D2ABF8 /* Pods_SEDaily_IOSTests.framework */; }; 24412438C753126FDDD37CBF /* Pods_SEDaily_IOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A3651E5B02B83E392546E167 /* Pods_SEDaily_IOS.framework */; }; /* End PBXBuildFile section */ @@ -63,10 +60,10 @@ 01BB1D6C1F29999E004A912E /* PodcastPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodcastPageViewController.swift; sourceTree = ""; }; 161F3DE51F61F73D00A8F825 /* SearchTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTableViewController.swift; sourceTree = ""; }; 161F3DE91F62703100A8F825 /* PodcastTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodcastTableViewCell.swift; sourceTree = ""; }; + 16307FC91F8FDD02001783CB /* Podcast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Podcast.swift; sourceTree = ""; }; + 16307FCB1F8FEE3A001783CB /* PodcastViewModelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PodcastViewModelController.swift; sourceTree = ""; }; 1649A7891F204EC6005C4A6E /* ContainerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerViewController.swift; sourceTree = ""; }; 164C71041F021AC8003803BC /* CustomTabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomTabViewController.swift; sourceTree = ""; }; - 164FE9D61F02CD99009419CA /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; - 164FE9D81F02D611009419CA /* CollectionReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionReusableView.swift; sourceTree = ""; }; 164FE9DB1F02DAB5009419CA /* PodcastCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodcastCollectionViewCell.swift; sourceTree = ""; }; 164FE9E01F02EE67009419CA /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; 164FE9E41F02EE83009419CA /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; @@ -75,17 +72,9 @@ 164FE9EA1F02F340009419CA /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; 164FE9EC1F02F7E2009419CA /* UIButtonExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIButtonExtension.swift; sourceTree = ""; }; 164FE9EE1F03065C009419CA /* NavigationControllerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationControllerExtension.swift; sourceTree = ""; }; - 164FE9F01F030A2E009419CA /* PodcastModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodcastModel.swift; sourceTree = ""; }; - 165A5B671F7C32FD00FDE80B /* TempObjectMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempObjectMapper.swift; sourceTree = ""; }; + 165484541F902D3F005AEA23 /* GeneralCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralCollectionViewController.swift; sourceTree = ""; }; 166036201F266FF300A22B7B /* Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; - 166812491F350737007F288B /* GeneralCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralCollectionViewController.swift; sourceTree = ""; }; - 1672FAC31F06C141008445B1 /* NotificationCenterExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationCenterExtension.swift; sourceTree = ""; }; - 1673B8FE1F27E56100923BB8 /* TopCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopCollectionViewController.swift; sourceTree = ""; }; - 1673B9001F27E70200923BB8 /* JustForYouCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JustForYouCollectionViewController.swift; sourceTree = ""; }; - 167AFAB11F043CF600A1332F /* PostDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostDetailTableViewController.swift; sourceTree = ""; }; - 167AFAB41F043ED500A1332F /* DetailTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailTableViewCell.swift; sourceTree = ""; }; 167AFAB61F043F1100A1332F /* HeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; - 167AFAB81F04624E00A1332F /* SingleLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleLabelTableViewCell.swift; sourceTree = ""; }; 1686FBFD1F009EC00088A6C1 /* SEDaily-IOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SEDaily-IOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 1686FC001F009EC00088A6C1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 1686FC051F009EC00088A6C1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -96,15 +85,20 @@ 1686FC151F009EC00088A6C1 /* SEDaily_IOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEDaily_IOSTests.swift; sourceTree = ""; }; 1686FC171F009EC00088A6C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 169806A51F8EF08F0075D8AD /* L10nEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = L10nEnum.swift; sourceTree = ""; }; + 16AD3E091F9B138D0084C545 /* PodcastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PodcastViewModel.swift; sourceTree = ""; }; + 16AD3E0B1F9B13EE0084C545 /* FilterObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterObject.swift; sourceTree = ""; }; + 16AD3E0D1F9B14130084C545 /* PodcastDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PodcastDataSource.swift; sourceTree = ""; }; 16B147B01F16BF9C00433A42 /* AudioViewManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioViewManager.swift; sourceTree = ""; }; + 16CE698E1F98029E0057BAC3 /* PodcastDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PodcastDetailViewController.swift; sourceTree = ""; }; + 16CE69901F9807CF0057BAC3 /* PodcastDescriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PodcastDescriptionView.swift; sourceTree = ""; }; 16D67C491F33AC620065E838 /* AnswersTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnswersTracker.swift; sourceTree = ""; }; - 16D67C4C1F33AE370065E838 /* ObjectExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectExtensions.swift; sourceTree = ""; }; 16D766B91F06B4850066C143 /* AudioView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioView.swift; sourceTree = ""; }; + 16F3A1B91F90918D00364709 /* PodcastRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PodcastRepository.swift; sourceTree = ""; }; + 16FA84021F8D323700A45D9B /* SkeletonCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonCollectionView.swift; sourceTree = ""; }; 1E44AEFF1F87ACF500221B22 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/LaunchScreen.strings; sourceTree = ""; }; 1E44AF001F87ACF500221B22 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Main.strings; sourceTree = ""; }; 1E44AF061F87B09100221B22 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; 1E44AF081F87B0A700221B22 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; - 16FA84021F8D323700A45D9B /* SkeletonCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonCollectionView.swift; sourceTree = ""; }; 675FBEB71FD81FBA8767227A /* Pods-SEDaily-IOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SEDaily-IOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-SEDaily-IOS/Pods-SEDaily-IOS.release.xcconfig"; sourceTree = ""; }; 995F64385352A79BA0DF7DEB /* Pods-SEDaily-IOSTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SEDaily-IOSTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SEDaily-IOSTests/Pods-SEDaily-IOSTests.debug.xcconfig"; sourceTree = ""; }; 9FDA28C254F62BB468401B75 /* Pods-SEDaily-IOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SEDaily-IOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SEDaily-IOS/Pods-SEDaily-IOS.debug.xcconfig"; sourceTree = ""; }; @@ -133,88 +127,51 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 1610E2C21F227BE0006C8E83 /* Managers */ = { - isa = PBXGroup; - children = ( - 16B147B01F16BF9C00433A42 /* AudioViewManager.swift */, - ); - name = Managers; - sourceTree = ""; - }; - 164FE9DA1F02DAA5009419CA /* CollectionViews */ = { - isa = PBXGroup; - children = ( - 1673B8FE1F27E56100923BB8 /* TopCollectionViewController.swift */, - 1673B9001F27E70200923BB8 /* JustForYouCollectionViewController.swift */, - 166812491F350737007F288B /* GeneralCollectionViewController.swift */, - ); - name = CollectionViews; - sourceTree = ""; - }; - 164FE9DD1F02DAB8009419CA /* CollectionViewCells */ = { + 16307FC81F8FDCE4001783CB /* PodcastModels */ = { isa = PBXGroup; children = ( - 164FE9DB1F02DAB5009419CA /* PodcastCollectionViewCell.swift */, - ); - name = CollectionViewCells; + 16AD3E0B1F9B13EE0084C545 /* FilterObject.swift */, + 16307FC91F8FDD02001783CB /* Podcast.swift */, + 16AD3E0D1F9B14130084C545 /* PodcastDataSource.swift */, + 16F3A1B91F90918D00364709 /* PodcastRepository.swift */, + 16AD3E091F9B138D0084C545 /* PodcastViewModel.swift */, + 16307FCB1F8FEE3A001783CB /* PodcastViewModelController.swift */, + ); + name = PodcastModels; sourceTree = ""; }; - 164FE9DE1F02DACA009419CA /* Views */ = { + 164FE9DF1F02EE4E009419CA /* Podcasts */ = { isa = PBXGroup; children = ( - 164FE9D81F02D611009419CA /* CollectionReusableView.swift */, - 167AFAB61F043F1100A1332F /* HeaderView.swift */, - 16D766B91F06B4850066C143 /* AudioView.swift */, + 165484541F902D3F005AEA23 /* GeneralCollectionViewController.swift */, 16FA84021F8D323700A45D9B /* SkeletonCollectionView.swift */, + 16307FC81F8FDCE4001783CB /* PodcastModels */, + 16AD3E001F9A975F0084C545 /* PodcastDetail */, ); - name = Views; + name = Podcasts; sourceTree = ""; }; - 164FE9DF1F02EE4E009419CA /* ViewControllers */ = { + 164FE9E21F02EE71009419CA /* CommonModels */ = { isa = PBXGroup; children = ( - 164FE9E01F02EE67009419CA /* LoginViewController.swift */, - 167AFAB11F043CF600A1332F /* PostDetailTableViewController.swift */, - 1649A7891F204EC6005C4A6E /* ContainerViewController.swift */, - 01BB1D6C1F29999E004A912E /* PodcastPageViewController.swift */, - ); - name = ViewControllers; - sourceTree = ""; - }; - 164FE9E21F02EE71009419CA /* Models */ = { - isa = PBXGroup; - children = ( - 164FE9F01F030A2E009419CA /* PodcastModel.swift */, 164FE9E61F02F02F009419CA /* UserModel.swift */, ); - name = Models; + name = CommonModels; sourceTree = ""; }; 164FE9E31F02EE77009419CA /* Helpers */ = { isa = PBXGroup; children = ( - 164FE9D61F02CD99009419CA /* Extensions.swift */, 164FE9E81F02F049009419CA /* Stylesheet.swift */, 164FE9EA1F02F340009419CA /* Helpers.swift */, 164FE9EC1F02F7E2009419CA /* UIButtonExtension.swift */, 164FE9EE1F03065C009419CA /* NavigationControllerExtension.swift */, - 1672FAC31F06C141008445B1 /* NotificationCenterExtension.swift */, 166036201F266FF300A22B7B /* Notifications.swift */, 16D67C491F33AC620065E838 /* AnswersTracker.swift */, ); name = Helpers; sourceTree = ""; }; - 167AFAB31F043E8F00A1332F /* TableViewCell */ = { - isa = PBXGroup; - children = ( - 167AFAB41F043ED500A1332F /* DetailTableViewCell.swift */, - 167AFAB81F04624E00A1332F /* SingleLabelTableViewCell.swift */, - 161F3DE91F62703100A8F825 /* PodcastTableViewCell.swift */, - ); - name = TableViewCell; - sourceTree = ""; - }; 1686FBF41F009EC00088A6C1 = { isa = PBXGroup; children = ( @@ -239,25 +196,20 @@ isa = PBXGroup; children = ( 169806A41F8EF08F0075D8AD /* Constants */, - 1610E2C21F227BE0006C8E83 /* Managers */, 1686FC0C1F009EC00088A6C1 /* Info.plist */, 164FE9E41F02EE83009419CA /* API.swift */, 1686FC001F009EC00088A6C1 /* AppDelegate.swift */, - 164C71041F021AC8003803BC /* CustomTabViewController.swift */, 1E44AF051F87B08D00221B22 /* Localizable.strings */, 1686FC071F009EC00088A6C1 /* Assets.xcassets */, - 164FE9DD1F02DAB8009419CA /* CollectionViewCells */, - 164FE9DA1F02DAA5009419CA /* CollectionViews */, + 16AD3E011F9B06480084C545 /* Audio */, + 16AD3E071F9B07520084C545 /* Auth */, + 16AD3E061F9B07320084C545 /* CommonVCs */, + 16AD3E051F9B07160084C545 /* CommonCells */, + 164FE9E21F02EE71009419CA /* CommonModels */, + 16AD3E081F9B07A70084C545 /* Core */, 164FE9E31F02EE77009419CA /* Helpers */, - 1686FC091F009EC00088A6C1 /* LaunchScreen.storyboard */, - 1686FC041F009EC00088A6C1 /* Main.storyboard */, - 164FE9E21F02EE71009419CA /* Models */, - 167AFAB31F043E8F00A1332F /* TableViewCell */, - 164FE9DF1F02EE4E009419CA /* ViewControllers */, - 164FE9DE1F02DACA009419CA /* Views */, - 16D67C4C1F33AE370065E838 /* ObjectExtensions.swift */, - 161F3DE51F61F73D00A8F825 /* SearchTableViewController.swift */, - 165A5B671F7C32FD00FDE80B /* TempObjectMapper.swift */, + 164FE9DF1F02EE4E009419CA /* Podcasts */, + 16AD3E031F9B06950084C545 /* Search */, ); path = "SEDaily-IOS"; sourceTree = ""; @@ -279,6 +231,69 @@ path = Constants; sourceTree = SOURCE_ROOT; }; + 16AD3E001F9A975F0084C545 /* PodcastDetail */ = { + isa = PBXGroup; + children = ( + 167AFAB61F043F1100A1332F /* HeaderView.swift */, + 16CE69901F9807CF0057BAC3 /* PodcastDescriptionView.swift */, + 16CE698E1F98029E0057BAC3 /* PodcastDetailViewController.swift */, + ); + name = PodcastDetail; + sourceTree = ""; + }; + 16AD3E011F9B06480084C545 /* Audio */ = { + isa = PBXGroup; + children = ( + 16B147B01F16BF9C00433A42 /* AudioViewManager.swift */, + 16D766B91F06B4850066C143 /* AudioView.swift */, + ); + name = Audio; + sourceTree = ""; + }; + 16AD3E031F9B06950084C545 /* Search */ = { + isa = PBXGroup; + children = ( + 161F3DE51F61F73D00A8F825 /* SearchTableViewController.swift */, + ); + name = Search; + sourceTree = ""; + }; + 16AD3E051F9B07160084C545 /* CommonCells */ = { + isa = PBXGroup; + children = ( + 161F3DE91F62703100A8F825 /* PodcastTableViewCell.swift */, + 164FE9DB1F02DAB5009419CA /* PodcastCollectionViewCell.swift */, + ); + name = CommonCells; + sourceTree = ""; + }; + 16AD3E061F9B07320084C545 /* CommonVCs */ = { + isa = PBXGroup; + children = ( + 164C71041F021AC8003803BC /* CustomTabViewController.swift */, + 01BB1D6C1F29999E004A912E /* PodcastPageViewController.swift */, + 1649A7891F204EC6005C4A6E /* ContainerViewController.swift */, + ); + name = CommonVCs; + sourceTree = ""; + }; + 16AD3E071F9B07520084C545 /* Auth */ = { + isa = PBXGroup; + children = ( + 164FE9E01F02EE67009419CA /* LoginViewController.swift */, + ); + name = Auth; + sourceTree = ""; + }; + 16AD3E081F9B07A70084C545 /* Core */ = { + isa = PBXGroup; + children = ( + 1686FC091F009EC00088A6C1 /* LaunchScreen.storyboard */, + 1686FC041F009EC00088A6C1 /* Main.storyboard */, + ); + name = Core; + sourceTree = ""; + }; 4E74333675FE5899EC260076 /* Pods */ = { isa = PBXGroup; children = ( @@ -534,22 +549,21 @@ "${SRCROOT}/Pods/Target Support Files/Pods-SEDaily-IOS/Pods-SEDaily-IOS-frameworks.sh", "${BUILT_PRODUCTS_DIR}/ActiveLabel/ActiveLabel.framework", "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", + "${BUILT_PRODUCTS_DIR}/Disk/Disk.framework", "${BUILT_PRODUCTS_DIR}/Eureka/Eureka.framework", "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework", "${BUILT_PRODUCTS_DIR}/KTResponsiveUI/KTResponsiveUI.framework", - "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework", "${BUILT_PRODUCTS_DIR}/KoalaTeaFlowLayout/KoalaTeaFlowLayout.framework", "${BUILT_PRODUCTS_DIR}/KoalaTeaPlayer/KoalaTeaPlayer.framework", - "${BUILT_PRODUCTS_DIR}/ObjectMapper/ObjectMapper.framework", "${BUILT_PRODUCTS_DIR}/Pageboy/Pageboy.framework", "${BUILT_PRODUCTS_DIR}/PureLayout/PureLayout.framework", - "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", - "${BUILT_PRODUCTS_DIR}/RealmSwift/RealmSwift.framework", "${BUILT_PRODUCTS_DIR}/Reusable/Reusable.framework", + "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/SideMenu/SideMenu.framework", "${BUILT_PRODUCTS_DIR}/Skeleton/Skeleton.framework", "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", "${BUILT_PRODUCTS_DIR}/SwiftIcons/SwiftIcons.framework", + "${BUILT_PRODUCTS_DIR}/SwiftSoup/SwiftSoup.framework", "${BUILT_PRODUCTS_DIR}/SwifterSwift/SwifterSwift.framework", "${BUILT_PRODUCTS_DIR}/SwiftyBeaver/SwiftyBeaver.framework", "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", @@ -560,22 +574,21 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ActiveLabel.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Disk.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Eureka.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManagerSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KTResponsiveUI.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KoalaTeaFlowLayout.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KoalaTeaPlayer.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ObjectMapper.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Pageboy.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PureLayout.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RealmSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reusable.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SideMenu.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Skeleton.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftIcons.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftSoup.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwifterSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyBeaver.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", @@ -594,36 +607,33 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 167AFAB51F043ED500A1332F /* DetailTableViewCell.swift in Sources */, 164FE9E11F02EE67009419CA /* LoginViewController.swift in Sources */, 1649A78A1F204EC6005C4A6E /* ContainerViewController.swift in Sources */, 164FE9EF1F03065C009419CA /* NavigationControllerExtension.swift in Sources */, - 167AFAB21F043CF600A1332F /* PostDetailTableViewController.swift in Sources */, + 165484551F902D3F005AEA23 /* GeneralCollectionViewController.swift in Sources */, 161F3DE61F61F73D00A8F825 /* SearchTableViewController.swift in Sources */, - 16D67C4D1F33AE370065E838 /* ObjectExtensions.swift in Sources */, - 164FE9D91F02D611009419CA /* CollectionReusableView.swift in Sources */, - 167AFAB91F04624E00A1332F /* SingleLabelTableViewCell.swift in Sources */, 167AFAB71F043F1100A1332F /* HeaderView.swift in Sources */, 16D67C4A1F33AC620065E838 /* AnswersTracker.swift in Sources */, - 164FE9D71F02CD99009419CA /* Extensions.swift in Sources */, 164FE9E51F02EE83009419CA /* API.swift in Sources */, 164FE9E91F02F049009419CA /* Stylesheet.swift in Sources */, - 1668124A1F350737007F288B /* GeneralCollectionViewController.swift in Sources */, + 16CE698F1F98029E0057BAC3 /* PodcastDetailViewController.swift in Sources */, + 16AD3E0C1F9B13EE0084C545 /* FilterObject.swift in Sources */, + 16AD3E0A1F9B138D0084C545 /* PodcastViewModel.swift in Sources */, 16B147B11F16BF9C00433A42 /* AudioViewManager.swift in Sources */, - 1673B9011F27E70200923BB8 /* JustForYouCollectionViewController.swift in Sources */, - 1673B8FF1F27E56100923BB8 /* TopCollectionViewController.swift in Sources */, 164C71051F021AC8003803BC /* CustomTabViewController.swift in Sources */, 164FE9DC1F02DAB5009419CA /* PodcastCollectionViewCell.swift in Sources */, - 165A5B681F7C32FD00FDE80B /* TempObjectMapper.swift in Sources */, 16D766BA1F06B4850066C143 /* AudioView.swift in Sources */, 164FE9EB1F02F340009419CA /* Helpers.swift in Sources */, 16FA84031F8D323700A45D9B /* SkeletonCollectionView.swift in Sources */, 166036211F266FF300A22B7B /* Notifications.swift in Sources */, - 1672FAC41F06C141008445B1 /* NotificationCenterExtension.swift in Sources */, 169806A61F8EF3970075D8AD /* L10nEnum.swift in Sources */, 01BB1D6D1F29999E004A912E /* PodcastPageViewController.swift in Sources */, - 164FE9F11F030A2E009419CA /* PodcastModel.swift in Sources */, + 16CE69911F9807CF0057BAC3 /* PodcastDescriptionView.swift in Sources */, + 16307FCC1F8FEE3A001783CB /* PodcastViewModelController.swift in Sources */, + 16AD3E0E1F9B14130084C545 /* PodcastDataSource.swift in Sources */, 164FE9ED1F02F7E2009419CA /* UIButtonExtension.swift in Sources */, + 16307FCA1F8FDD02001783CB /* Podcast.swift in Sources */, + 16F3A1BA1F90918D00364709 /* PodcastRepository.swift in Sources */, 1686FC011F009EC00088A6C1 /* AppDelegate.swift in Sources */, 161F3DEA1F62703100A8F825 /* PodcastTableViewCell.swift in Sources */, 164FE9E71F02F02F009419CA /* UserModel.swift in Sources */, @@ -796,12 +806,15 @@ baseConfigurationReference = 9FDA28C254F62BB468401B75 /* Pods-SEDaily-IOS.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 6; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = 6TY8WC8WPP; INFOPLIST_FILE = "SEDaily-IOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "koala-tea.SEDaily"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 4.0; }; @@ -812,12 +825,15 @@ baseConfigurationReference = 675FBEB71FD81FBA8767227A /* Pods-SEDaily-IOS.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 6; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = 6TY8WC8WPP; INFOPLIST_FILE = "SEDaily-IOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "koala-tea.SEDaily"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 4.0; }; diff --git a/SEDaily-IOS/API.swift b/SEDaily-IOS/API.swift index 13c634b..fab7544 100644 --- a/SEDaily-IOS/API.swift +++ b/SEDaily-IOS/API.swift @@ -8,8 +8,6 @@ import UIKit import Alamofire -//import AlamofireObjectMapper -import RealmSwift import SwiftyJSON import Fabric import Crashlytics @@ -68,16 +66,14 @@ class API { extension API { // MARK: Auth - func login(username: String, password: String, completion: @escaping (_ success: Bool?) -> Void) { + func login(firstName: String, lastName: String, email: String, password: String, completion: @escaping (_ success: Bool?) -> Void) { let urlString = rootURL + Endpoints.login let _headers : HTTPHeaders = [Headers.contentType:Headers.x_www_form_urlencoded] var params = [String: String]() - params[Params.username] = username + params[Params.username] = email params[Params.password] = password - typealias model = PodcastModel - Alamofire.request(urlString, method: .post, parameters: params, encoding: URLEncoding.httpBody , headers: _headers).responseJSON { response in switch response.result { case .success: @@ -90,11 +86,9 @@ extension API { return } - if let token = jsonResponse["token"] { - let user = User() - user.email = username - user.token = token as? String - user.save() + if let token = jsonResponse["token"] as? String { + let user = User(firstName: firstName, lastName: lastName, email: email, token: token) + UserManager.sharedInstance.setCurrentUser(to: user) NotificationCenter.default.post(name: .loginChanged, object: nil) completion(true) @@ -109,16 +103,14 @@ extension API { } } - func register(username: String, password: String, completion: @escaping (_ success: Bool?) -> Void) { + func register(firstName: String, lastName: String, email: String, password: String, completion: @escaping (_ success: Bool?) -> Void) { let urlString = rootURL + Endpoints.register let _headers : HTTPHeaders = [Headers.contentType:Headers.x_www_form_urlencoded] var params = [String: String]() - params[Params.username] = username + params[Params.username] = email params[Params.password] = password - typealias model = PodcastModel - Alamofire.request(urlString, method: .post, parameters: params, encoding: URLEncoding.httpBody , headers: _headers).responseJSON { response in switch response.result { case .success: @@ -133,11 +125,9 @@ extension API { return } - if let token = jsonResponse["token"] { - let user = User() - user.email = username - user.token = token as? String - user.save() + if let token = jsonResponse["token"] as? String { + let user = User(firstName: firstName, lastName: lastName, email: email, token: token) + UserManager.sharedInstance.setCurrentUser(to: user) NotificationCenter.default.post(name: .loginChanged, object: nil) completion(true) @@ -154,186 +144,136 @@ extension API { } +typealias podcastModel = Podcast +// MARK: Search extension API { - //MARK: Getters - func getPosts(type: String, createdAtBefore beforeDate: String = "", tags: String = "-1", categoires: String = "", completion: @escaping (_ hasChanges: Bool) -> Void) { + func getPostsWith(searchTerm: String, + createdAtBefore beforeDate: String = "", + onSucces: @escaping ([Podcast]) -> Void, + onFailure: @escaping (APIError?) -> Void) { let urlString = rootURL + Endpoints.posts var params = [String: String]() - params[Params.type] = type + params[Params.search] = searchTerm params[Params.createdAtBefore] = beforeDate - // @TODO: Allow for an array and join the array - if (tags != "-1") { - params[Params.tags] = tags - } - if (categoires != "-1") { - params[Params.categories] = categoires - } - - let user = User.getActiveUser() - guard let userToken = user.token else { return } + let user = UserManager.sharedInstance.getActiveUser() + let userToken = user.token let _headers : HTTPHeaders = [ Headers.authorization:Headers.bearer + userToken, - ] + ] - typealias model = PodcastModel - - Alamofire.request(urlString, method: .get, parameters: params, headers: _headers).responseArray { (response: DataResponse<[model]>) in - - // Variable to check if this function returns changes - var hasChanges = false - + Alamofire.request(urlString, method: .get, parameters: params, headers: _headers).responseJSON { response in switch response.result { case .success: - let modelsArray = response.result.value - guard let array = modelsArray else { return } - - if array.isEmpty { - completion(hasChanges) + guard let responseData = response.data else { + // Handle error here + log.error("response has no data") + onFailure(.NoResponseDataError) + return } - for item in array { - hasChanges = true - - let realm = try! Realm() - let existingItem = realm.object(ofType: model.self, forPrimaryKey: item.key) - - if item.key != existingItem?.key { - switch type { - case API.Types.top: - item.isTop = true - case API.Types.new: - item.isNew = true - default: - break - } - item.save() - } - else { - // Just update the existing item - let recommended = existingItem!.isRecommended - - existingItem?.updateFrom(item: item) - - existingItem?.update(isRecommended: recommended) - - switch type { - case API.Types.top: - existingItem?.update(isTop: true) - case API.Types.new: - existingItem?.update(isNew: true) - default: - break - } + + var data: [podcastModel] = [] + let this = JSON(responseData) + for (_, subJson):(String, JSON) in this { + guard let jsonData = try? subJson.rawData() else { continue } + let newObject = try? JSONDecoder().decode(podcastModel.self, from: jsonData) + if let newObject = newObject { + data.append(newObject) } } - completion(hasChanges) + onSucces(data) case .failure(let error): - log.error(error) + log.error(error.localizedDescription) Tracker.logGeneralError(error: error) - completion(false) + onFailure(.GeneralFailure) } } } - - func getRecommendedPosts(createdAtBefore beforeDate: String = "", completion: @escaping (_ hasChanges: Bool) -> Void) { - let urlString = rootURL + Endpoints.recommendations +} + +// MARK: - MVVM Getters +extension API { + func getPosts(type: String = "", + createdAtBefore beforeDate: String = "", + tags: String = "-1", + categories: String = "", + onSucces: @escaping ([Podcast]) -> Void, + onFailure: @escaping (APIError?) -> Void) { + var type = type - let user = User.getActiveUser() - guard let userToken = user.token else { return } + let user = UserManager.sharedInstance.getActiveUser() + let userToken = user.token let _headers : HTTPHeaders = [ Headers.authorization:Headers.bearer + userToken, - ] + ] - typealias model = PodcastModel - - // Variable to check if this function returns changes - var hasChanges = false + if userToken.isEmpty && type == PodcastTypes.recommended.rawValue { + type = PodcastTypes.top.rawValue + } - Alamofire.request(urlString, method: .get, parameters: nil, headers: _headers).responseArray { (response: DataResponse<[model]>) in - - switch response.result { - - case .success: - let modelsArray = response.result.value - guard let array = modelsArray else { return } - - if array.isEmpty { - completion(hasChanges) - } - - for item in array { - hasChanges = true - // Check if Achievement Model already exists - let realm = try! Realm() - let existingItem = realm.object(ofType: model.self, forPrimaryKey: item.key) - - if item.key != existingItem?.key { - item.isRecommended = true - item.save() - } - else { - // Just update the existing item - existingItem?.updateFrom(item: item) - existingItem?.update(isRecommended: true) - } - } - completion(hasChanges) - break - case .failure(let error): - log.error(error) - Tracker.logGeneralError(error: error) - completion(false) - break - } + var urlString = self.rootURL + API.Endpoints.posts + if type == PodcastTypes.recommended.rawValue { + urlString = self.rootURL + Endpoints.recommendations } - } - - func getPostsWith(searchTerm: String, createdAtBefore beforeDate: String = "", completion: @escaping (_ posts: [PodcastModel]?) -> Void) { - let urlString = rootURL + Endpoints.posts + // Params var params = [String: String]() - params[Params.search] = searchTerm - params[Params.createdAtBefore] = beforeDate - - let user = User.getActiveUser() - guard let userToken = user.token else { return } - let _headers : HTTPHeaders = [ - Headers.authorization:Headers.bearer + userToken, - ] + params[Params.type] = type + if beforeDate != "" && type != PodcastTypes.recommended.rawValue { + params[Params.createdAtBefore] = beforeDate + } - typealias model = PodcastModel + // @TODO: Allow for an array and join the array + if (tags != "") { + params[Params.tags] = tags + } - Alamofire.request(urlString, method: .get, parameters: params, headers: _headers).responseArray { (response: DataResponse<[model]>) in + if (categories != "") { + params[Params.categories] = categories + } + + Alamofire.request(urlString, method: .get, parameters: params, headers: _headers).responseJSON { response in switch response.result { case .success: - let modelsArray = response.result.value - guard let array = modelsArray else { - completion(nil) + guard let responseData = response.data else { + // Handle error here + log.error("response has no data") + onFailure(.NoResponseDataError) return } - completion(array) + + var data: [podcastModel] = [] + let this = JSON(responseData) + for (_, subJson):(String, JSON) in this { + guard let jsonData = try? subJson.rawData() else { continue } + let newObject = try? JSONDecoder().decode(podcastModel.self, from: jsonData) + if var newObject = newObject { + newObject.type = type + data.append(newObject) + } + } + onSucces(data) case .failure(let error): - log.error(error) + log.error(error.localizedDescription) Tracker.logGeneralError(error: error) - completion(nil) + onFailure(.GeneralFailure) } } } } +// MARK: Voting extension API { func upvotePodcast(podcastId: String, completion: @escaping (_ success: Bool?, _ active: Bool?) -> Void) { let urlString = rootURL + Endpoints.posts + "/" + podcastId + Endpoints.upvote - let user = User.getActiveUser() - guard let userToken = user.token else { return } + let user = UserManager.sharedInstance.getActiveUser() + let userToken = user.token let _headers : HTTPHeaders = [ Headers.authorization:Headers.bearer + userToken, Headers.contentType:Headers.x_www_form_urlencoded ] - - typealias model = PodcastModel Alamofire.request(urlString, method: .post, parameters: nil, encoding: URLEncoding.httpBody , headers: _headers).responseJSON { response in switch response.result { @@ -361,15 +301,13 @@ extension API { func downvotePodcast(podcastId: String, completion: @escaping (_ success: Bool?, _ active: Bool?) -> Void) { let urlString = rootURL + Endpoints.posts + "/" + podcastId + Endpoints.downvote - let user = User.getActiveUser() - guard let userToken = user.token else { return } + let user = UserManager.sharedInstance.getActiveUser() + let userToken = user.token let _headers : HTTPHeaders = [ Headers.authorization:Headers.bearer + userToken, Headers.contentType:Headers.x_www_form_urlencoded ] - typealias model = PodcastModel - Alamofire.request(urlString, method: .post, parameters: nil, encoding: URLEncoding.httpBody , headers: _headers).responseJSON { response in switch response.result { case .success: @@ -392,33 +330,3 @@ extension API { } } } - -extension API { - func createDefaultData() { -// User.createDefault() - } - -// func loadAllObjects() { -// self.getEvents() -// self.getPets() -// self.getShelters() -// } -// -// func loadLoggedInData() { -// self.getFavorites() -// self.getFollowingShelters() -// } -// -// func reloadAllObjects() { -// let realm = try! Realm() -// try! realm.write { -// realm.delete(EventModel.all()) -// realm.delete(PetModel.all()) -// realm.delete(ShelterModel.all()) -// realm.delete(UpdatesModel.all()) -// } -// self.getEvents() -// self.getPets() -// self.getShelters() -// } -} diff --git a/SEDaily-IOS/AnswersTracker.swift b/SEDaily-IOS/AnswersTracker.swift index dc361bb..bf0fdf5 100644 --- a/SEDaily-IOS/AnswersTracker.swift +++ b/SEDaily-IOS/AnswersTracker.swift @@ -12,7 +12,7 @@ //Uninstalls //Returning Users //Follows -//FAvorites +//Favorites import Foundation import Crashlytics @@ -27,9 +27,15 @@ class Tracker { ) } - class func logPlayPodcast(podcast: PodcastModel) { - let dictionary = podcast.podcastToDictionary() as? [String : Any] - Answers.logCustomEvent(withName: "Podcast_Play", customAttributes: dictionary) + class func logPlayPodcast(podcast: PodcastViewModel) { + Answers.logCustomEvent(withName: "Podcast_Play", customAttributes: + [ + "podcastId": podcast._id, + "podcastTitle": podcast.podcastTitle, + "tags": podcast.tagsAsString, + "categories": podcast.categoriesAsString + ] + ) } class func logLogin(user: User) { diff --git a/SEDaily-IOS/AppDelegate.swift b/SEDaily-IOS/AppDelegate.swift index d993d09..3af6028 100644 --- a/SEDaily-IOS/AppDelegate.swift +++ b/SEDaily-IOS/AppDelegate.swift @@ -7,7 +7,6 @@ // import UIKit -import RealmSwift import SwiftyBeaver let log = SwiftyBeaver.self import IQKeyboardManagerSwift @@ -24,11 +23,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Override point for customization after application launch. Fabric.with([Crashlytics.self]) - migrateRealmDatabaseIfNeeded() setupSwiftyBeaver() setupIQKeyboard() setupFirstScreen() - API.sharedInstance.createDefaultData() return true } @@ -74,23 +71,8 @@ extension AppDelegate { window = UIWindow(frame: UIScreen.main.bounds) let rootVC = ContainerViewController() rootVC.view.backgroundColor = .white -// let navVC = UINavigationController(rootViewController: rootVC) -// navVC.view.backgroundColor = .white -// navVC.navigationBar.isTranslucent = false -// window!.rootViewController = navVC window!.rootViewController = rootVC window!.makeKeyAndVisible() - - let realm = try! Realm() - log.info(realm.configuration.fileURL ?? "") - } - - func migrateRealmDatabaseIfNeeded() { - var config = Realm.Configuration() - config.deleteRealmIfMigrationNeeded = true - - Realm.Configuration.defaultConfiguration = config - _ = try! Realm() } } diff --git a/SEDaily-IOS/AudioView.swift b/SEDaily-IOS/AudioView.swift index 660991f..73e380a 100644 --- a/SEDaily-IOS/AudioView.swift +++ b/SEDaily-IOS/AudioView.swift @@ -9,6 +9,7 @@ import UIKit import AVFoundation import SwifterSwift +import KTResponsiveUI enum PlaybackSpeed: Float { case _1x = 1.0 @@ -99,12 +100,12 @@ class AudioView: UIView { var previousSliderValue: Float = 0.0 var isFirstLoad = true - var settingsButton = UIButton() + var playbackSpeedButton = UIButton() var currentSpeed: PlaybackSpeed = ._1x { willSet { guard currentSpeed != newValue else { return } - self.settingsButton.setTitle(newValue.shortTitle, for: .normal) + self.playbackSpeedButton.setTitle(newValue.shortTitle, for: .normal) self.delegate?.audioRateChanged(newRate: newValue.rawValue) } } @@ -147,13 +148,12 @@ class AudioView: UIView { containerView.addSubview(podcastLabel) podcastLabel.snp.makeConstraints { (make) -> Void in - make.left.equalToSuperview().inset(60.calculateWidth()) - make.right.equalToSuperview().inset(60.calculateWidth()) + make.left.right.equalToSuperview().inset(UIView.getValueScaledByScreenWidthFor(baseValue: 60)) make.centerX.equalToSuperview() - make.centerY.equalToSuperview().inset(-30.calculateHeight()) + make.centerY.equalToSuperview().inset(UIView.getValueScaledByScreenHeightFor(baseValue: -30)) } - podcastLabel.font = UIFont.systemFont(ofSize: 16.calculateWidth()) + podcastLabel.font = UIFont.systemFont(ofSize: UIView.getValueScaledByScreenWidthFor(baseValue: 16)) podcastLabel.numberOfLines = 0 podcastLabel.textAlignment = .center @@ -164,8 +164,8 @@ class AudioView: UIView { stackView.distribution = .fillEqually stackView.snp.makeConstraints { (make) -> Void in - make.height.equalTo(70.calculateHeight()) - make.width.equalTo((50 * 5).calculateHeight()) + make.height.equalTo(UIView.getValueScaledByScreenHeightFor(baseValue: 70)) + make.width.equalTo(UIView.getValueScaledByScreenWidthFor(baseValue: (50 * 5))) make.top.equalTo(podcastLabel.snp.bottom) make.centerX.equalToSuperview() } @@ -176,7 +176,7 @@ class AudioView: UIView { stackView.addArrangedSubview(pauseButton) stackView.addArrangedSubview(skipForwardButton) - let iconHeight = (70 / 2).calculateHeight() + let iconHeight = UIView.getValueScaledByScreenHeightFor(baseValue: (70 / 2)) skipBackwardbutton.setImage(#imageLiteral(resourceName: "Backward"), for: .normal) skipBackwardbutton.height = iconHeight @@ -198,18 +198,19 @@ class AudioView: UIView { playButton.isHidden = true - settingsButton.setTitle(PlaybackSpeed._1x.shortTitle, for: .normal) - settingsButton.setTitleColor(Stylesheet.Colors.secondaryColor, for: .normal) - settingsButton.addTarget(self, action: #selector(self.settingsButtonPressed), for: .touchUpInside) - self.addSubview(settingsButton) + playbackSpeedButton.titleLabel?.font = UIFont.systemFont(ofSize: UIView.getValueScaledByScreenWidthFor(baseValue: 20)) + playbackSpeedButton.setTitle(PlaybackSpeed._1x.shortTitle, for: .normal) + playbackSpeedButton.setTitleColor(Stylesheet.Colors.secondaryColor, for: .normal) + playbackSpeedButton.addTarget(self, action: #selector(self.settingsButtonPressed), for: .touchUpInside) + self.addSubview(playbackSpeedButton) let width = UIView.getValueScaledByScreenWidthFor(baseValue: 40) let height = UIView.getValueScaledByScreenHeightFor(baseValue: 40) - settingsButton.snp.makeConstraints { (make) -> Void in + playbackSpeedButton.snp.makeConstraints { (make) -> Void in make.width.equalTo(width) make.height.equalTo(height) make.bottom.equalToSuperview() - make.right.equalToSuperview() + make.right.equalToSuperview().inset(UIView.getValueScaledByScreenWidthFor(baseValue: 2)) } setupActivityIndicator() @@ -233,7 +234,7 @@ class AudioView: UIView { playbackSlider.snp.makeConstraints { (make) -> Void in make.top.equalToSuperview().inset(-10) - make.height.equalTo(20.calculateHeight()) + make.height.equalTo(UIView.getValueScaledByScreenHeightFor(baseValue: 20)) make.left.right.equalToSuperview() } @@ -258,7 +259,7 @@ class AudioView: UIView { bufferBackgroundSlider.snp.makeConstraints { (make) -> Void in make.top.equalToSuperview().inset(-10) - make.height.equalTo(20.calculateHeight()) + make.height.equalTo(UIView.getValueScaledByScreenHeightFor(baseValue: 20)) make.left.right.equalToSuperview() } @@ -276,7 +277,7 @@ class AudioView: UIView { bufferSlider.snp.makeConstraints { (make) -> Void in make.top.equalToSuperview().inset(-10) - make.height.equalTo(20.calculateHeight()) + make.height.equalTo(UIView.getValueScaledByScreenHeightFor(baseValue: 20)) make.left.right.equalToSuperview() } @@ -284,30 +285,31 @@ class AudioView: UIView { } func addLabels() { - currentTimeLabel.text = "00.00.00" + let labelFontSize = UIView.getValueScaledByScreenWidthFor(baseValue: 12) + currentTimeLabel.text = "00:00" currentTimeLabel.textAlignment = .left - currentTimeLabel.font = UIFont.systemFont(ofSize: 12) + currentTimeLabel.font = UIFont.systemFont(ofSize: labelFontSize) - timeLeftLabel.text = "00.00.00" + timeLeftLabel.text = "00:00" timeLeftLabel.textAlignment = .right timeLeftLabel.adjustsFontSizeToFitWidth = true - timeLeftLabel.font = UIFont.systemFont(ofSize: 12) + timeLeftLabel.font = UIFont.systemFont(ofSize: labelFontSize) self.containerView.addSubview(currentTimeLabel) self.containerView.addSubview(timeLeftLabel) currentTimeLabel.snp.makeConstraints { (make) -> Void in - make.left.equalTo(playbackSlider).inset(5.calculateWidth()) - make.top.equalTo(playbackSlider.snp.bottom).inset(5.calculateHeight()) - make.height.equalTo(20.calculateHeight()) - make.width.equalTo(55.calculateWidth()) + make.left.equalTo(playbackSlider).inset(UIView.getValueScaledByScreenWidthFor(baseValue: 5)) + make.top.equalTo(playbackSlider.snp.bottom).inset(UIView.getValueScaledByScreenHeightFor(baseValue: 5)) + make.height.equalTo(UIView.getValueScaledByScreenHeightFor(baseValue: 20)) + make.width.equalTo(UIView.getValueScaledByScreenWidthFor(baseValue: 55)) } timeLeftLabel.snp.makeConstraints { (make) -> Void in - make.right.equalTo(playbackSlider).inset(5.calculateWidth()) - make.top.equalTo(playbackSlider.snp.bottom).inset(5.calculateHeight()) - make.height.equalTo(20.calculateHeight()) - make.width.equalTo(55.calculateWidth()) + make.right.equalTo(playbackSlider).inset(UIView.getValueScaledByScreenWidthFor(baseValue: 5)) + make.top.equalTo(playbackSlider.snp.bottom).inset(UIView.getValueScaledByScreenHeightFor(baseValue: 5)) + make.height.equalTo(UIView.getValueScaledByScreenHeightFor(baseValue: 20)) + make.width.equalTo(UIView.getValueScaledByScreenWidthFor(baseValue: 55)) } } @@ -408,7 +410,7 @@ class AudioView: UIView { activityView.snp.makeConstraints { (make) -> Void in make.centerY.equalToSuperview() - make.right.equalToSuperview().inset(10.calculateWidth()) + make.right.equalToSuperview().inset(UIView.getValueScaledByScreenWidthFor(baseValue: 10)) } } diff --git a/SEDaily-IOS/AudioViewManager.swift b/SEDaily-IOS/AudioViewManager.swift index b061159..f6e053e 100644 --- a/SEDaily-IOS/AudioViewManager.swift +++ b/SEDaily-IOS/AudioViewManager.swift @@ -26,9 +26,9 @@ class AudioViewManager: NSObject { var remoteCommandManager: RemoteCommandManager! = nil var audioView: AudioView? - var podcastModel: PodcastModel? + var podcastModel: PodcastViewModel? - func setupManager(podcastModel: PodcastModel) { + func setupManager(podcastModel: PodcastViewModel) { self.podcastModel = podcastModel Tracker.logPlayPodcast(podcast: podcastModel) self.presentAudioView() @@ -36,11 +36,12 @@ class AudioViewManager: NSObject { fileprivate func setupAudioManager(url: URL, name: String) { var savedTime: Float = 0 - if let time = podcastModel?.currentTime { - if let float = Float(time) { - savedTime = float - } - } + //@TODO: Add tracking for current time again +// if let time = podcastModel?.currentTime { +// if let float = Float(time) { +// savedTime = float +// } +// } log.info(savedTime, "savedtime") let asset = Asset(assetName: name, url: url, savedTime: savedTime) @@ -56,6 +57,7 @@ class AudioViewManager: NSObject { remoteCommandManager.toggleChangePlaybackPositionCommand(true) remoteCommandManager.toggleSkipBackwardCommand(true, interval: 30) remoteCommandManager.toggleSkipForwardCommand(true, interval: 30) + remoteCommandManager.toggleChangePlaybackPositionCommand(true) } fileprivate func presentAudioView() { @@ -76,8 +78,8 @@ class AudioViewManager: NSObject { controller.setContainerViewInset() } - guard let url = self.podcastModel?.getMP3asURL() else { return } - guard let name = self.podcastModel?.podcastName else { return } + guard let url = self.podcastModel?.mp3URL else { return } + guard let name = self.podcastModel?.podcastTitle else { return } self.setupAudioManager(url: url, name: name) } @@ -104,7 +106,7 @@ class AudioViewManager: NSObject { fileprivate func setupView(over vc: UIViewController) { if audioView != nil { // Setup progress, text, other stuff - setText(text: podcastModel?.podcastName) + setText(text: podcastModel?.podcastTitle) return } @@ -115,11 +117,11 @@ class AudioViewManager: NSObject { vc.view.addSubview(audioView!) audioView?.width = UIScreen.main.bounds.width - audioView?.height = 110.calculateHeight() + audioView?.height = UIView.getValueScaledByScreenHeightFor(baseValue: 110) audioView?.center.x = vc.view.center.x audioView?.frame.origin.y = UIScreen.main.bounds.height - setText(text: podcastModel?.podcastName) + setText(text: podcastModel?.podcastTitle) audioView?.animateIn() } @@ -133,7 +135,7 @@ class AudioViewManager: NSObject { //@TODO: Add manager param and update everything here (maybe) fileprivate func handleStateChange(for state: AssetPlayerPlaybackState) { if let model = podcastModel { - self.setText(text: model.podcastName) + self.setText(text: model.podcastTitle) } switch state { @@ -197,7 +199,8 @@ extension AudioViewManager: AssetPlayerDelegate { } func playerCurrentTimeDidChange(_ player: AssetPlayer) { - podcastModel?.update(currentTime: Float(player.currentTime)) + //@TODO: Add back current time tracking +// podcastModel?.update(currentTime: Float(player.currentTime)) audioView?.updateTimeLabels(currentTimeText: player.timeElapsedText, timeLeftText: player.timeLeftText) @@ -205,7 +208,8 @@ extension AudioViewManager: AssetPlayerDelegate { } func playerPlaybackDidEnd(_ player: AssetPlayer) { - podcastModel?.update(currentTime: 0.0) + //@TODO: Add back current time tracking +// podcastModel?.update(currentTime: 0.0) } func playerIsLikelyToKeepUp(_ player: AssetPlayer) { diff --git a/SEDaily-IOS/Base.lproj/Localizable.strings b/SEDaily-IOS/Base.lproj/Localizable.strings index 8855426..e370503 100644 --- a/SEDaily-IOS/Base.lproj/Localizable.strings +++ b/SEDaily-IOS/Base.lproj/Localizable.strings @@ -48,3 +48,4 @@ "GenericError" = "Error"; "GenericOkay" = "Okay"; "GenericOK" = "OK"; +"Play" = "Play"; diff --git a/SEDaily-IOS/CollectionReusableView.swift b/SEDaily-IOS/CollectionReusableView.swift deleted file mode 100644 index eb9df29..0000000 --- a/SEDaily-IOS/CollectionReusableView.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// CollectionReusableView.swift -// SEDaily-IOS -// -// Created by Craig Holliday on 6/27/17. -// Copyright © 2017 Koala Tea. All rights reserved. -// - -import UIKit - -class CollectionReusableView: UICollectionReusableView { - var titleLabel = UILabel() - - override init(frame: CGRect) { - super.init(frame: frame); - - self.addSubview(titleLabel) - - titleLabel.snp.makeConstraints{ (make) in - make.left.right.equalToSuperview().inset(15.calculateWidth()) - make.top.bottom.equalToSuperview().inset(2.calculateHeight()) - } - - titleLabel.font = UIFont.systemFont(ofSize: 30.calculateWidth()) - titleLabel.minimumScaleFactor = 0.25 - titleLabel.numberOfLines = 1 - titleLabel.textColor = .black - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented"); - } - - func setupTitleLabel(title: String) { - titleLabel.text = title - } -} diff --git a/SEDaily-IOS/ContainerViewController.swift b/SEDaily-IOS/ContainerViewController.swift index d51cc3d..66ed257 100644 --- a/SEDaily-IOS/ContainerViewController.swift +++ b/SEDaily-IOS/ContainerViewController.swift @@ -54,12 +54,10 @@ class ContainerViewController: UIViewController { func setContainerViewInset() { self.containerView.snp.updateConstraints { (make) -> Void in - make.bottom.equalToSuperview().inset(110.calculateHeight()) + make.bottom.equalToSuperview().inset(UIView.getValueScaledByScreenHeightFor(baseValue: 110)) } -// UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: { - self.view.layoutIfNeeded() -// }) + self.view.layoutIfNeeded() } func removeContainerViewInset() { diff --git a/SEDaily-IOS/CustomTabViewController.swift b/SEDaily-IOS/CustomTabViewController.swift index 1a11adc..2188690 100644 --- a/SEDaily-IOS/CustomTabViewController.swift +++ b/SEDaily-IOS/CustomTabViewController.swift @@ -15,7 +15,6 @@ // import UIKit -import SideMenu import SwifterSwift import SnapKit import SwiftIcons @@ -34,9 +33,7 @@ class CustomTabViewController: UITabBarController, UITabBarControllerDelegate { self.view.backgroundColor = .white setupTabs() - setupSideMenu() setupTitleView() - self.selectedIndex = 0 } override func viewDidAppear(_ animated: Bool) { @@ -47,7 +44,7 @@ class CustomTabViewController: UITabBarController, UITabBarControllerDelegate { let rightBarButton = UIBarButtonItem(barButtonSystemItem: .search, target: self, action: #selector(self.leftBarButtonPressed)) self.navigationItem.rightBarButtonItem = rightBarButton - switch User.getActiveUser().isLoggedIn() { + switch UserManager.sharedInstance.getActiveUser().isLoggedIn() { case false: let leftBarButton = UIBarButtonItem(title: L10n.loginTitle, style: .done, target: self, action: #selector(self.loginButtonPressed)) self.navigationItem.leftBarButtonItem = leftBarButton @@ -68,22 +65,10 @@ class CustomTabViewController: UITabBarController, UITabBarControllerDelegate { } @objc func logoutButtonPressed() { - User.logout() + UserManager.sharedInstance.logoutUser() self.setupNavBar() } -// func setupNavButton() { -// let navBarHeight = 44.0 -// let height = navBarHeight / 1.5 -// let icon = #imageLiteral(resourceName: "Bell") -// let iconSize = CGRect(origin: .zero, size: CGSize(width: height, height: height)) -// bellButton = UIButton(frame: iconSize) -// bellButton.setBackgroundImage(icon, for: .normal) -// bellButton.addTarget(self, action: #selector(self.updatesButtonPressed), for: .touchUpInside) -// let barButton = UIBarButtonItem(customView: bellButton) -// navigationItem.rightBarButtonItem = barButton -// } - override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. @@ -91,56 +76,22 @@ class CustomTabViewController: UITabBarController, UITabBarControllerDelegate { func setupTabs() { let layout = UICollectionViewLayout() - let vc1 = PodcastPageViewController() - let vc2 = JustForYouCollectionViewController(collectionViewLayout: layout) - let vc3 = TopCollectionViewController(collectionViewLayout: layout) -// let vc2 = GeneralCollectionViewController(collectionViewLayout: layout, type: API.Types.recommended) -// let vc3 = GeneralCollectionViewController(collectionViewLayout: layout, type: API.Types.top) - - let icon1 = UITabBarItem(title: L10n.tabBarTitleLatest, image: #imageLiteral(resourceName: "mic_stand"), selectedImage: #imageLiteral(resourceName: "mic_stand_selected")) - let icon2 = UITabBarItem(title: L10n.tabBarJustForYou, image: #imageLiteral(resourceName: "activity_feed"), selectedImage: #imageLiteral(resourceName: "activity_feed_selected")) - let icon3 = UITabBarItem(tabBarSystemItem: .mostViewed, tag: 0) - - vc1.tabBarItem = icon1 - vc2.tabBarItem = icon2 - vc3.tabBarItem = icon3 - let controllers = [vc1,vc2,vc3] - self.viewControllers = controllers + self.viewControllers = [ + PodcastPageViewController(), + GeneralCollectionViewController(collectionViewLayout: layout, type: .recommended), + GeneralCollectionViewController(collectionViewLayout: layout, type: .top), + ] self.tabBar.backgroundColor = .white self.tabBar.isTranslucent = false } func setupTitleView() { - let height = 40.calculateHeight() + let height = UIView.getValueScaledByScreenHeightFor(baseValue: 40) let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: height, height: height)) imageView.contentMode = .scaleAspectFit imageView.image = #imageLiteral(resourceName: "Logo_BarButton") self.navigationItem.titleView = imageView } } - -extension CustomTabViewController { - fileprivate func setupSideMenu() { - // Define the menus - guard !ifset else { return } -// let leftSideMenu = UISideMenuNavigationController(rootViewController: LeftViewController()) -// SideMenuManager.menuLeftNavigationController = leftSideMenu - ifset = true - // Enable gestures. The left and/or right menus must be set up above for these to work. - // Note that these continue to work on the Navigation Controller independent of the View Controller it displays! - // SideMenuManager.menuAddPanGestureToPresent(toView: self.navigationController!.navigationBar) - // SideMenuManager.menuAddScreenEdgePanGesturesToPresent(toView: self.navigationController!.view) - - // Set up a cool background image for demo purposes - // SideMenuManager.menuAnimationBackgroundColor = UIColor(patternImage: UIImage(named: "background")!) - SideMenuManager.menuFadeStatusBar = false - - SideMenuManager.menuPresentMode = .viewSlideInOut - } - - func presentLeftSideMenu() { - present(SideMenuManager.menuLeftNavigationController!, animated: true, completion: nil) - } -} diff --git a/SEDaily-IOS/DetailTableViewCell.swift b/SEDaily-IOS/DetailTableViewCell.swift deleted file mode 100644 index e1c390b..0000000 --- a/SEDaily-IOS/DetailTableViewCell.swift +++ /dev/null @@ -1,211 +0,0 @@ -// -// DetailTableViewCell.swift -// SEDaily-IOS -// -// Created by Craig Holliday on 6/28/17. -// Copyright © 2017 Koala Tea. All rights reserved. -// - -import UIKit -import Reusable -import SnapKit -import SwifterSwift - -class DetailTableViewCell: UITableViewCell, Reusable { - let containerView = UIView() - let titleLabel = UILabel() - let descLabel = UILabel() - let ageDescLabel = UILabel() - let ageLabel = UILabel() - let breedDescLabel = UILabel() - let breedLabel = UILabel() - - let contactButton = UIButton() - var buttonStackView = UIStackView() - var contactButtonLabel = UILabel() - var contactButtonImageView = UIImageView() - - override init(style: UITableViewCellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - self.contentView.addSubview(containerView) - - let views = [ - titleLabel, - descLabel, - ageDescLabel, - ageLabel, - breedDescLabel, - breedLabel, - ] - - self.containerView.addSubviews(views) - self.containerView.addSubview(contactButton) - - self.backgroundColor = .clear -// setupConstraints() -// Stylesheet.applyOn(self) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:)") - } - - override func layoutSubviews() { - - } - - func setupConstraints() { - self.contentView.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() - make.bottom.equalToSuperview().priority(99) - make.bottom.equalTo(containerView).priority(100) - } - - titleLabel.snp.makeConstraints { (make) -> Void in - make.top.equalToSuperview().offset(12) - make.left.equalToSuperview() - make.right.equalToSuperview() - } - - descLabel.snp.makeConstraints { (make) -> Void in - make.top.equalTo(titleLabel.snp.bottom).offset(12) - make.left.equalToSuperview() - make.right.equalToSuperview() - } - - ageDescLabel.snp.makeConstraints { (make) -> Void in - make.top.equalTo(descLabel.snp.bottom).offset(20) - make.left.equalToSuperview() - make.right.equalToSuperview() - - make.height.equalTo(20.calculateHeight()) - } - - ageLabel.snp.makeConstraints { (make) -> Void in - make.top.equalTo(ageDescLabel.snp.bottom) - make.left.equalToSuperview() - make.right.equalToSuperview() - - make.height.equalTo(20.calculateHeight()) - } - - breedDescLabel.snp.makeConstraints { (make) -> Void in - make.top.equalTo(ageLabel.snp.bottom).offset(20) - make.left.equalToSuperview() - make.right.equalToSuperview() - - make.height.equalTo(20.calculateHeight()) - } - - breedLabel.snp.makeConstraints { (make) -> Void in - make.top.equalTo(breedDescLabel.snp.bottom) - make.left.equalToSuperview() - make.right.equalToSuperview() - - make.height.equalTo(20.calculateHeight()) - } - - contactButton.snp.makeConstraints { (make) -> Void in - make.top.equalTo(breedLabel.snp.bottom).offset(20) - make.left.equalToSuperview() - make.right.equalToSuperview() - - make.height.equalTo(36.calculateHeight()) - } - - setupButton() - - containerView.snp.makeConstraints { (make) -> Void in - make.top.left.right.equalToSuperview().inset(13) - make.bottom.equalTo(contactButton).offset(46) - } - - setupLabels() - } - - func setupButton() { - self.contactButton.addSubview(buttonStackView) - self.buttonStackView.addArrangedSubview(contactButtonImageView) - self.buttonStackView.addArrangedSubview(contactButtonLabel) - - contactButton.addTarget(self, action: #selector(self.contactButtonPressed), for: .touchUpInside) - contactButton.setTitleColor(.lightGray, for: .normal) - contactButton.setBackgroundColor(color: Stylesheet.Colors.offWhite, forState: .normal) - - contactButton.cornerRadius = 6 - - buttonStackView.alignment = .center - buttonStackView.axis = .horizontal - buttonStackView.distribution = .fillProportionally - - buttonStackView.snp.makeConstraints { (make) -> Void in - make.center.equalToSuperview() - make.height.equalToSuperview() - make.width.equalToSuperview().dividedBy(3) - } - - contactButtonLabel.text = "Contact" - contactButtonLabel.textColor = .lightGray - - contactButtonImageView.image = #imageLiteral(resourceName: "Mail") - contactButtonImageView.contentMode = .scaleAspectFit - - contactButtonLabel.isUserInteractionEnabled = false - contactButtonImageView.isUserInteractionEnabled = false - buttonStackView.isUserInteractionEnabled = false - } - - @objc func contactButtonPressed() { -// guard let urlString = model.websiteURL, model.websiteURL != "" else { -// Helpers.alertWithMessage(title: Helpers.Alerts.error, message: Helpers.Messages.noWebsite) -// return -// } -// guard let url = URL(string: urlString) else { return } -// if #available(iOS 10.0, *) { -// UIApplication.shared.open(url, options: [:], completionHandler: nil) -// } else { -// UIApplication.shared.openURL(url) -// } - } - - func setupLabels() { - - titleLabel.adjustsFontSizeToFitWidth = true - titleLabel.minimumScaleFactor = 0.25 - titleLabel.numberOfLines = 1 - - descLabel.adjustsFontSizeToFitWidth = false - descLabel.minimumScaleFactor = 0.25 - descLabel.numberOfLines = 0 - - ageDescLabel.adjustsFontSizeToFitWidth = true - ageDescLabel.minimumScaleFactor = 0.25 - ageDescLabel.numberOfLines = 1 - - ageLabel.adjustsFontSizeToFitWidth = true - ageLabel.minimumScaleFactor = 0.25 - ageLabel.numberOfLines = 1 - - breedDescLabel.adjustsFontSizeToFitWidth = true - breedDescLabel.minimumScaleFactor = 0.25 - breedDescLabel.numberOfLines = 1 - - breedLabel.adjustsFontSizeToFitWidth = true - breedLabel.minimumScaleFactor = 0.25 - breedLabel.numberOfLines = 1 - } - - - - func setupCell(_ item: PodcastModel) { -// self.model = item -// -// titleLabel.text = item.getName() -// descLabel.text = item.getDescription() -// ageDescLabel.text = item.age -// ageLabel.text = "Age" -// breedDescLabel.text = item.breed1 -// breedLabel.text = "Breed" - } -} diff --git a/SEDaily-IOS/Extensions.swift b/SEDaily-IOS/Extensions.swift deleted file mode 100644 index b98b836..0000000 --- a/SEDaily-IOS/Extensions.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// Extensions.swift -// SEDaily-IOS -// -// Created by Craig Holliday on 6/27/17. -// Copyright © 2017 Koala Tea. All rights reserved. -// - -import UIKit - -extension Int { - func calculateHeight() -> CGFloat { - let screenHeight = UIScreen.main.bounds.height - let divisor: CGFloat = 667 / CGFloat(self) - let calculatedHeight = screenHeight / divisor - return calculatedHeight - } - - func calculateWidth() -> CGFloat { - let screenWidth = UIScreen.main.bounds.width - let divisor: CGFloat = 375.0 / CGFloat(self) - let calculatedWidth = screenWidth / divisor - return calculatedWidth - } -} - -extension Double { - func calculateHeight() -> CGFloat { - let screenHeight = UIScreen.main.bounds.height - let divisor: CGFloat = 667 / CGFloat(self) - let calculatedHeight = screenHeight / divisor - return calculatedHeight - } - - func calculateWidth() -> CGFloat { - let screenWidth = UIScreen.main.bounds.width - let divisor: CGFloat = 375.0 / CGFloat(self) - let calculatedWidth = screenWidth / divisor - return calculatedWidth - } -} - -extension CGFloat { - func calculateHeight() -> CGFloat { - let screenHeight = UIScreen.main.bounds.height - let divisor: CGFloat = 667 / self - let calculatedHeight = screenHeight / divisor - return calculatedHeight - } - - func calculateWidth() -> CGFloat { - let screenWidth = UIScreen.main.bounds.width - let divisor: CGFloat = 375.0 / self - let calculatedWidth = screenWidth / divisor - return calculatedWidth - } -} diff --git a/SEDaily-IOS/FilterObject.swift b/SEDaily-IOS/FilterObject.swift new file mode 100644 index 0000000..19d049c --- /dev/null +++ b/SEDaily-IOS/FilterObject.swift @@ -0,0 +1,38 @@ +// +// FilterObject.swift +// SEDaily-IOS +// +// Created by Craig Holliday on 10/21/17. +// Copyright © 2017 Koala Tea. All rights reserved. +// + +import Foundation + +struct FilterObject: Codable { + let type: String + let tags: [Int] + var tagsAsString: String { + get { + let stringArray = tags.map { String($0) } + return stringArray.joined(separator: " ") + } + } + let lastDate: String + let categories: [Int] + var categoriesAsString: String { + get { + let stringArray = categories.map { String($0) } + return stringArray.joined(separator: " ") + } + } + + init(type: String = "", + tags: [Int] = [], + lastDate: String = "", + categories: [Int] = []) { + self.type = type + self.tags = tags + self.lastDate = lastDate + self.categories = categories + } +} diff --git a/SEDaily-IOS/GeneralCollectionViewController.swift b/SEDaily-IOS/GeneralCollectionViewController.swift index 52a467a..39bc74d 100644 --- a/SEDaily-IOS/GeneralCollectionViewController.swift +++ b/SEDaily-IOS/GeneralCollectionViewController.swift @@ -1,15 +1,14 @@ // -// GeneralCollectionViewController.swift +// CollectionViewController.swift // SEDaily-IOS // -// Created by Craig Holliday on 8/4/17. +// Created by Craig Holliday on 10/12/17. // Copyright © 2017 Koala Tea. All rights reserved. // import UIKit -import RealmSwift import KoalaTeaFlowLayout -import SwifterSwift +import SDWebImage private let reuseIdentifier = "Cell" @@ -18,281 +17,169 @@ class GeneralCollectionViewController: UICollectionViewController { return SkeletonCollectionView(frame: self.collectionView!.frame) }() - var type: String = "" - var tabTitle = "" - var tagId = -1 - var token: NotificationToken? - var data: Results! + var type: PodcastTypes + var tabTitle: String + var tags: [Int] + var categories: [Int] - var itemCount = 0 - - // MARK: - Paging + // Paging Properties var loading = false - let pageSize = 10 let preloadMargin = 5 + var lastLoadedPage = 0 - enum APICheckDates { - static let newFeedLastCheck = "newFeed" + var customTabBarItem: UITabBarItem! { + get { + switch type { + case .new: + return nil + case .recommended: + return UITabBarItem(title: L10n.tabBarJustForYou, image: #imageLiteral(resourceName: "activity_feed"), selectedImage: #imageLiteral(resourceName: "activity_feed_selected")) + case .top: + return UITabBarItem(tabBarSystemItem: .mostViewed, tag: 0) + } + } } - init(collectionViewLayout layout: UICollectionViewLayout, tagId: Int = -1, type: String) { - super.init(collectionViewLayout: layout) - - self.tagId = tagId + // ViewModelController + private let podcastViewModelController: PodcastViewModelController = PodcastViewModelController() + + init(collectionViewLayout layout: UICollectionViewLayout, + tags: [Int] = [], + categories: [PodcastCategoryIds] = [], + type: PodcastTypes = .new, + tabTitle: String = "") { + self.tabTitle = tabTitle self.type = type + self.tags = tags + self.categories = categories.flatMap { $0.rawValue } + super.init(collectionViewLayout: layout) + self.tabBarItem = self.customTabBarItem } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func viewDidLoad() { super.viewDidLoad() - + // Uncomment the following line to preserve selection between presentations // self.clearsSelectionOnViewWillAppear = false - + // Register cell classes - self.collectionView!.register(PodcastCell.self, forCellWithReuseIdentifier: reuseIdentifier) - - self.collectionView?.backgroundColor = UIColor(hex: 0xfafafa) - self.collectionView?.showsHorizontalScrollIndicator = false - self.collectionView?.showsVerticalScrollIndicator = false + self.collectionView?.register(PodcastCell.self, forCellWithReuseIdentifier: reuseIdentifier) - let layout = KoalaTeaFlowLayout(cellWidth: 158, - cellHeight: 250, - topBottomMargin: 12, - leftRightMargin: 20, - cellSpacing: 8) + let layout = KoalaTeaFlowLayout(cellWidth: UIView.getValueScaledByScreenWidthFor(baseValue: 158), + cellHeight: UIView.getValueScaledByScreenHeightFor(baseValue: 250), + topBottomMargin: UIView.getValueScaledByScreenHeightFor(baseValue: 12), + leftRightMargin: UIView.getValueScaledByScreenWidthFor(baseValue: 20), + cellSpacing: UIView.getValueScaledByScreenWidthFor(baseValue: 8)) self.collectionView?.collectionViewLayout = layout + self.collectionView?.backgroundColor = .white // User Login observer NotificationCenter.default.addObserver(self, selector: #selector(self.loginObserver), name: .loginChanged, object: nil) - self.collectionView?.addSubview(skeletonCollectionView) } - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - - @objc func loginObserver() { - loadData(lastItemDate: "") - } - - // MARK: - Data stuff - - func getData(page: Int = 0, lastItemDate: String = "") { - guard self.loading == false else { return } - - lastLoadedPage = page - - self.loading = true - loadData(lastItemDate: lastItemDate) - } - - func checkPage(indexPath: IndexPath, item: PodcastModel) { - let nextPage: Int = Int(indexPath.item / pageSize) + 1 - let preloadIndex = nextPage * pageSize - preloadMargin - - if (indexPath.item >= preloadIndex && lastLoadedPage < nextPage) || indexPath == collectionView?.indexPathForLastItem! { - if let lastDate = item.uploadDate { - //@TODO: This is left open for paging for recommended and top posts - // (I think top posts could be paged) - guard type != API.Types.recommended || type != API.Types.top else { return } - getData(page: nextPage, lastItemDate: lastDate) - } + override func viewDidAppear(_ animated: Bool) { + // Make sure skeletonCollectionView is animating when the view is visible + if self.skeletonCollectionView.alpha != 0 { + self.skeletonCollectionView.collectionView.reloadData() } } - // @TODO: Move to repository - func loadNewLocalPodcasts (hasChanges: Bool) { - //@TODO: Fix this, right now it stops all loading completely - // if hasChanges { - self.loading = false - // } - - // @TODO: Should probably only load count - var returnData = PodcastModel.all() - - if self.tagId != -1 { - returnData = returnData.filter("categories CONTAINS '\(self.tagId)'") - } - - self.data = returnData.sorted(byKeyPath: "uploadDate", ascending: false) - self.registerNotifications() + override func viewDidDisappear(_ animated: Bool) { + //@TODO: Find a better way to manage cached Images + SDImageCache.shared().clearMemory() } - func alreadyLoadedNewToday (tagId: Int, lastItemDate: String?) -> Bool { - let defaults = UserDefaults.standard - // @TODO: we may be able to add this to the filters dictionary - var key = APICheckDates.newFeedLastCheck - - var filters = [String: String]() - filters["lastItemDate"] = lastItemDate - filters["tagId"] = String(tagId) - - key = "\(key)-\(filters.description)" - - if let newFeedLastCheck = defaults.string(forKey: key) { - let todayDate = Date().dateString() - let newFeedDate = Date(iso8601String: newFeedLastCheck)!.dateString() - if (newFeedDate == todayDate) { - return true + @objc func loginObserver() { + if self.type == .recommended { + self.podcastViewModelController.clearViewModels() + DispatchQueue.main.async { + self.collectionView?.reloadData() } - - return false } - return false - } - - func setLoadedNewToday (tagId: Int, lastItemDate: String?) { - let todayString = Date().iso8601String - var key = APICheckDates.newFeedLastCheck - - var filters = [String: String]() - filters["lastItemDate"] = lastItemDate - filters["tagId"] = String(tagId) - - key = "\(key)-\(filters.description)" - - let defaults = UserDefaults.standard - defaults.set(todayString, forKey: key) + self.getData(lastIdentifier: "", nextPage: 0) } - - - func loadData(lastItemDate: String) { - //@TODO: Fix this for recommended and top - switch type { - case API.Types.new: - let alreadLoadedStartToday = self.alreadyLoadedNewToday(tagId: self.tagId, lastItemDate: lastItemDate) - - if (alreadLoadedStartToday) { - // !TODO: This may be being called during scroll when it doesn't need to be since we load all. However, we probably shouldn't load all from realm? - self.loadNewLocalPodcasts(hasChanges: false) - return; - } - - API.sharedInstance.getPosts(type: type, createdAtBefore: lastItemDate, categoires: String(self.tagId), completion: { (hasChanges) in - self.setLoadedNewToday(tagId: self.tagId, lastItemDate: lastItemDate) - self.loadNewLocalPodcasts(hasChanges: hasChanges) - }) - break - case API.Types.recommended: - guard User.getActiveUser().isLoggedIn() else { - API.sharedInstance.getPosts(type: API.Types.top, createdAtBefore: lastItemDate, completion: { (hasChanges) in - - if hasChanges { - self.loading = false - } - - self.data = PodcastModel.getTop() - self.registerNotifications() - }) - break - } - API.sharedInstance.getRecommendedPosts(completion: { (hasChanges) in - if hasChanges { - self.loading = false - } - - self.data = PodcastModel.getRecommended() - self.registerNotifications() - }) - break - case API.Types.top: - API.sharedInstance.getPosts(type: API.Types.top, createdAtBefore: lastItemDate, completion: { (hasChanges) in - if hasChanges { - self.loading = false - } - - self.data = PodcastModel.getTop() - self.registerNotifications() - }) - break - default: - break - } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. } -} -extension GeneralCollectionViewController { // MARK: UICollectionViewDataSource - + override func numberOfSections(in collectionView: UICollectionView) -> Int { - // #warning Incomplete implementation, return the number of sections return 1 } - + override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - if itemCount > 0 { + if podcastViewModelController.viewModelsCount > 0 { self.skeletonCollectionView.fadeOut(duration: 0.5, completion: nil) } - if itemCount < 10 { - self.getData() + if podcastViewModelController.viewModelsCount <= 0 { + // Load initial data + self.getData(lastIdentifier: "", nextPage: 0) } - return itemCount + return podcastViewModelController.viewModelsCount } - + override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! PodcastCell - - let item = data[indexPath.row] - - checkPage(indexPath: indexPath, item: item) - + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! PodcastCell + // Configure the cell - let uploadDate = Date(iso8601String: (item.uploadDate ?? "")) - cell.setupCell(imageURLString: item.imageURLString, title: item.podcastName!, timeLength: nil, date: uploadDate) - + if let viewModel = podcastViewModelController.viewModel(at: indexPath.row) { + cell.viewModel = viewModel + if let lastIndexPath = self.collectionView?.indexPathForLastItem { + if let lastItem = podcastViewModelController.viewModel(at: lastIndexPath.row) { + self.checkPage(currentIndexPath: indexPath, + lastIndexPath: lastIndexPath, + lastIdentifier: lastItem.uploadDateiso8601) + } + } + } + return cell } - override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let item = data[indexPath.row] - let vc = PostDetailTableViewController() - vc.model = item - self.navigationController?.pushViewController(vc, animated: true) - } -} + func checkPage(currentIndexPath: IndexPath, lastIndexPath: IndexPath, lastIdentifier: String) { + let nextPage: Int = Int(currentIndexPath.item / self.pageSize) + 1 + let preloadIndex = nextPage * self.pageSize - self.preloadMargin -extension GeneralCollectionViewController { - // MARK: Realm - func registerNotifications() { - token = data.addNotificationBlock {[weak self] (changes: RealmCollectionChange) in - guard let collectionView = self?.collectionView else { return } - - switch changes { - case .initial: - guard let int = self?.data.count else { return } - self?.itemCount = int - collectionView.reloadData() - break - case .update(_, let deletions, let insertions, let modifications): - let deleteIndexPaths = deletions.map { IndexPath(item: $0, section: 0) } - let insertIndexPaths = insertions.map { IndexPath(item: $0, section: 0) } - let updateIndexPaths = modifications.map { IndexPath(item: $0, section: 0) } - - self?.collectionView?.performBatchUpdates({ - self?.collectionView?.deleteItems(at: deleteIndexPaths) - if !deleteIndexPaths.isEmpty { - self?.itemCount -= 1 - } - self?.collectionView?.insertItems(at: insertIndexPaths) - if !insertIndexPaths.isEmpty { - self?.itemCount += 1 - } - self?.collectionView?.reloadItems(at: updateIndexPaths) - }, completion: nil) - break - case .error(let error): - print(error) - break + if (currentIndexPath.item >= preloadIndex && self.lastLoadedPage < nextPage) || currentIndexPath == lastIndexPath { + // @TODO: Turn lastIdentifier into some T + self.getData(lastIdentifier: lastIdentifier, nextPage: nextPage) + } + } + + func getData(lastIdentifier: String, nextPage: Int) { + guard self.loading == false else { return } + self.loading = true + podcastViewModelController.fetchData(type: self.type.rawValue, createdAtBefore: lastIdentifier, tags: self.tags, categories: self.categories, page: nextPage, onSucces: { + self.loading = false + self.lastLoadedPage = nextPage + DispatchQueue.main.async { + self.collectionView?.reloadData() } + }) { (apiError) in + self.loading = false + log.error(apiError) } } + + // MARK: UICollectionViewDelegate + + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if let viewModel = podcastViewModelController.viewModel(at: indexPath.row) { + let vc = PodcastDetailViewController() + vc.model = viewModel + self.navigationController?.pushViewController(vc, animated: true) + } + } + } diff --git a/SEDaily-IOS/HeaderView.swift b/SEDaily-IOS/HeaderView.swift index 50e8fad..98eebe3 100644 --- a/SEDaily-IOS/HeaderView.swift +++ b/SEDaily-IOS/HeaderView.swift @@ -10,7 +10,7 @@ import UIKit import SwiftIcons class HeaderView: UIView { - var model: PodcastModel! + var model = PodcastViewModel() let titleLabel = UILabel() let dateLabel = UILabel() @@ -28,7 +28,6 @@ class HeaderView: UIView { super.init(frame: frame); self.performLayout() -// Stylesheet.applyOn(self) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented"); } @@ -45,28 +44,28 @@ class HeaderView: UIView { setupPlayView() titleLabel.snp.makeConstraints{ (make) in - make.bottom.equalTo(playView.snp.top).offset(-60.calculateHeight()) - make.left.equalToSuperview().offset(15.calculateHeight()) - make.right.equalToSuperview().inset(15.calculateHeight()) + make.bottom.equalTo(playView.snp.top).offset(UIView.getValueScaledByScreenHeightFor(baseValue: -60)) + make.left.equalToSuperview().offset(UIView.getValueScaledByScreenWidthFor(baseValue: 15)) + make.right.equalToSuperview().inset(UIView.getValueScaledByScreenHeightFor(baseValue: 15)) } dateLabel.snp.makeConstraints{ (make) in - make.top.equalTo(titleLabel.snp.bottom).offset(10.calculateHeight()) - make.left.equalToSuperview().offset(15.calculateHeight()) - make.right.equalToSuperview().inset(15.calculateHeight()) + make.top.equalTo(titleLabel.snp.bottom).offset(UIView.getValueScaledByScreenHeightFor(baseValue: 10)) + make.left.equalToSuperview().offset(UIView.getValueScaledByScreenWidthFor(baseValue: 15)) + make.right.equalToSuperview().inset(UIView.getValueScaledByScreenHeightFor(baseValue: 15)) } setupLabels() } func setupLabels() { - titleLabel.font = UIFont(font: .helveticaNeue, size: 20.calculateHeight()) + titleLabel.font = UIFont(font: .helveticaNeue, size: UIView.getValueScaledByScreenWidthFor(baseValue: 20)) titleLabel.adjustsFontSizeToFitWidth = false titleLabel.minimumScaleFactor = 0.25 titleLabel.numberOfLines = 0 titleLabel.textColor = Stylesheet.Colors.white - dateLabel.font = UIFont(font: .helveticaNeue, size: 16.calculateHeight()) + dateLabel.font = UIFont(font: .helveticaNeue, size: UIView.getValueScaledByScreenWidthFor(baseValue: 16)) dateLabel.adjustsFontSizeToFitWidth = false dateLabel.minimumScaleFactor = 0.25 dateLabel.numberOfLines = 1 @@ -80,27 +79,27 @@ class HeaderView: UIView { playView.snp.makeConstraints{ (make) in make.bottom.equalToSuperview() make.width.equalToSuperview() - make.height.equalTo(65.calculateHeight()) + make.height.equalTo(UIView.getValueScaledByScreenHeightFor(baseValue: 65)) } playView.addSubview(playButton) - playButton.setTitle("Play", for: .normal) + playButton.setTitle(L10n.play, for: .normal) playButton.setBackgroundColor(color: Stylesheet.Colors.secondaryColor, forState: .normal) playButton.addTarget(self, action: #selector(self.playButtonPressed), for: .touchUpInside) - playButton.cornerRadius = 4.calculateWidth() + playButton.cornerRadius = UIView.getValueScaledByScreenHeightFor(baseValue: 4) playButton.snp.makeConstraints{ (make) in make.centerY.equalToSuperview() - make.right.equalToSuperview().inset(15.calculateWidth()) - make.width.equalTo(84.calculateWidth()) - make.height.equalTo(42.calculateHeight()) + make.right.equalToSuperview().inset(UIView.getValueScaledByScreenWidthFor(baseValue: 15)) + make.width.equalTo(UIView.getValueScaledByScreenWidthFor(baseValue: 84)) + make.height.equalTo(UIView.getValueScaledByScreenHeightFor(baseValue: 42)) } playView.addSubview(voteView) voteView.snp.makeConstraints{ (make) in make.centerY.equalToSuperview() - make.left.equalToSuperview().inset(15.calculateWidth()) - make.width.equalTo((35 * 3).calculateWidth()) + make.left.equalToSuperview().inset(UIView.getValueScaledByScreenWidthFor(baseValue: 15)) + make.width.equalTo(UIView.getValueScaledByScreenWidthFor(baseValue: (35 * 3))) make.height.equalToSuperview() } @@ -118,123 +117,112 @@ class HeaderView: UIView { scoreLabel.textAlignment = .center scoreLabel.baselineAdjustment = .alignCenters - scoreLabel.font = UIFont(font: .helveticaNeue, size: 24.calculateWidth()) + scoreLabel.font = UIFont(font: .helveticaNeue, size: UIView.getValueScaledByScreenWidthFor(baseValue: 24)) - downVoteButton.setIcon(icon: .fontAwesome(.thumbsODown), iconSize: 35.calculateHeight(), color: Stylesheet.Colors.offBlack, forState: .normal) - downVoteButton.setIcon(icon: .fontAwesome(.thumbsDown), iconSize: 35.calculateHeight(), color: Stylesheet.Colors.base, forState: .selected) + let iconSize = UIView.getValueScaledByScreenHeightFor(baseValue: 35) + downVoteButton.setIcon(icon: .fontAwesome(.thumbsODown), iconSize: iconSize, color: Stylesheet.Colors.offBlack, forState: .normal) + downVoteButton.setIcon(icon: .fontAwesome(.thumbsDown), iconSize: iconSize, color: Stylesheet.Colors.base, forState: .selected) downVoteButton.setTitleColor(Stylesheet.Colors.secondaryColor, for: .selected) downVoteButton.addTarget(self, action: #selector(self.downVoteButtonPressed), for: .touchUpInside) - upVoteButton.setIcon(icon: .fontAwesome(.thumbsOUp), iconSize: 35.calculateHeight(), color: Stylesheet.Colors.offBlack, forState: .normal) - upVoteButton.setIcon(icon: .fontAwesome(.thumbsUp), iconSize: 35.calculateHeight(), color: Stylesheet.Colors.base, forState: .selected) + upVoteButton.setIcon(icon: .fontAwesome(.thumbsOUp), iconSize: iconSize, color: Stylesheet.Colors.offBlack, forState: .normal) + upVoteButton.setIcon(icon: .fontAwesome(.thumbsUp), iconSize: iconSize, color: Stylesheet.Colors.base, forState: .selected) upVoteButton.setTitleColor(Stylesheet.Colors.secondaryColor, for: .selected) upVoteButton.addTarget(self, action: #selector(self.upvoteButtonPressed), for: .touchUpInside) } - func setupHeader(model: PodcastModel) { + func setupHeader(model: PodcastViewModel) { self.model = model - self.titleLabel.text = model.podcastName! - self.dateLabel.text = Helpers.formatDate(dateString: model.uploadDate!) - self.scoreLabel.text = model.score! - - if self.model.isUpvoted { - upVoteButton.isSelected = self.model.isUpvoted - guard var int = Int(model.score!) else { return } - int += 1 - self.scoreLabel.text = String(int) - } - if self.model.isDownvoted { - downVoteButton.isSelected = self.model.isDownvoted - guard var int = Int(model.score!) else { return } - int -= 1 - self.scoreLabel.text = String(int) - } + self.titleLabel.text = model.podcastTitle + self.dateLabel.text = model.getLastUpdatedAsDate()?.dateString() ?? "" + self.scoreLabel.text = model.score.string + + upVoteButton.isSelected = self.model.isUpvoted + downVoteButton.isSelected = self.model.isDownvoted + self.scoreLabel.text = String(self.model.score) } } extension HeaderView { @objc func playButtonPressed() { //@TODO: Switch button and/or stop if playing -// AudioViewManager.shared.presentAudioView() - _ = "http://traffic.libsyn.com/rtpodcast/podcast_update.mp3" // Podcast model checks here AudioViewManager.shared.setupManager(podcastModel: model) } @objc func upvoteButtonPressed() { - guard User.checkAndAlert() else { return } - guard let podcastId = model.podcastId else { return } + guard UserManager.sharedInstance.isCurrentUserLoggedIn() == true else { + Helpers.alertWithMessage(title: Helpers.Alerts.error, message: Helpers.Messages.youMustLogin, completionHandler: nil) + return + } + + // Immediately set UI to upvote + self.setUpvoteTo(!self.upVoteButton.isSelected) + self.setDownvoteTo(false) + + let podcastId = model._id API.sharedInstance.upvotePodcast(podcastId: podcastId, completion: { (success, active) in guard success != nil else { return } if success == true { guard let active = active else { return } - switch active { - case true: - self.addScore(active: active) - case false: - self.addScore(active: active) - } + self.addScore(active: active) } }) } @objc func downVoteButtonPressed() { - guard User.checkAndAlert() else { return } - guard let podcastId = model.podcastId else { return } + guard UserManager.sharedInstance.isCurrentUserLoggedIn() == true else { + Helpers.alertWithMessage(title: Helpers.Alerts.error, message: Helpers.Messages.youMustLogin, completionHandler: nil) + return + } + + // Immediately set UI to downvote + self.setUpvoteTo(false) + self.setDownvoteTo(!self.downVoteButton.isSelected) + + let podcastId = model._id API.sharedInstance.downvotePodcast(podcastId: podcastId, completion: { (success, active) in guard success != nil else { return } if success == true { // Switch if active guard let active = active else { return } - switch active { - case true: - self.subtractScore(active: active) - case false: - self.subtractScore(active: active) - } + self.subtractScore(active: active) } }) } func addScore(active: Bool) { - if active == false { - self.scoreLabel.text = String(model.score!) - self.model.update(isUpvoted: false) - upVoteButton.isSelected = self.model.isUpvoted - downVoteButton.isSelected = self.model.isDownvoted + self.setUpvoteTo(active) + guard active != false else { + self.setScoreTo(self.model.score - 1) return } - - guard let score = self.model.score else { return } - guard var int = Int(score) else { return } - int += 1 - // Update score label - self.scoreLabel.text = String(int) - - self.model.update(isUpvoted: true) - - upVoteButton.isSelected = self.model.isUpvoted - downVoteButton.isSelected = self.model.isDownvoted + self.setScoreTo(self.model.score + 1) } func subtractScore(active: Bool) { - if active == false { - self.scoreLabel.text = String(model.score!) - self.model.update(isDownvoted: false) - upVoteButton.isSelected = self.model.isUpvoted - downVoteButton.isSelected = self.model.isDownvoted + self.setDownvoteTo(active) + guard active != false else { + self.setScoreTo(self.model.score + 1) return } - guard let score = self.model.score else { return } - guard var int = Int(score) else { return } - int -= 1 - // Update score label - self.scoreLabel.text = String(int) - - self.model.update(isDownvoted: true) - - upVoteButton.isSelected = self.model.isUpvoted - downVoteButton.isSelected = self.model.isDownvoted + self.setScoreTo(self.model.score - 1) + } + + func setUpvoteTo(_ bool: Bool) { + self.model.isUpvoted = bool + self.upVoteButton.isSelected = bool + } + + func setDownvoteTo(_ bool: Bool) { + self.model.isDownvoted = bool + self.downVoteButton.isSelected = bool + } + + func setScoreTo(_ score: Int) { + guard self.model.score != score else { return } + self.model.score = score + self.scoreLabel.text = String(score) } } diff --git a/SEDaily-IOS/Helpers.swift b/SEDaily-IOS/Helpers.swift index 609338f..6d5c2cd 100644 --- a/SEDaily-IOS/Helpers.swift +++ b/SEDaily-IOS/Helpers.swift @@ -9,7 +9,6 @@ import UIKit import SwifterSwift -import RealmSwift extension Helpers { static var alert: UIAlertController! @@ -123,27 +122,44 @@ extension Helpers { } } +import SwiftSoup + public extension String { - /// Decodes string with html encoding. + // Decodes string with html encoding. + // This is very fast var htmlDecoded: String { - guard let encodedData = self.data(using: .utf8) else { return self } - - let attributedOptions: [NSAttributedString.DocumentReadingOptionKey : Any] = [ - NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html, - NSAttributedString.DocumentReadingOptionKey.characterEncoding: String.Encoding.utf8.rawValue] - do { - let attributedString = try NSAttributedString(data: encodedData, - options: attributedOptions, - documentAttributes: nil) - return attributedString.string + let html = self + let doc: Document = try SwiftSoup.parse(html) + return try doc.text() } catch { - print("Error: \(error)") return self } } } +extension String { + // Decode HTML while keeping attributes like "/n" and bulleted lists + // This is a bit slow + var htmlDecodedWithSomeEntities: String? { + guard let data = self.data(using: .utf8) else { + return nil + } + let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [ + NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html, + NSAttributedString.DocumentReadingOptionKey.characterEncoding: String.Encoding.utf8.rawValue + ] + do { + + let attributedString = try NSAttributedString(data: data, options: options, documentAttributes: nil) + return attributedString.string + } + catch { + return nil + } + } +} + class TextField: UITextField { let padding = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10); diff --git a/SEDaily-IOS/Info.plist b/SEDaily-IOS/Info.plist index 60a48fd..c8366e7 100644 --- a/SEDaily-IOS/Info.plist +++ b/SEDaily-IOS/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.2 CFBundleVersion - 6 + 12 Fabric APIKey diff --git a/SEDaily-IOS/JustForYouCollectionViewController.swift b/SEDaily-IOS/JustForYouCollectionViewController.swift deleted file mode 100644 index 3e2f744..0000000 --- a/SEDaily-IOS/JustForYouCollectionViewController.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// JustForYouCollectionViewController.swift -// SEDaily-IOS -// -// Created by Craig Holliday on 7/25/17. -// Copyright © 2017 Koala Tea. All rights reserved. -// - -import UIKit -import RealmSwift -import KoalaTeaFlowLayout - -private let reuseIdentifier = "Cell" - -class JustForYouCollectionViewController: UICollectionViewController { - var skeletonCollectionView = SkeletonCollectionView(frame: .zero) - - var token: NotificationToken? - var data: Results = { - let data = PodcastModel.getTop() - - return data - }() - - var itemCount = 0 - - override func viewDidLoad() { - super.viewDidLoad() - - // Uncomment the following line to preserve selection between presentations - // self.clearsSelectionOnViewWillAppear = false - - // Register cell classes - self.collectionView!.register(PodcastCell.self, forCellWithReuseIdentifier: reuseIdentifier) - - self.collectionView?.backgroundColor = UIColor(hex: 0xfafafa) - self.collectionView?.showsHorizontalScrollIndicator = false - self.collectionView?.showsVerticalScrollIndicator = false - - let layout = KoalaTeaFlowLayout(cellWidth: 158, - cellHeight: 250, - topBottomMargin: 12, - leftRightMargin: 20, - cellSpacing: 8) - self.collectionView?.collectionViewLayout = layout - - // User Login observer - NotificationCenter.default.addObserver(self, selector: #selector(self.loginObserver), name: .loginChanged, object: nil) - - self.skeletonCollectionView = SkeletonCollectionView(frame: collectionView!.frame) - self.collectionView?.addSubview(skeletonCollectionView) - - loadData() - } - - @objc func loginObserver() { - itemCount = 0 - loadData() - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - - override func viewDidAppear(_ animated: Bool) { - // loadData() - } - - func loadData() { - guard User.getActiveUser().isLoggedIn() else { - API.sharedInstance.getPosts(type: API.Types.top, completion: {_ in - }) - self.data = PodcastModel.getTop() - self.registerNotifications() - return - } - API.sharedInstance.getRecommendedPosts(completion: {_ in - }) - self.data = PodcastModel.getRecommended() - self.registerNotifications() - } -} - - -extension JustForYouCollectionViewController { - // MARK: UICollectionViewDataSource - - override func numberOfSections(in collectionView: UICollectionView) -> Int { - // #warning Incomplete implementation, return the number of sections - return 1 - } - - - override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - if itemCount > 0 { - self.skeletonCollectionView.fadeOut(duration: 0.5, completion: nil) - } - return itemCount - } - - override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! PodcastCell - - let item = data[indexPath.row] - - // Configure the cell - let uploadDate = Date(iso8601String: (item.uploadDate ?? "")) - cell.setupCell(imageURLString: item.imageURLString, title: item.podcastName!, timeLength: nil, date: uploadDate) - - return cell - } - - override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let item = data[indexPath.row] - let vc = PostDetailTableViewController() - vc.model = item - self.navigationController?.pushViewController(vc, animated: true) - } -} - -extension JustForYouCollectionViewController { - // MARK: Realm - func registerNotifications() { - token = data.addNotificationBlock {[weak self] (changes: RealmCollectionChange) in - guard let collectionView = self?.collectionView else { return } - - switch changes { - case .initial: - guard let int = self?.data.count else { return } - self?.itemCount = int - collectionView.reloadData() - break - case .update(_, let deletions, let insertions, let modifications): - let deleteIndexPaths = deletions.map { IndexPath(item: $0, section: 0) } - let insertIndexPaths = insertions.map { IndexPath(item: $0, section: 0) } - let updateIndexPaths = modifications.map { IndexPath(item: $0, section: 0) } - - self?.collectionView?.performBatchUpdates({ - self?.collectionView?.deleteItems(at: deleteIndexPaths) - if !deleteIndexPaths.isEmpty { - self?.itemCount -= 1 - } - self?.collectionView?.insertItems(at: insertIndexPaths) - if !insertIndexPaths.isEmpty { - self?.itemCount += 1 - } - self?.collectionView?.reloadItems(at: updateIndexPaths) - }, completion: nil) - break - case .error(let error): - print(error) - break - } - } - } -} diff --git a/SEDaily-IOS/LoginViewController.swift b/SEDaily-IOS/LoginViewController.swift index 0d8bbee..8699a68 100644 --- a/SEDaily-IOS/LoginViewController.swift +++ b/SEDaily-IOS/LoginViewController.swift @@ -38,14 +38,6 @@ class LoginViewController: UIViewController { self.view.backgroundColor = Stylesheet.Colors.base } - override func viewWillAppear(_ animated: Bool) { -// self.navigationBar?.setColors(background: Stylesheet.Colors.base, text: Stylesheet.Colors.white) - } - - override func viewWillDisappear(_ animated: Bool) { -// self.navigationBar?.setColors(background: Stylesheet.Colors.white, text: Stylesheet.Colors.base) - } - override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. @@ -55,20 +47,9 @@ class LoginViewController: UIViewController { return .lightContent } - override func viewDidLayoutSubviews() { -// if User.getActiveUser().isLoggedIn() != false { -// -// // If so move on to the next screen -//// let vc = CustomTabViewController() -//// self.navigationController?.pushViewController(vc, animated: true) -// User.logout() -// self.navigationController?.popViewController() -// } - } - func addBottomBorderToView(view: UIView, height: CGFloat, width: CGFloat) { let border = CALayer() - let borderWidth = CGFloat(2.0.calculateHeight()) + let borderWidth = CGFloat(UIView.getValueScaledByScreenHeightFor(baseValue: 2)) border.borderColor = Stylesheet.Colors.secondaryColor.cgColor border.frame = CGRect(x: 0, y: height - borderWidth, width: width, height: height) @@ -148,43 +129,45 @@ class LoginViewController: UIViewController { make.centerX.equalToSuperview() make.top.equalToSuperview().inset(50) - let height = 200.calculateHeight() + let height = UIView.getValueScaledByScreenHeightFor(baseValue: 200) make.height.equalTo(height) make.width.equalTo(height) } } private func setupTextFields() { + let width = UIView.getValueScaledByScreenWidthFor(baseValue: 316) + let height = UIView.getValueScaledByScreenHeightFor(baseValue: 36) emailTextField.snp.makeConstraints { (make) -> Void in - make.width.equalTo(316.calculateWidth()) - make.height.equalTo(36.calculateHeight()) + make.width.equalTo(width) + make.height.equalTo(height) } passwordTextField.snp.makeConstraints { (make) -> Void in - make.width.equalTo(316.calculateWidth()) - make.height.equalTo(36.calculateHeight()) + make.width.equalTo(width) + make.height.equalTo(height) } passwordConfirmTextField.snp.makeConstraints { (make) -> Void in - make.width.equalTo(316.calculateWidth()) - make.height.equalTo(36.calculateHeight()) + make.width.equalTo(width) + make.height.equalTo(height) } firstNameTextField.snp.makeConstraints { (make) -> Void in - make.width.equalTo(316.calculateWidth()) - make.height.equalTo(36.calculateHeight()) + make.width.equalTo(width) + make.height.equalTo(height) } lastNameTextField.snp.makeConstraints { (make) -> Void in - make.width.equalTo(316.calculateWidth()) - make.height.equalTo(36.calculateHeight()) + make.width.equalTo(width) + make.height.equalTo(height) } - addBottomBorderToView(view: emailTextField, height: 36.calculateHeight(), width: 316.calculateWidth()) - addBottomBorderToView(view: passwordTextField, height: 36.calculateHeight(), width: 316.calculateWidth()) - addBottomBorderToView(view: passwordConfirmTextField, height: 36.calculateHeight(), width: 316.calculateWidth()) - addBottomBorderToView(view: firstNameTextField, height: 36.calculateHeight(), width: 316.calculateWidth()) - addBottomBorderToView(view: lastNameTextField, height: 36.calculateHeight(), width: 316.calculateWidth()) + addBottomBorderToView(view: emailTextField, height: height, width: width) + addBottomBorderToView(view: passwordTextField, height: height, width: width) + addBottomBorderToView(view: passwordConfirmTextField, height: height, width: width) + addBottomBorderToView(view: firstNameTextField, height: height, width: width) + addBottomBorderToView(view: lastNameTextField, height: height, width: width) emailTextField.placeholder = L10n.emailAddressPlaceholder emailTextField.setPlaceHolderTextColor(Stylesheet.Colors.secondaryColor) @@ -220,39 +203,42 @@ class LoginViewController: UIViewController { } private func setupButtons() { + let width = UIView.getValueScaledByScreenWidthFor(baseValue: 94) + let height = UIView.getValueScaledByScreenHeightFor(baseValue: 42) loginButton.snp.makeConstraints { (make) -> Void in - make.width.equalTo(94.calculateWidth()) - make.height.equalTo(42.calculateHeight()) + make.width.equalTo(width) + make.height.equalTo(height) } cancelButton.snp.makeConstraints { (make) -> Void in - make.width.equalTo(94.calculateWidth()) - make.height.equalTo(42.calculateHeight()) + make.width.equalTo(width) + make.height.equalTo(height) } signUpButton.snp.makeConstraints { (make) -> Void in - make.width.equalTo(94.calculateWidth()) - make.height.equalTo(42.calculateHeight()) + make.width.equalTo(width) + make.height.equalTo(height) } loginButton.setTitle(L10n.loginButtonTitle, for: .normal) + let cornerRadius = UIView.getValueScaledByScreenWidthFor(baseValue: 4) loginButton.setTitleColor(Stylesheet.Colors.white, for: .normal) loginButton.setBackgroundColor(color: Stylesheet.Colors.secondaryColor, forState: .normal) loginButton.addTarget(self, action: #selector(self.loginButtonPressed), for: .touchUpInside) - loginButton.cornerRadius = 4.calculateWidth() + loginButton.cornerRadius = cornerRadius cancelButton.setTitle(L10n.cancelButtonTitle, for: .normal) cancelButton.setTitleColor(Stylesheet.Colors.white, for: .normal) cancelButton.setBackgroundColor(color: Stylesheet.Colors.secondaryColor, forState: .normal) cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), for: .touchUpInside) - cancelButton.cornerRadius = 4.calculateWidth() + cancelButton.cornerRadius = cornerRadius cancelButton.isHidden = true signUpButton.setTitle(L10n.signUpButtonTitle, for: .normal) signUpButton.setTitleColor(Stylesheet.Colors.white, for: .normal) signUpButton.setBackgroundColor(color: Stylesheet.Colors.secondaryColor, forState: .normal) signUpButton.addTarget(self, action: #selector(self.signUpButtonPressed), for: .touchUpInside) - signUpButton.cornerRadius = 4.calculateWidth() + signUpButton.cornerRadius = cornerRadius } @objc func loginButtonPressed() { @@ -279,16 +265,12 @@ class LoginViewController: UIViewController { } // API Login Call - API.sharedInstance.login(username: email, password: password, completion: { (success) -> Void in - + API.sharedInstance.login(firstName: "", lastName: "", email: email, password: password) { (success) in if success == false { -// HUD.hide() return } -// HUD.hide({ _ in self.navigationController?.popViewController() -// }) - }) + } } @objc func cancelButtonPressed() { @@ -343,15 +325,11 @@ class LoginViewController: UIViewController { } // API Login Call - API.sharedInstance.register(username: email, password: password, completion: { (success) -> Void in - + API.sharedInstance.register(firstName: "", lastName: "", email: email, password: password, completion: { (success) -> Void in if success == false { -// HUD.hide() return } -// HUD.hide({ _ in - self.navigationController?.popViewController() -// }) + self.navigationController?.popViewController() }) } } diff --git a/SEDaily-IOS/NotificationCenterExtension.swift b/SEDaily-IOS/NotificationCenterExtension.swift deleted file mode 100644 index 4ca8c78..0000000 --- a/SEDaily-IOS/NotificationCenterExtension.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// NotificationCenterExtension.swift -// SEDaily-IOS -// -// Created by Craig Holliday on 6/30/17. -// Copyright © 2017 Koala Tea. All rights reserved. -// - -import UIKit - -extension Notification.Name { - static let playingAudio = Notification.Name("playingAudio") -} diff --git a/SEDaily-IOS/ObjectExtensions.swift b/SEDaily-IOS/ObjectExtensions.swift deleted file mode 100644 index 58036fe..0000000 --- a/SEDaily-IOS/ObjectExtensions.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// ObjectExtensions.swift -// SEDaily-IOS -// -// Created by Craig Holliday on 8/3/17. -// Copyright © 2017 Koala Tea. All rights reserved. -// - -import Foundation -import UIKit -import RealmSwift - -extension Object { - @objc func toDictionary() -> NSDictionary { - let properties = self.objectSchema.properties.map { $0.name } - let dictionary = self.dictionaryWithValues(forKeys: properties) - let mutabledic = NSMutableDictionary() - mutabledic.setValuesForKeys(dictionary) - - for prop in self.objectSchema.properties as [Property]! { - // find lists - if let nestedObject = self[prop.name] as? Object { - mutabledic.setValue(nestedObject.toDictionary(), forKey: prop.name) - } else if let nestedListObject = self[prop.name] as? ListBase { - var objects = [AnyObject]() - for index in 0.. NSDictionary { - let properties = self.objectSchema.properties.map { $0.name } - let dictionary = self.dictionaryWithValues(forKeys: properties) - let mutabledic = NSMutableDictionary() - mutabledic.setValuesForKeys(dictionary) - for prop in self.objectSchema.properties as [Property]! { - // find lists - if let nestedObject = self[prop.name] as? Object { - mutabledic.setValue(nestedObject.toDictionary(), forKey: prop.name) - } else if let nestedListObject = self[prop.name] as? ListBase { - var objects = [String:Bool]() - for index in 0.. NSDictionary { - let properties = self.objectSchema.properties.map { $0.name } - let dictionary = self.dictionaryWithValues(forKeys: properties) - let mutabledic = NSMutableDictionary() - mutabledic.setValuesForKeys(dictionary) - - for prop in self.objectSchema.properties as [Property]! { - // find lists - // Remove podcast description because it's too long to send - if prop.name == "podcastDesc" { - mutabledic.setValue("", forKey: prop.name) - continue - } - if let nestedObject = self[prop.name] as? Object { - mutabledic.setValue(nestedObject.toDictionary(), forKey: prop.name) - } else if let nestedListObject = self[prop.name] as? ListBase { - var objects = [AnyObject]() - for index in 0.. Bool { + return lhs._id == rhs._id && + lhs.date == rhs.date && + lhs.link == rhs.link && + lhs.categories ?? [] == rhs.categories ?? [] && + lhs.tags ?? [] == rhs.tags ?? [] && + lhs.mp3 == rhs.mp3 && + lhs.featuredImage == rhs.featuredImage && + lhs.content.rendered == rhs.content.rendered && + lhs.title.rendered == rhs.title.rendered && + lhs.score == rhs.score && + lhs.type == rhs.type + } +} + +extension Podcast { + func getLastUpdatedAsDateWith(completion: @escaping (Date?) -> Void) { + DispatchQueue.global().async { + // slow calculations performed here + let date = Date(iso8601String: self.date) + DispatchQueue.main.async { + completion(date) + } + } + } + + func getLastUpdatedAsDate() -> Date? { + return Date(iso8601String: self.date) + } +} + +// Extension to go Encodable -> Dictionary +extension Encodable { + var dictionary: [String: Any] { + return (try? JSONSerialization.jsonObject(with: JSONEncoder().encode(self))) as? [String: Any] ?? [:] + } + var nsDictionary: NSDictionary { + return dictionary as NSDictionary + } +} + diff --git a/SEDaily-IOS/PodcastCollectionViewCell.swift b/SEDaily-IOS/PodcastCollectionViewCell.swift index 70b3b93..cf84569 100644 --- a/SEDaily-IOS/PodcastCollectionViewCell.swift +++ b/SEDaily-IOS/PodcastCollectionViewCell.swift @@ -8,87 +8,28 @@ import UIKit import SnapKit -import RealmSwift -import Kingfisher import KTResponsiveUI import Skeleton - -class PodcastCollectionViewCell: UICollectionViewCell { - - var podcastModel: PodcastModel! - - let imageView = UIImageView() - let titleLabel = UILabel() - - override init(frame: CGRect) { - super.init(frame: frame) - - self.contentView.addSubview(imageView) - self.contentView.addSubview(titleLabel) - - self.contentView.backgroundColor = .white - contentView.layer.cornerRadius = 2.calculateWidth() - - contentView.layer.shadowColor = UIColor.lightGray.cgColor - contentView.layer.shadowOpacity = 0.75 - contentView.layer.shadowOffset = CGSize(width: 0, height: 1.calculateHeight()) - contentView.layer.shadowRadius = 2.calculateWidth() - - let topBottomInset = 5.0.calculateHeight() - let amountToSubtract = topBottomInset * 2 - - let twoThirds: CGFloat = (2.0/3.0) - - imageView.snp.makeConstraints{ (make) in - make.top.equalToSuperview().inset(topBottomInset) - make.left.right.equalToSuperview().inset(10.calculateWidth()) - make.height.equalTo(((self.height * twoThirds) - amountToSubtract)) - } - - imageView.contentMode = .scaleAspectFit - - let oneThird: CGFloat = (1.0/3.0) - - titleLabel.snp.makeConstraints{ (make) in - make.bottom.equalToSuperview().inset(topBottomInset) - make.left.right.equalToSuperview().inset(10.calculateWidth()) - make.height.equalTo(((self.height * oneThird) - amountToSubtract)) - } - - titleLabel.font = UIFont.systemFont(ofSize: 16.calculateWidth()) - titleLabel.adjustsFontSizeToFitWidth = false - titleLabel.lineBreakMode = .byTruncatingTail - titleLabel.minimumScaleFactor = 0.25 - titleLabel.numberOfLines = 0 - titleLabel.textAlignment = .center - titleLabel.textColor = Stylesheet.Colors.offBlack - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:)") - } - - func setupCell(model: PodcastModel) { - self.podcastModel = model - guard let name = model.podcastName else { return } - titleLabel.text = name - - guard let imageURLString = model.imageURLString else { - self.imageView.image = #imageLiteral(resourceName: "SEDaily_Logo") - return - } - if let url = URL(string: imageURLString) { - self.imageView.kf.indicatorType = .activity - self.imageView.kf.setImage(with: url) - } - } -} +import SDWebImage class PodcastCell: UICollectionViewCell { var imageView: UIImageView! var titleLabel: UILabel! var timeDayLabel: UILabel! + var viewModel: PodcastViewModel = PodcastViewModel() { + willSet { + guard newValue != self.viewModel else { return } + } + didSet { + self.titleLabel.text = viewModel.podcastTitle + viewModel.getLastUpdatedAsDateWith { (date) in + self.setupTimeDayLabel(timeLength: nil, date: date) + } + self.setupImageView(imageURL: viewModel.featuredImageURL) + } + } + override init(frame: CGRect) { super.init(frame: frame) let newContentView = UIView(width: 158, height: 250) @@ -114,31 +55,20 @@ class PodcastCell: UICollectionViewCell { fatalError("init(coder:)") } - func setupCell(imageURLString: String?, title: String?, timeLength: Int?, date: Date?) { - self.setupImageView(imageURLString: imageURLString) - titleLabel.text = title ?? "" - setupTimeDayLabel(timeLength: timeLength, date: date) - } - - func setupImageView(imageURLString: String?) { - guard let imageURLString = imageURLString else { + private func setupImageView(imageURL: URL?) { + guard let imageURL = imageURL else { self.imageView.image = #imageLiteral(resourceName: "SEDaily_Logo") return } - if let url = URL(string: imageURLString) { - self.imageView.kf.indicatorType = .activity - self.imageView.kf.setImage(with: url) - } + + imageView.sd_setShowActivityIndicatorView(true) + imageView.sd_setIndicatorStyle(.gray) + imageView.sd_setImage(with: imageURL) } - func setupTimeDayLabel(timeLength: Int?, date: Date?) { - let timeString = Helpers.createTimeString(time: (Float(timeLength ?? 0))) + private func setupTimeDayLabel(timeLength: Int?, date: Date?) { let dateString = date?.dateString() ?? "" - guard timeString != "0:00" else { - timeDayLabel.text = dateString - return - } - timeDayLabel.text = timeString + " \u{2022} " + dateString + timeDayLabel.text = dateString } // MARK: Skeleton diff --git a/SEDaily-IOS/PodcastDataSource.swift b/SEDaily-IOS/PodcastDataSource.swift new file mode 100644 index 0000000..dd2dc24 --- /dev/null +++ b/SEDaily-IOS/PodcastDataSource.swift @@ -0,0 +1,130 @@ +// +// PodcastDataSource.swift +// SEDaily-IOS +// +// Created by Craig Holliday on 10/21/17. +// Copyright © 2017 Koala Tea. All rights reserved. +// + +import Foundation +import Disk + +protocol DataSource { + associatedtype T + + func getAll(completion: @escaping ([T]?) -> Void) + func getById(id: String, completion: @escaping (T?) -> Void) + func insert(item: T) + func update(item: T) + func clean() + func deleteById(id: String) +} + +enum DiskKeys: String { + case PodcastFolder = "Podcasts" + + var folderPath: String { + return self.rawValue + "/" + self.rawValue + ".json" + } +} + +class PodcastDataSource: DataSource { + typealias T = Podcast + + func getAll(completion: @escaping ([T]?) -> Void) { + DispatchQueue.global(qos: .userInitiated).async { + let retrievedObjects = try? Disk.retrieve(DiskKeys.PodcastFolder.folderPath, from: .caches, as: [T].self) + DispatchQueue.main.async { + completion(retrievedObjects) + } + } + } + + func getAllWith(filterObject: FilterObject, completion: @escaping ([T]?) -> Void) { + self.getAll { (returnedData) in + DispatchQueue.global(qos: .userInitiated).async { + //@TODO: Guard + let filteredObjects = returnedData?.filter({ (podcast) -> Bool in + return podcast.tags!.contains(filterObject.tags) && + podcast.categories!.contains(filterObject.categories) && + podcast.type == filterObject.type + }) + + let dateString = filterObject.lastDate + if let passedDate = Date(iso8601String: dateString) { + //@TODO: Gaurd + let dateFilteredObjects = filteredObjects?.filter({ (podcast) -> Bool in + return podcast.getLastUpdatedAsDate()! < passedDate + }) + //@TODO: Gaurd + DispatchQueue.main.async { + completion(Array(dateFilteredObjects!.prefix(10))) + + } + return + } + DispatchQueue.main.async { + // Prefix = to max paging + completion(Array(filteredObjects!.prefix(10))) + + } + return + } + } + } + + func getById(id: String, completion: @escaping (T?) -> Void) { + self.getAll { (returnedData) in + let foundObject = returnedData?.filter({ (item) -> Bool in + return item._id == id + }).first + completion(foundObject) + } + + } + + func insert(item: T) { + //@TODO: When would this fail + DispatchQueue.global(qos: .userInitiated).async { + do { + try Disk.append(item, to: DiskKeys.PodcastFolder.folderPath, in: .caches) + } catch { + //@TODO: Handle errors? + // ... + } + } + } + + func insert(items: [T]) { + DispatchQueue.global(qos: .userInitiated).async { + do { + try Disk.append(items, to: DiskKeys.PodcastFolder.folderPath, in: .caches) + } catch { + //@TODO: Handle errors? + // ... + } + } + } + + func update(item: T) { + + } + + func clean() { + try? Disk.remove(DiskKeys.PodcastFolder.rawValue, from: .caches) + } + + func deleteById(id: String) { + + } + + //@TODO: We may need to check if items exist? + // func checkIfExists(item: Podcast) { + // self.getById(id: item._id) { (returnedItem) in + // if returnedItem != nil { + // log.info("not nil") + // } + // log.info("nil?") + // } + // } +} diff --git a/SEDaily-IOS/PodcastDescriptionView.swift b/SEDaily-IOS/PodcastDescriptionView.swift new file mode 100644 index 0000000..24e23ef --- /dev/null +++ b/SEDaily-IOS/PodcastDescriptionView.swift @@ -0,0 +1,48 @@ +// +// PodcastDescriptionView.swift +// SEDaily-IOS +// +// Created by Craig Holliday on 10/18/17. +// Copyright © 2017 Koala Tea. All rights reserved. +// + +import UIKit +import ActiveLabel +import KTResponsiveUI + +class PodcastDescriptionView: UIView { + + private lazy var label: ActiveLabel = { + return ActiveLabel(leftInset: 15, topInset: 15, width: 375 - 30, height: 600) + }() + + private let bottomMarginForLabel = UIView.getValueScaledByScreenHeightFor(baseValue: 30) + + override func performLayout() { + self.backgroundColor = Stylesheet.Colors.offWhite + + self.addSubview(label) + self.height = label.height + + label.numberOfLines = 0 + label.enabledTypes = [.url] + label.textColor = Stylesheet.Colors.offBlack + label.handleURLTap { url in + if #available(iOS 10.0, *) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + Tracker.logMovedToWebView(url: url.absoluteString) + } else { + UIApplication.shared.openURL(url) + Tracker.logMovedToWebView(url: url.absoluteString) + } + } + } + + func setupView(podcastModel: PodcastViewModel) { + podcastModel.getHTMLDecodedDescription { (returnedString) in + self.label.text = returnedString + self.label.sizeToFit() + self.height = self.label.height + self.bottomMarginForLabel + } + } +} diff --git a/SEDaily-IOS/PodcastDetailViewController.swift b/SEDaily-IOS/PodcastDetailViewController.swift new file mode 100644 index 0000000..9847bdc --- /dev/null +++ b/SEDaily-IOS/PodcastDetailViewController.swift @@ -0,0 +1,39 @@ +// +// PostDetailViewController.swift +// SEDaily-IOS +// +// Created by Craig Holliday on 10/18/17. +// Copyright © 2017 Koala Tea. All rights reserved. +// + +import UIKit + +class PodcastDetailViewController: UIViewController { + + var model = PodcastViewModel() + + lazy var scrollView: UIScrollView = { + return UIScrollView(frame: self.view.frame) + }() + + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = Stylesheet.Colors.base + self.view.addSubview(scrollView) + + let headerView = HeaderView(width: 375, height: 200) + headerView.setupHeader(model: model) + self.scrollView.addSubview(headerView) + + let view = PodcastDescriptionView(origin: headerView.bottomLeftPoint(),width: 375, height: 20) + view.setupView(podcastModel: model) + scrollView.addSubview(view) + view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, + constant: UIView.getValueScaledByScreenHeightFor(baseValue: -65)).isActive = true + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } +} diff --git a/SEDaily-IOS/PodcastModel.swift b/SEDaily-IOS/PodcastModel.swift deleted file mode 100644 index 13d3237..0000000 --- a/SEDaily-IOS/PodcastModel.swift +++ /dev/null @@ -1,206 +0,0 @@ -// -// PodcastModel.swift -// SEDaily-IOS -// -// Created by Craig Holliday on 6/27/17. -// Copyright © 2017 Koala Tea. All rights reserved. -// - - -import UIKit -import RealmSwift -import ObjectMapper -import SwifterSwift - -public class RealmString: Object { - @objc dynamic var stringValue = "" -} - -public class PodcastModel: Object, Mappable { - @objc dynamic var key: String? = nil - @objc dynamic var podcastId: String? = nil - @objc dynamic var podcastName: String? = nil - @objc dynamic var podcastDesc: String? = nil - @objc dynamic var uploadDate: String? = nil - @objc dynamic var mp3URL: String? = nil - @objc dynamic var link: String? = nil - @objc dynamic var type: String? = nil - @objc dynamic var score: String? = nil - @objc dynamic var currentTime: String = "0" - @objc dynamic var imageURLString: String? = nil - @objc dynamic var tags: String = "" - @objc dynamic var categories: String = "" - - @objc dynamic var mp3Saved: Bool = false - - @objc dynamic var isNew: Bool = false - @objc dynamic var isTop: Bool = false - @objc dynamic var isRecommended: Bool = false - - @objc dynamic var isUpvoted: Bool = false - @objc dynamic var isDownvoted: Bool = false - - override public static func primaryKey() -> String? { - return "key" - } - - //Impl. of Mappable protocol - required convenience public init?(map: Map) { - self.init() - } - - // Mappable - public func mapping(map: Map) { - key <- map["_id"] - podcastId <- map["_id"] - podcastName <- map["title.rendered"] - podcastDesc <- (map["content.rendered"], TransformOf(fromJSON: { $0.map { String($0).htmlDecoded } }, toJSON: { $0!.htmlDecoded })) - uploadDate <- map["date"] - mp3URL <- map["mp3"] - link <- map["link"] - score <- (map["score"], TransformOf(fromJSON: { $0.map { String($0) } }, toJSON: { Int($0!) })) - imageURLString <- map["featuredImage"] - isUpvoted <- map["upvoted"] - isDownvoted <- map["downvoted"] - - if let unwrappedTags = map.JSON["tags"] as? [Int] { - for intTag in unwrappedTags { - tags += "\(intTag)," - } - } - - if let unwrappedCategories = map.JSON["categories"] as? [Int] { - for intTag in unwrappedCategories { - categories += "\(intTag)," - } - } - } - - func getDescription() -> String { - guard let desc = podcastDesc else { return "" } - return desc - } - - func getMP3asURL() -> URL? { - guard let urlString = self.mp3URL else { - return nil - } - guard let url = URL(string: urlString) else { - return nil - } - return url - } - - func getSavedMP3URL() -> URL { - var documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - - // the name of the file here I kept is yourFileName with appended extension - documentsURL.appendPathComponent(self.podcastName! + "." + "mp3") - return documentsURL - } - - func getCurrentTime() -> Double? { - return Double(self.currentTime) - } -} - -extension PodcastModel { - func save() { - let realm = try! Realm() - try! realm.write { - realm.add(self, update: true) - } - } - - class func all() -> Results { - let realm = try! Realm() - return realm.objects(PodcastModel.self) - } - - class func filterByTag(tag: String) -> Results { - let realm = try! Realm() - return realm.objects(PodcastModel.self).filter ("tags contains '\(tag)'") - } - - class func getRecommended() -> Results { - return self.all().filter("isRecommended == true").sorted(byKeyPath: "uploadDate", ascending: false) - } - - class func getTop() -> Results { - return self.all().filter("isTop == true").sorted(byKeyPath: "score", ascending: false) - } - - func updateFrom(item: PodcastModel) { - guard self.podcastName == item.podcastName else { return } - let realm = try! Realm() - try! realm.write { - realm.add(item, update: true) - } - } - - func update(name: String) { - let realm = try! Realm() - try! realm.write { - self.podcastName = podcastName - } - } - - func update(currentTime: Float) { - log.info(currentTime) - let realm = try! Realm() - try! realm.write { - self.currentTime = String(currentTime) - } - } - - func update(mp3Saved: Bool) { - let realm = try! Realm() - try! realm.write { - self.mp3Saved = mp3Saved - } - } - - func update(isNew: Bool) { - let realm = try! Realm() - try! realm.write { - self.isNew = isNew - } - } - - func update(isTop: Bool) { - let realm = try! Realm() - try! realm.write { - self.isTop = isTop - } - } - - func update(isRecommended: Bool) { - let realm = try! Realm() - try! realm.write { - self.isRecommended = isRecommended - } - } - - func update(isUpvoted: Bool) { - let realm = try! Realm() - try! realm.write { - self.isUpvoted = isUpvoted - self.isDownvoted = false - } - } - - func update(isDownvoted: Bool) { - let realm = try! Realm() - try! realm.write { - self.isDownvoted = isDownvoted - self.isUpvoted = false - } - } - - func delete() { - let realm = try! Realm() - try! realm.write { - realm.delete(self) - } - } -} diff --git a/SEDaily-IOS/PodcastPageViewController.swift b/SEDaily-IOS/PodcastPageViewController.swift index d877a8e..8c202e2 100644 --- a/SEDaily-IOS/PodcastPageViewController.swift +++ b/SEDaily-IOS/PodcastPageViewController.swift @@ -14,9 +14,15 @@ class PodcastPageViewController: TabmanViewController, PageboyViewControllerData var viewControllers = [GeneralCollectionViewController]() var barItems = [TabmanBar.Item]() + var customTabBarItem: UITabBarItem! { + get { + return UITabBarItem(title: L10n.tabBarTitleLatest, image: #imageLiteral(resourceName: "mic_stand"), selectedImage: #imageLiteral(resourceName: "mic_stand_selected")) + } + } override func viewDidLoad() { super.viewDidLoad() + self.tabBarItem = customTabBarItem self.dataSource = self @@ -28,6 +34,9 @@ class PodcastPageViewController: TabmanViewController, PageboyViewControllerData self.bar.items = barItems self.reloadPages() + + // Set the tab bar controller first selected item here + self.tabBarController?.selectedIndex = 0 } func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int { @@ -46,49 +55,40 @@ class PodcastPageViewController: TabmanViewController, PageboyViewControllerData func loadViewControllers() { let layout = UICollectionViewLayout() - let child_1 = GeneralCollectionViewController(collectionViewLayout: layout, type: API.Types.new) - child_1.tabTitle = L10n.tabTitleAll - viewControllers.append(child_1) - - let child_2 = GeneralCollectionViewController(collectionViewLayout: layout, tagId: 1068, type: API.Types.new) - child_2.tabTitle = L10n.tabTitleBusinessAndPhilosophy - viewControllers.append(child_2) - - let child_3 = GeneralCollectionViewController(collectionViewLayout: layout, tagId: 1082, type: API.Types.new) - child_3.tabTitle = L10n.tabTitleBlockchain - viewControllers.append(child_3) - - let child_4 = GeneralCollectionViewController(collectionViewLayout: layout, tagId: 1079, type: API.Types.new) - child_4.tabTitle = L10n.tabTitleCloudEngineering - viewControllers.append(child_4) - - let child_5 = GeneralCollectionViewController(collectionViewLayout: layout, tagId: 1081, type: API.Types.new) - child_5.tabTitle = L10n.tabTitleData - viewControllers.append(child_5) - - let child_6 = GeneralCollectionViewController(collectionViewLayout: layout, tagId: 1084, type: API.Types.new) - child_6.tabTitle = L10n.tabTitleJavaScript - viewControllers.append(child_6) - - let child_7 = GeneralCollectionViewController(collectionViewLayout: layout, tagId: 1080, type: API.Types.new) - child_7.tabTitle = L10n.tabTitleMachineLearning - viewControllers.append(child_7) - - let child_8 = GeneralCollectionViewController(collectionViewLayout: layout, tagId: 1078, type: API.Types.new) - child_8.tabTitle = L10n.tabTitleOpenSource - viewControllers.append(child_8) - - let child_9 = GeneralCollectionViewController(collectionViewLayout: layout, tagId: 1083, type: API.Types.new) - child_9.tabTitle = L10n.tabTitleSecurity - viewControllers.append(child_9) - - let child_10 = GeneralCollectionViewController(collectionViewLayout: layout, tagId: 1085, type: API.Types.new) - child_10.tabTitle = L10n.tabTitleHackers - viewControllers.append(child_10) - - let child_11 = GeneralCollectionViewController(collectionViewLayout: layout, tagId: 1069, type: API.Types.new) - child_11.tabTitle = L10n.tabTitleGreatestHits - viewControllers.append(child_11) + viewControllers = [ + GeneralCollectionViewController(collectionViewLayout: layout, + tabTitle: PodcastCategoryIds.All.description), + GeneralCollectionViewController(collectionViewLayout: layout, + categories: [PodcastCategoryIds.Business_and_Philosophy], + tabTitle: PodcastCategoryIds.Business_and_Philosophy.description), + GeneralCollectionViewController(collectionViewLayout: layout, + categories: [PodcastCategoryIds.Blockchain], + tabTitle: PodcastCategoryIds.Blockchain.description), + GeneralCollectionViewController(collectionViewLayout: layout, + categories: [PodcastCategoryIds.Cloud_Engineering], + tabTitle: PodcastCategoryIds.Cloud_Engineering.description), + GeneralCollectionViewController(collectionViewLayout: layout, + categories: [PodcastCategoryIds.Data], + tabTitle: PodcastCategoryIds.Data.description), + GeneralCollectionViewController(collectionViewLayout: layout, + categories: [PodcastCategoryIds.JavaScript], + tabTitle: PodcastCategoryIds.JavaScript.description), + GeneralCollectionViewController(collectionViewLayout: layout, + categories: [PodcastCategoryIds.Machine_Learning], + tabTitle: PodcastCategoryIds.Machine_Learning.description), + GeneralCollectionViewController(collectionViewLayout: layout, + categories: [PodcastCategoryIds.Open_Source], + tabTitle: PodcastCategoryIds.Open_Source.description), + GeneralCollectionViewController(collectionViewLayout: layout, + categories: [PodcastCategoryIds.Security], + tabTitle: PodcastCategoryIds.Security.description), + GeneralCollectionViewController(collectionViewLayout: layout, + categories: [PodcastCategoryIds.Hackers], + tabTitle: PodcastCategoryIds.Hackers.description), + GeneralCollectionViewController(collectionViewLayout: layout, + categories: [PodcastCategoryIds.Greatest_Hits], + tabTitle: PodcastCategoryIds.Greatest_Hits.description) + ] viewControllers.forEach { (controller) in barItems.append(Item(title: controller.tabTitle)) diff --git a/SEDaily-IOS/PodcastRepository.swift b/SEDaily-IOS/PodcastRepository.swift new file mode 100644 index 0000000..1202447 --- /dev/null +++ b/SEDaily-IOS/PodcastRepository.swift @@ -0,0 +1,129 @@ +// +// PodcastRepository.swift +// SEDaily-IOS +// +// Created by Craig Holliday on 10/13/17. +// Copyright © 2017 Koala Tea. All rights reserved. +// + +import Foundation + +enum APICheckDates { + static let newFeedLastCheck = "newFeed" +} + +protocol RepositoryProtocol { + associatedtype DataModel + var lastReturnedDataArray: [DataModel] { get set } +} + +public class Repository: NSObject, RepositoryProtocol { + typealias DataModel = T + + internal var lastReturnedDataArray: [DataModel] = [] +} + +enum RepositoryError: Error { + case ErrorGettingFromAPI + case ErrorGettingFromRealm + case ReturnedDataEqualsLastData + case ReturnedDataIsZero +} + +class PodcastRepository: Repository { + typealias RepositorySuccessCallback = ([DataModel]) -> Void + typealias RepositoryErrorCallback = (RepositoryError) -> Void + + private let dataSource = PodcastDataSource() + + // MARK: Getters With Paging + let tag = "podcasts" + + var loading = false + + func getData(page: Int = 0, + filterObject: FilterObject, + onSucces: @escaping RepositorySuccessCallback, + onFailure: @escaping RepositoryErrorCallback) { + self.retrieveDataFromRealmOrAPI(filterObject: filterObject, onSucces: { (returnedData) in + onSucces(returnedData) + }) { (error) in + onFailure(error) + } + } + + // MARK: Disk and API data getter + private func retrieveDataFromRealmOrAPI(filterObject: FilterObject, + onSucces: @escaping RepositorySuccessCallback, + onFailure: @escaping RepositoryErrorCallback) { + // Check if we made requests today + let alreadLoadedStartToday = self.checkAlreadyLoadedNewToday(filterObject: filterObject) + //@TODO: Fix this special case for recommneded. We can't load from disk here because we are display top podcasts when a user is not logged in + if alreadLoadedStartToday && filterObject.type != PodcastTypes.recommended.rawValue { + self.loading = true + log.warning("from disk") + // Check if we have realm data saved + self.dataSource.getAllWith(filterObject: filterObject, completion: { (returnedData) in + guard let data = returnedData, !data.isEmpty else { + self.loading = false + onFailure(.ErrorGettingFromRealm) + return + } + guard data != self.lastReturnedDataArray else { + self.loading = false + onFailure(.ReturnedDataEqualsLastData) + return + } + + self.setLoadedNewToday(filterObject: filterObject) + self.lastReturnedDataArray = data + self.loading = false + onSucces(data) + }) + return + } + log.warning("from api") + guard self.loading == false else { return } + self.loading = true + + // API Call and return + API.sharedInstance.getPosts(type: filterObject.type, createdAtBefore: filterObject.lastDate, tags: filterObject.tagsAsString, categories: filterObject.categoriesAsString, onSucces: { (podcasts) in + self.loading = false + guard podcasts != self.lastReturnedDataArray else { + onFailure(.ReturnedDataEqualsLastData) + return + } + self.dataSource.insert(items: podcasts) + self.setLoadedNewToday(filterObject: filterObject) + self.lastReturnedDataArray = podcasts + onSucces(podcasts) + }) { (apiError) in + self.loading = false + onFailure(.ErrorGettingFromAPI) + } + } + + // MARK: Already loaded today checks + func checkAlreadyLoadedNewToday(filterObject: FilterObject) -> Bool { + let key = "\(APICheckDates.newFeedLastCheck)-\(filterObject.dictionary)" + + let defaults = UserDefaults.standard + if let newFeedLastCheck = defaults.string(forKey: key) { + let todayDate = Date().dateString() + let newFeedDate = Date(iso8601String: newFeedLastCheck)!.dateString() + if (newFeedDate == todayDate) { + return true + } + + return false + } + return false + } + + func setLoadedNewToday (filterObject: FilterObject) { + let todayString = Date().iso8601String + let key = "\(APICheckDates.newFeedLastCheck)-\(filterObject.dictionary)" + let defaults = UserDefaults.standard + defaults.set(todayString, forKey: key) + } +} diff --git a/SEDaily-IOS/PodcastTableViewCell.swift b/SEDaily-IOS/PodcastTableViewCell.swift index 46dc56d..7b27bcd 100644 --- a/SEDaily-IOS/PodcastTableViewCell.swift +++ b/SEDaily-IOS/PodcastTableViewCell.swift @@ -11,12 +11,25 @@ import Reusable import SnapKit import SwifterSwift import KTResponsiveUI -import Kingfisher +import SDWebImage class PodcastTableViewCell: UITableViewCell, Reusable { private var cellLabel: UILabel! private var cellImageView: UIImageView! + var viewModel: PodcastViewModel = PodcastViewModel() { + willSet { + guard newValue != self.viewModel else { return } + } + didSet { + self.cellLabel.text = viewModel.podcastTitle + + if let url = viewModel.featuredImageURL { + cellImageView.sd_setImage(with: url) + } + } + } + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) cellImageView = UIImageView(leftInset: 10, height: 75) @@ -36,14 +49,4 @@ class PodcastTableViewCell: UITableViewCell, Reusable { required init(coder aDecoder: NSCoder) { fatalError("init(coder:)") } - - func setupCell(title: String, imageURLString: String?) { - self.cellLabel.text = title - - guard let imageURLString = imageURLString else { return } - if let url = URL(string: imageURLString) { - self.cellImageView.kf.indicatorType = .activity - self.cellImageView.kf.setImage(with: url) - } - } } diff --git a/SEDaily-IOS/PodcastViewModel.swift b/SEDaily-IOS/PodcastViewModel.swift new file mode 100644 index 0000000..cfecaed --- /dev/null +++ b/SEDaily-IOS/PodcastViewModel.swift @@ -0,0 +1,121 @@ +// +// PodcastViewModel.swift +// SEDaily-IOS +// +// Created by Craig Holliday on 10/21/17. +// Copyright © 2017 Koala Tea. All rights reserved. +// + +import Foundation + +public struct PodcastViewModel: Codable { + let _id: String + let uploadDateiso8601: String + let postLinkURL: URL? + let categories: [Int]? + var categoriesAsString: String { + get { + guard let categories = self.categories else { return "" } + let stringArray = categories.map { String(describing: $0) } + return stringArray.joined(separator: " ") + } + } + let tags: [Int]? + var tagsAsString: String { + get { + guard let tags = self.tags else { return "" } + let stringArray = tags.map { String(describing: $0) } + return stringArray.joined(separator: " ") + } + } + let mp3URL: URL? + let featuredImageURL: URL? + private let encodedPodcastTitle: String + private let encodedPodcastDescription: String + var score: Int + var isUpvoted: Bool = false + var isDownvoted: Bool = false + + var podcastTitle: String { + get { + return encodedPodcastTitle.htmlDecoded + } + } + + init(podcast: Podcast) { + self._id = podcast._id + self.uploadDateiso8601 = podcast.date + self.postLinkURL = URL(string: podcast.link) + self.categories = podcast.categories + self.tags = podcast.tags + self.mp3URL = URL(string: podcast.mp3) + self.featuredImageURL = URL(string: podcast.featuredImage ?? "") + self.encodedPodcastTitle = podcast.title.rendered + self.encodedPodcastDescription = podcast.content.rendered + self.score = podcast.score + + if let upvoted = podcast.upvoted { + self.isUpvoted = upvoted + } + if let downvoted = podcast.downvoted { + self.isDownvoted = downvoted + } + } + + init() { + self._id = "" + self.uploadDateiso8601 = "" + self.postLinkURL = nil + self.categories = [] + self.tags = [] + self.mp3URL = nil + self.featuredImageURL = nil + self.encodedPodcastTitle = "" + self.encodedPodcastDescription = "" + self.score = 0 + } +} + +extension PodcastViewModel: Equatable { + public static func ==(lhs: PodcastViewModel, rhs: PodcastViewModel) -> Bool { + return lhs._id == rhs._id && + lhs.uploadDateiso8601 == rhs.uploadDateiso8601 && + lhs.postLinkURL == rhs.postLinkURL && + lhs.categories ?? [] == rhs.categories ?? [] && + lhs.tags ?? [] == rhs.tags ?? [] && + lhs.mp3URL == rhs.mp3URL && + lhs.featuredImageURL == rhs.featuredImageURL && + lhs.encodedPodcastTitle == rhs.encodedPodcastTitle && + lhs.encodedPodcastDescription == rhs.encodedPodcastDescription && + lhs.score == rhs.score + } +} + +extension PodcastViewModel { + func getLastUpdatedAsDateWith(completion: @escaping (Date?) -> Void) { + DispatchQueue.global().async { + // slow calculations performed here + let date = Date(iso8601String: self.uploadDateiso8601) + DispatchQueue.main.async { + completion(date) + } + } + } + + // This is too slow for a cell collection view call + func getLastUpdatedAsDate() -> Date? { + return Date(iso8601String: self.uploadDateiso8601) + } +} + +extension PodcastViewModel { + func getHTMLDecodedDescription(completion: @escaping (String) -> Void) { + DispatchQueue.global().async { + // slow calculations performed here + let decodedString = self.encodedPodcastDescription.htmlDecodedWithSomeEntities ?? "" + DispatchQueue.main.async { + completion(decodedString) + } + } + } +} diff --git a/SEDaily-IOS/PodcastViewModelController.swift b/SEDaily-IOS/PodcastViewModelController.swift new file mode 100644 index 0000000..d38db65 --- /dev/null +++ b/SEDaily-IOS/PodcastViewModelController.swift @@ -0,0 +1,126 @@ +// +// PodcastViewModelController.swift +// SEDaily-IOS +// +// Created by Craig Holliday on 10/12/17. +// Copyright © 2017 Koala Tea. All rights reserved. +// + +import Foundation + +enum APIError: Error { + case NoResponseDataError + case JSONParseError + case GeneralFailure +} + +public class PodcastViewModelController { + typealias Model = Podcast + typealias ViewModel = PodcastViewModel + typealias SuccessCallback = () -> Void + typealias ErrorCallback = (RepositoryError?) -> Void + + fileprivate let repository = PodcastRepository() + fileprivate var viewModels: [ViewModel?] = [] + + var viewModelsCount: Int { + return viewModels.count + } + + func viewModel(at index: Int) -> ViewModel? { + guard index >= 0 && index < viewModelsCount else { return nil } + return viewModels[index] + } + + func clearViewModels() { + self.viewModels.removeAll() + } + + func fetchData(type: String = "", + createdAtBefore beforeDate: String = "", + tags: [Int] = [], + categories: [Int] = [], + page: Int = 0, + clearData: Bool = false, + onSucces: @escaping SuccessCallback, + onFailure: @escaping ErrorCallback) { + if clearData { + self.clearViewModels() + } + let filterObject = FilterObject(type: type, tags: tags, lastDate: beforeDate, categories: categories) + repository.getData(filterObject: filterObject, onSucces: { (podcasts) in + let newViewModels: [ViewModel?] = podcasts.map { model in + return ViewModel(podcast: model) + } + guard !self.viewModels.isEmpty else { + self.viewModels.append(contentsOf: newViewModels) + onSucces() + return + } + + //@TODO: Do this in the background? + let filteredArray = newViewModels.filter { newPodcast in + let contains = self.viewModels.contains { currentPodcast in + return newPodcast == currentPodcast + } + return !contains + } + + guard filteredArray.count != 0 else { + // OnFailure Nothing to append + //@TODO: Change handle error + onFailure(.ReturnedDataIsZero) + return + } + + self.viewModels.append(contentsOf: filteredArray) + onSucces() + }) { (error) in + //@TODO: make this not api error + onFailure(error) + } + } + + func fetchSearchData(searchTerm: String, + createdAtBefore beforeDate: String = "", + firstSearch: Bool, + onSucces: @escaping SuccessCallback, + onFailure: @escaping (APIError?) -> Void) { + if firstSearch { + self.clearViewModels() + } + API.sharedInstance.getPostsWith(searchTerm: searchTerm, createdAtBefore: beforeDate, onSucces: { (podcasts) in + let newViewModels: [ViewModel?] = podcasts.map { model in + return ViewModel(podcast: model) + } + + guard !self.viewModels.isEmpty else { + self.viewModels.append(contentsOf: newViewModels) + onSucces() + return + } + + //@TODO: Do this in the background? + let filteredArray = newViewModels.filter { newPodcast in + let contains = self.viewModels.contains { currentPodcast in + return newPodcast == currentPodcast + } + return !contains + } + + guard filteredArray.count != 0 else { + // OnFailure Nothing to append + //@TODO: Change handle error + onFailure(.GeneralFailure) + return + } + + self.viewModels.append(contentsOf: filteredArray) + onSucces() + }) { (apiError) in + //@TODO: handle error + log.error(apiError?.localizedDescription) + onFailure(apiError) + } + } +} diff --git a/SEDaily-IOS/PostDetailTableViewController.swift b/SEDaily-IOS/PostDetailTableViewController.swift deleted file mode 100644 index fbad468..0000000 --- a/SEDaily-IOS/PostDetailTableViewController.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// PostDetailTableViewController.swift -// SEDaily-IOS -// -// Created by Craig Holliday on 6/28/17. -// Copyright © 2017 Koala Tea. All rights reserved. -// - -import UIKit -import Reusable - -class PostDetailTableViewController: UITableViewController { - - var model: PodcastModel! - - override func viewDidLoad() { - super.viewDidLoad() - - self.tableView.register(cellType: SingleLabelTableViewCell.self) - - tableView.allowsSelection = false - tableView.alwaysBounceVertical = false - tableView.estimatedRowHeight = 20 - tableView.rowHeight = UITableViewAutomaticDimension - tableView.separatorStyle = .none - tableView.showsHorizontalScrollIndicator = false - tableView.tableFooterView = UIView() - tableView.showsVerticalScrollIndicator = false - tableView.showsHorizontalScrollIndicator = false - - let headerView = HeaderView(frame: CGRect(x: 0, y: 0, width: tableView.width, height: 200.calculateHeight())) - headerView.setupHeader(model: model) - tableView.tableHeaderView = headerView - - self.tableView.backgroundColor = Stylesheet.Colors.base - setupTitleView() - } - - func setupTitleView() { - guard let navigationBarHeight = self.navigationController?.navigationBar.height else { return } - let height = navigationBarHeight - (4.calculateHeight()) - let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: height, height: height)) - imageView.contentMode = .scaleAspectFit - imageView.image = #imageLiteral(resourceName: "Logo_BarButton") - self.navigationItem.titleView = imageView - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - - // MARK: - Table view data source - - override func numberOfSections(in tableView: UITableView) -> Int { - // #warning Incomplete implementation, return the number of sections - return 1 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - // #warning Incomplete implementation, return the number of rows - return 1 - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(for: indexPath) as SingleLabelTableViewCell - - // Configure the cell... - cell.setupCell(model: model) - - return cell - } - - override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - - if cell.responds(to: #selector(setter: UITableViewCell.separatorInset)) { - cell.separatorInset = .zero - } - - if cell.responds(to: #selector(setter: UIView.preservesSuperviewLayoutMargins)) { - cell.preservesSuperviewLayoutMargins = false - } - - if cell.responds(to: #selector(setter: UIView.layoutMargins)) { - cell.layoutMargins = .zero - } - // cell.isSelected = (indexPath as NSIndexPath).row == selectedRowIndex - } -} diff --git a/SEDaily-IOS/SearchTableViewController.swift b/SEDaily-IOS/SearchTableViewController.swift index faf0605..895e76d 100644 --- a/SEDaily-IOS/SearchTableViewController.swift +++ b/SEDaily-IOS/SearchTableViewController.swift @@ -13,6 +13,9 @@ private let reuseIdentifier = "reuseIdentifier" class SearchTableViewController: UITableViewController { + // ViewModelController + private let podcastViewModelController = PodcastViewModelController() + lazy var footerView: UIActivityIndicatorView = { let footerView = UIActivityIndicatorView(height: 44) footerView.width = self.tableView.width @@ -20,13 +23,11 @@ class SearchTableViewController: UITableViewController { return footerView }() - var lastData = [PodcastModel]() - var filteredData = [PodcastModel]() - // MARK: - Paging let pageSize = 10 let preloadMargin = 5 var lastLoadedPage = 0 + var loading = false let searchController = UISearchController(searchResultsController: nil) var searchText: String { @@ -53,7 +54,6 @@ class SearchTableViewController: UITableViewController { searchController.searchBar.delegate = self searchController.searchBar.tintColor = Stylesheet.Colors.base - searchController.searchResultsUpdater = self searchController.dimsBackgroundDuringPresentation = false searchController.hidesNavigationBarDuringPresentation = false // definesPresentationContext = true @@ -89,64 +89,58 @@ class SearchTableViewController: UITableViewController { } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return filteredData.count + return podcastViewModelController.viewModelsCount } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! PodcastTableViewCell - - let item = filteredData[indexPath.row] - // Configure the cell... - cell.setupCell(title: item.podcastName ?? "", imageURLString: item.imageURLString ?? nil) - - checkPage(indexPath: indexPath, item: item) + + if let viewModel = podcastViewModelController.viewModel(at: indexPath.row) { + cell.viewModel = viewModel + if let lastIndexPath = self.tableView?.indexPathForLastRow { + if let lastItem = podcastViewModelController.viewModel(at: lastIndexPath.row) { + self.checkPage(currentIndexPath: indexPath, + lastIndexPath: lastIndexPath, + lastIdentifier: lastItem.uploadDateiso8601) + } + } + } return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let item = filteredData[indexPath.row] - let vc = PostDetailTableViewController() - vc.model = item - self.navigationController?.pushViewController(vc, animated: true) + if let viewModel = podcastViewModelController.viewModel(at: indexPath.row) { + let vc = PodcastDetailViewController() + vc.model = viewModel + self.navigationController?.pushViewController(vc, animated: true) + } } } extension SearchTableViewController { // MARK: Data/Paging - func checkPage(indexPath: IndexPath, item: PodcastModel) { - let nextPage: Int = Int(indexPath.item / pageSize) + 1 - let preloadIndex = nextPage * pageSize - preloadMargin + func checkPage(currentIndexPath: IndexPath, lastIndexPath: IndexPath, lastIdentifier: String) { + let nextPage: Int = Int(currentIndexPath.item / self.pageSize) + 1 + let preloadIndex = nextPage * self.pageSize - self.preloadMargin - if (indexPath.item >= preloadIndex && lastLoadedPage < nextPage) || indexPath == tableView?.indexPathForLastRow! { - if let lastDate = item.uploadDate { - guard !self.isLoading else { return } - self.isLoading = true - getData(page: nextPage, lastItemDate: lastDate) - } + if (currentIndexPath.item >= preloadIndex && self.lastLoadedPage < nextPage) || currentIndexPath == lastIndexPath { + self.getData(lastIdentifier: lastIdentifier, nextPage: nextPage, firstSearch: false) } } - func getData(page: Int = 0, lastItemDate: String = "") { - lastLoadedPage = page - loadData(lastItemDate: lastItemDate) - } - - func loadData(lastItemDate: String) { - API.sharedInstance.getPostsWith(searchTerm: searchText.lowercased(), createdAtBefore: lastItemDate) { (podcastArray) in - self.isLoading = false - //@TODO: add podcastArray?.count is 0 check - if let podcastArray = podcastArray { - for item in podcastArray { - let existingObject = self.filteredData.filter { $0.podcastName! == item.podcastName! }.first - guard existingObject == nil else { continue } - self.filteredData.append(item) - } - // Guard for new data - guard self.lastData != podcastArray else { return } - self.lastData = podcastArray + func getData(lastIdentifier: String, nextPage: Int, firstSearch: Bool) { + guard self.loading == false else { return } + self.loading = true + podcastViewModelController.fetchSearchData(searchTerm: self.searchText.lowercased(), createdAtBefore: lastIdentifier, firstSearch: firstSearch, onSucces: { + self.loading = false + self.lastLoadedPage = nextPage + DispatchQueue.main.async { self.tableView.reloadData() } + }) { (apiError) in + self.loading = false + log.error(apiError) } } } @@ -156,9 +150,7 @@ extension SearchTableViewController { func filterContentForSearchText(_ searchText: String) { guard !searchBarIsEmpty() else { return } - - self.isLoading = true - self.loadData(lastItemDate: "") + self.getData(lastIdentifier: "", nextPage: 0, firstSearch: true) } func searchBarIsEmpty() -> Bool { @@ -174,17 +166,6 @@ extension SearchTableViewController { extension SearchTableViewController: UISearchBarDelegate { // MARK: - UISearchBar Delegate func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { - if !filteredData.isEmpty { - filteredData = [PodcastModel]() - } filterContentForSearchText(searchController.searchBar.text!) } } - -extension SearchTableViewController: UISearchResultsUpdating { - // MARK: - UISearchResultsUpdating Delegate - func updateSearchResults(for searchController: UISearchController) { - - } -} - diff --git a/SEDaily-IOS/SingleLabelTableViewCell.swift b/SEDaily-IOS/SingleLabelTableViewCell.swift deleted file mode 100644 index bda4855..0000000 --- a/SEDaily-IOS/SingleLabelTableViewCell.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// SingleLabelTableViewCell.swift -// SEDaily-IOS -// -// Created by Craig Holliday on 6/28/17. -// Copyright © 2017 Koala Tea. All rights reserved. -// - -import UIKit -import Reusable -import SnapKit -import SwifterSwift -import ActiveLabel - -class SingleLabelTableViewCell: UITableViewCell, Reusable { - let label = ActiveLabel() - - override init(style: UITableViewCellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - self.contentView.addSubview(label) - - self.contentView.snp.makeConstraints { (make) in - make.top.left.right.equalToSuperview() - make.bottom.equalToSuperview().priority(99) - make.bottom.equalTo(label).priority(100) - } - - self.label.snp.makeConstraints { (make) in - make.top.equalToSuperview().inset(15.calculateHeight()) - make.left.right.equalToSuperview().inset(15.calculateWidth()) - } - - self.backgroundColor = Stylesheet.Colors.offWhite -// Stylesheet.applyOn(self) - - label.numberOfLines = 0 - label.enabledTypes = [.url] - label.textColor = Stylesheet.Colors.offBlack - label.handleURLTap { url in - if #available(iOS 10.0, *) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - Tracker.logMovedToWebView(url: url.absoluteString) - } else { - UIApplication.shared.openURL(url) - Tracker.logMovedToWebView(url: url.absoluteString) - } - } - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:)") - } - - func setupCell(model: PodcastModel) { - label.text = model.getDescription() - } -} diff --git a/SEDaily-IOS/SkeletonCollectionView.swift b/SEDaily-IOS/SkeletonCollectionView.swift index 83909f3..2999347 100644 --- a/SEDaily-IOS/SkeletonCollectionView.swift +++ b/SEDaily-IOS/SkeletonCollectionView.swift @@ -21,11 +21,11 @@ class SkeletonCollectionView: UIView, UICollectionViewDataSource { self.collectionView!.register(PodcastCell.self, forCellWithReuseIdentifier: reuseIdentifier) - let layout = KoalaTeaFlowLayout(cellWidth: 158, - cellHeight: 250, - topBottomMargin: 12, - leftRightMargin: 20, - cellSpacing: 8) + let layout = KoalaTeaFlowLayout(cellWidth: UIView.getValueScaledByScreenWidthFor(baseValue: 158), + cellHeight: UIView.getValueScaledByScreenHeightFor(baseValue: 250), + topBottomMargin: UIView.getValueScaledByScreenHeightFor(baseValue: 12), + leftRightMargin: UIView.getValueScaledByScreenWidthFor(baseValue: 20), + cellSpacing: UIView.getValueScaledByScreenWidthFor(baseValue: 8)) self.collectionView?.collectionViewLayout = layout self.collectionView?.backgroundColor = .white diff --git a/SEDaily-IOS/TempObjectMapper.swift b/SEDaily-IOS/TempObjectMapper.swift deleted file mode 100644 index 8edd19b..0000000 --- a/SEDaily-IOS/TempObjectMapper.swift +++ /dev/null @@ -1,214 +0,0 @@ -// -// TEMPALOBJECTMAPPER.swift -// Kibbl-IOS -// -// Created by Craig Holliday on 9/20/17. -// Copyright © 2017 Koala Tea. All rights reserved. -// - -// -// Request.swift -// AlamofireObjectMapper -// -// Created by Tristan Himmelman on 2015-04-30. -// -// The MIT License (MIT) -// -// Copyright (c) 2014-2015 Tristan Himmelman -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import Alamofire -import ObjectMapper - -extension DataRequest { - - enum ErrorCode: Int { - case noData = 1 - case dataSerializationFailed = 2 - } - - internal static func newError(_ code: ErrorCode, failureReason: String) -> NSError { - let errorDomain = "com.alamofireobjectmapper.error" - - let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason] - let returnError = NSError(domain: errorDomain, code: code.rawValue, userInfo: userInfo) - - return returnError - } - - /// Utility function for checking for errors in response - internal static func checkResponseForError(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) -> Error? { - if let error = error { - return error - } - guard let _ = data else { - let failureReason = "Data could not be serialized. Input data was nil." - let error = newError(.noData, failureReason: failureReason) - return error - } - return nil - } - - /// Utility function for extracting JSON from response - internal static func processResponse(request: URLRequest?, response: HTTPURLResponse?, data: Data?, keyPath: String?) -> Any? { - let jsonResponseSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments) - let result = jsonResponseSerializer.serializeResponse(request, response, data, nil) - - let JSON: Any? - if let keyPath = keyPath , keyPath.isEmpty == false { - JSON = (result.value as AnyObject?)?.value(forKeyPath: keyPath) - } else { - JSON = result.value - } - - return JSON - } - - /// BaseMappable Object Serializer - public static func ObjectMapperSerializer(_ keyPath: String?, mapToObject object: T? = nil, context: MapContext? = nil) -> DataResponseSerializer { - return DataResponseSerializer { request, response, data, error in - if let error = checkResponseForError(request: request, response: response, data: data, error: error){ - return .failure(error) - } - - let JSONObject = processResponse(request: request, response: response, data: data, keyPath: keyPath) - - if let object = object { - _ = Mapper(context: context, shouldIncludeNilValues: false).map(JSONObject: JSONObject, toObject: object) - return .success(object) - } else if let parsedObject = Mapper(context: context, shouldIncludeNilValues: false).map(JSONObject: JSONObject){ - return .success(parsedObject) - } - - let failureReason = "ObjectMapper failed to serialize response." - let error = newError(.dataSerializationFailed, failureReason: failureReason) - return .failure(error) - } - } - - /// ImmutableMappable Array Serializer - public static func ObjectMapperImmutableSerializer(_ keyPath: String?, context: MapContext? = nil) -> DataResponseSerializer { - return DataResponseSerializer { request, response, data, error in - if let error = checkResponseForError(request: request, response: response, data: data, error: error){ - return .failure(error) - } - - let JSONObject = processResponse(request: request, response: response, data: data, keyPath: keyPath) - - if let JSONObject = JSONObject, - let parsedObject = (try? Mapper(context: context, shouldIncludeNilValues: false).map(JSONObject: JSONObject)){ - return .success(parsedObject) - } - - let failureReason = "ObjectMapper failed to serialize response." - let error = newError(.dataSerializationFailed, failureReason: failureReason) - return .failure(error) - } - } - - /** - Adds a handler to be called once the request has finished. - - - parameter queue: The queue on which the completion handler is dispatched. - - parameter keyPath: The key path where object mapping should be performed - - parameter object: An object to perform the mapping on to - - parameter completionHandler: A closure to be executed once the request has finished and the data has been mapped by ObjectMapper. - - - returns: The request. - */ - @discardableResult - public func responseObject(queue: DispatchQueue? = nil, keyPath: String? = nil, mapToObject object: T? = nil, context: MapContext? = nil, completionHandler: @escaping (DataResponse) -> Void) -> Self { - return response(queue: queue, responseSerializer: DataRequest.ObjectMapperSerializer(keyPath, mapToObject: object, context: context), completionHandler: completionHandler) - } - - @discardableResult - public func responseObject(queue: DispatchQueue? = nil, keyPath: String? = nil, mapToObject object: T? = nil, context: MapContext? = nil, completionHandler: @escaping (DataResponse) -> Void) -> Self { - return response(queue: queue, responseSerializer: DataRequest.ObjectMapperImmutableSerializer(keyPath, context: context), completionHandler: completionHandler) - } - - /// BaseMappable Array Serializer - public static func ObjectMapperArraySerializer(_ keyPath: String?, context: MapContext? = nil) -> DataResponseSerializer<[T]> { - return DataResponseSerializer { request, response, data, error in - if let error = checkResponseForError(request: request, response: response, data: data, error: error){ - return .failure(error) - } - - let JSONObject = processResponse(request: request, response: response, data: data, keyPath: keyPath) - - if let parsedObject = Mapper(context: context, shouldIncludeNilValues: false).mapArray(JSONObject: JSONObject){ - return .success(parsedObject) - } - - let failureReason = "ObjectMapper failed to serialize response." - let error = newError(.dataSerializationFailed, failureReason: failureReason) - return .failure(error) - } - } - - /// ImmutableMappable Array Serializer - public static func ObjectMapperImmutableArraySerializer(_ keyPath: String?, context: MapContext? = nil) -> DataResponseSerializer<[T]> { - return DataResponseSerializer { request, response, data, error in - if let error = checkResponseForError(request: request, response: response, data: data, error: error){ - return .failure(error) - } - - if let JSONObject = processResponse(request: request, response: response, data: data, keyPath: keyPath){ - - if let parsedObject = try? Mapper(context: context, shouldIncludeNilValues: false).mapArray(JSONObject: JSONObject){ - return .success(parsedObject) - } - } - - let failureReason = "ObjectMapper failed to serialize response." - let error = newError(.dataSerializationFailed, failureReason: failureReason) - return .failure(error) - } - } - - /** - Adds a handler to be called once the request has finished. T: BaseMappable - - - parameter queue: The queue on which the completion handler is dispatched. - - parameter keyPath: The key path where object mapping should be performed - - parameter completionHandler: A closure to be executed once the request has finished and the data has been mapped by ObjectMapper. - - - returns: The request. - */ - @discardableResult - public func responseArray(queue: DispatchQueue? = nil, keyPath: String? = nil, context: MapContext? = nil, completionHandler: @escaping (DataResponse<[T]>) -> Void) -> Self { - return response(queue: queue, responseSerializer: DataRequest.ObjectMapperArraySerializer(keyPath, context: context), completionHandler: completionHandler) - } - - /** - Adds a handler to be called once the request has finished. T: ImmutableMappable - - - parameter queue: The queue on which the completion handler is dispatched. - - parameter keyPath: The key path where object mapping should be performed - - parameter completionHandler: A closure to be executed once the request has finished and the data has been mapped by ObjectMapper. - - - returns: The request. - */ - @discardableResult - public func responseArray(queue: DispatchQueue? = nil, keyPath: String? = nil, context: MapContext? = nil, completionHandler: @escaping (DataResponse<[T]>) -> Void) -> Self { - return response(queue: queue, responseSerializer: DataRequest.ObjectMapperImmutableArraySerializer(keyPath, context: context), completionHandler: completionHandler) - } -} - diff --git a/SEDaily-IOS/TopCollectionViewController.swift b/SEDaily-IOS/TopCollectionViewController.swift deleted file mode 100644 index 566ac42..0000000 --- a/SEDaily-IOS/TopCollectionViewController.swift +++ /dev/null @@ -1,149 +0,0 @@ -// -// TopCollectionViewController.swift -// SEDaily-IOS -// -// Created by Craig Holliday on 7/25/17. -// Copyright © 2017 Koala Tea. All rights reserved. -// - -import UIKit -import RealmSwift -import KoalaTeaFlowLayout - -private let reuseIdentifier = "Cell" - -class TopCollectionViewController: UICollectionViewController { - var skeletonCollectionView = SkeletonCollectionView(frame: .zero) - - var token: NotificationToken? - var data: Results = { - let data = PodcastModel.getTop() - - return data - }() - - var itemCount = 0 - - override func viewDidLoad() { - super.viewDidLoad() - - // Uncomment the following line to preserve selection between presentations - // self.clearsSelectionOnViewWillAppear = false - - // Register cell classes - self.collectionView!.register(PodcastCell.self, forCellWithReuseIdentifier: reuseIdentifier) - - self.collectionView?.backgroundColor = UIColor(hex: 0xfafafa) - self.collectionView?.showsHorizontalScrollIndicator = false - self.collectionView?.showsVerticalScrollIndicator = false - - let layout = KoalaTeaFlowLayout(cellWidth: 158, - cellHeight: 250, - topBottomMargin: 12, - leftRightMargin: 20, - cellSpacing: 8) - self.collectionView?.collectionViewLayout = layout - - // User Login observer - NotificationCenter.default.addObserver(self, selector: #selector(self.loginObserver), name: .loginChanged, object: nil) - - self.skeletonCollectionView = SkeletonCollectionView(frame: collectionView!.frame) - self.collectionView?.addSubview(skeletonCollectionView) - - loadData() - } - - @objc func loginObserver() { - loadData() - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - - override func viewDidAppear(_ animated: Bool) { - // loadData() - } - - func loadData() { - API.sharedInstance.getPosts(type: API.Types.top, completion: {_ in - - }) - self.registerNotifications() - } -} - - -extension TopCollectionViewController { - // MARK: UICollectionViewDataSource - - override func numberOfSections(in collectionView: UICollectionView) -> Int { - // #warning Incomplete implementation, return the number of sections - return 1 - } - - - override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - if itemCount > 0 { - self.skeletonCollectionView.fadeOut(duration: 0.5, completion: nil) - } - return itemCount - } - - override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! PodcastCell - - let item = data[indexPath.row] - - // Configure the cell - let uploadDate = Date(iso8601String: (item.uploadDate ?? "")) - cell.setupCell(imageURLString: item.imageURLString, title: item.podcastName!, timeLength: nil, date: uploadDate) - - return cell - } - - override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let item = data[indexPath.row] - let vc = PostDetailTableViewController() - vc.model = item - self.navigationController?.pushViewController(vc, animated: true) - } -} - -extension TopCollectionViewController { - // MARK: Realm - func registerNotifications() { - token = data.addNotificationBlock {[weak self] (changes: RealmCollectionChange) in - guard let collectionView = self?.collectionView else { return } - - switch changes { - case .initial: - guard let int = self?.data.count else { return } - self?.itemCount = int - collectionView.reloadData() - break - case .update(_, let deletions, let insertions, let modifications): - let deleteIndexPaths = deletions.map { IndexPath(item: $0, section: 0) } - let insertIndexPaths = insertions.map { IndexPath(item: $0, section: 0) } - let updateIndexPaths = modifications.map { IndexPath(item: $0, section: 0) } - - self?.collectionView?.performBatchUpdates({ - self?.collectionView?.deleteItems(at: deleteIndexPaths) - if !deleteIndexPaths.isEmpty { - self?.itemCount -= 1 - } - self?.collectionView?.insertItems(at: insertIndexPaths) - if !insertIndexPaths.isEmpty { - self?.itemCount += 1 - } - self?.collectionView?.reloadItems(at: updateIndexPaths) - }, completion: nil) - break - case .error(let error): - print(error) - break - } - } - } -} diff --git a/SEDaily-IOS/UserModel.swift b/SEDaily-IOS/UserModel.swift index 8d946e6..89ed805 100644 --- a/SEDaily-IOS/UserModel.swift +++ b/SEDaily-IOS/UserModel.swift @@ -7,122 +7,125 @@ // import UIKit -import RealmSwift -import ObjectMapper import SwifterSwift -public class User: Object, Mappable { - @objc dynamic var key = 1 - @objc dynamic var firstName: String? = nil - @objc dynamic var lastName: String? = nil - @objc dynamic var email: String? = nil - @objc dynamic var token: String? = nil - @objc dynamic var pushNotificationsSetting: Bool = false - @objc dynamic var deviceToken: String = "" +public struct User: Codable { + let key: Int = 1 + let firstName: String + let lastName: String + let email: String + let token: String + let pushNotificationsSetting: Bool = false + let deviceToken: String? = nil - override public static func primaryKey() -> String? { - return "key" + init(firstName: String, + lastName: String, + email: String, + token: String) { + self.firstName = firstName + self.lastName = lastName + self.email = email + self.token = token } - //Impl. of Mappable protocol - required convenience public init?(map: Map) { - self.init() - } - - // Mappable - public func mapping(map: Map) { - key <- map["id"] - firstName <- map["firstName"] - lastName <- map["lastName"] - email <- map["email"] - token <- map["token"] + init() { + self.firstName = "" + self.lastName = "" + self.email = "" + self.token = "" } // Mark: Getters - func getFullName() -> String? { - return self.firstName! + self.lastName! + func getFullName() -> String { + return self.firstName + self.lastName } func isLoggedIn() -> Bool { - if token != nil && token != "" { + if token != "" { return true } return false } } -extension User { - func save() { - let realm = try? Realm() - try! realm?.write { - realm?.add(self, update: true) - } +extension User: Equatable { + public static func ==(lhs: User, rhs: User) -> Bool { + return lhs.key == rhs.key && + lhs.firstName == rhs.firstName && + lhs.lastName == rhs.lastName && + lhs.email == rhs.email && + lhs.token == rhs.token && + lhs.pushNotificationsSetting == rhs.pushNotificationsSetting && + lhs.deviceToken == rhs.deviceToken } +} + +public class UserManager { + static let sharedInstance: UserManager = UserManager() + private init() {} + + let defaults = UserDefaults.standard + + let staticUserKey = "user" - // Probably rename this to something more understandable - class func checkAndAlert() -> Bool { - let realm = try! Realm() - let user = realm.objects(User.self).first - - if user?.token == nil || user?.token == "" { - Helpers.alertWithMessage(title: Helpers.Alerts.error, message: Helpers.Messages.youMustLogin, completionHandler: nil) - return false + var currentUser: User = User() { + didSet { + self.saveUserToDefaults(user: self.currentUser) } - return true } - class func getActiveUser() -> User { - let realm = try? Realm() - guard let user = realm?.objects(User.self).first else { - User.createDefault() - return (realm?.objects(User.self).first!)! + public func getActiveUser() -> User { + switch checkIfSavedUserEqualsCurrentUser() { + case true: + return self.currentUser + case false: + if let retrievedUser = self.retriveCurrentUserFromDefaults() { + if self.currentUser == User() && retrievedUser != User() { + self.setCurrentUser(to: retrievedUser) + return retrievedUser + } + } + self.saveUserToDefaults(user: self.currentUser) + return self.currentUser } - return user } - class func all() -> Results { - let realm = try! Realm() - return realm.objects(User.self) + public func setCurrentUser(to newUser: User) { + guard currentUser != newUser else { return } + self.currentUser = newUser } - class func logout() { - log.error("loggin out") - let user = User() - user.firstName = "" - user.lastName = "" - user.email = "" - user.token = "" - user.save() - NotificationCenter.default.post(name: .loginChanged, object: nil) + public func isCurrentUserLoggedIn() -> Bool { + let token = self.currentUser.token + guard token != "" else { return false } + return true } - class func createDefault() { - log.error("creating default") - let user = User() - user.email = "" - user.token = "" - user.save() + public func logoutUser() { + self.setCurrentUser(to: User()) + NotificationCenter.default.post(name: .loginChanged, object: nil) } - func delete() { - let realm = try! Realm() - try! realm.write { - realm.delete(self) - } + private func checkIfSavedUserEqualsCurrentUser() -> Bool { + guard let retrievedUser = self.retriveCurrentUserFromDefaults() else { return false } + guard retrievedUser == self.currentUser else { return false } + return true } - func update(deviceToken: String) { - let realm = try! Realm() - try! realm.write { - self.deviceToken = deviceToken + private func saveUserToDefaults(user: User) { + let encoder = JSONEncoder() + if let encoded = try? encoder.encode(user) { + defaults.set(encoded, forKey: staticUserKey) } } - func update(pushNotificationsSetting: Bool) { - let realm = try! Realm() - try! realm.write { - self.pushNotificationsSetting = pushNotificationsSetting + private func retriveCurrentUserFromDefaults() -> User? { + let decoder = JSONDecoder() + if let returnedEncodedUser = defaults.data(forKey: staticUserKey), + let user = try? decoder.decode(User.self, from: returnedEncodedUser) { + return user } + return nil } } diff --git a/SEDaily-IOS/fr.lproj/Localizable.strings b/SEDaily-IOS/fr.lproj/Localizable.strings index fdf47f5..c93a34e 100644 --- a/SEDaily-IOS/fr.lproj/Localizable.strings +++ b/SEDaily-IOS/fr.lproj/Localizable.strings @@ -48,4 +48,4 @@ "GenericError" = "Error*"; "GenericOkay" = "Okay*"; "GenericOK" = "OK*"; - +"Play" = "Play*"; diff --git a/SEDaily-IOSTests/Info.plist b/SEDaily-IOSTests/Info.plist index c711c24..957fa26 100644 --- a/SEDaily-IOSTests/Info.plist +++ b/SEDaily-IOSTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 6 + 12 diff --git a/SEDaily-IOSTests/SEDaily_IOSTests.swift b/SEDaily-IOSTests/SEDaily_IOSTests.swift index 304b78d..cfac8b5 100644 --- a/SEDaily-IOSTests/SEDaily_IOSTests.swift +++ b/SEDaily-IOSTests/SEDaily_IOSTests.swift @@ -7,6 +7,8 @@ // import XCTest +import SwiftSoup +import Atributika @testable import SEDaily_IOS class SEDaily_IOSTests: XCTestCase { diff --git a/fastlane/Fastfile b/fastlane/Fastfile index e62f134..67b6c6d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -31,7 +31,7 @@ platform :ios do desc "This will also make sure the profile is up to date" lane :beta do # match(type: "appstore") # more information: https://codesigning.guide - + increment_build_number gym(scheme: "SEDaily-IOS") # Build your app - more options available pilot diff --git a/fastlane/README.md b/fastlane/README.md index 51da9d2..b402dfb 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -14,7 +14,7 @@ xcode-select --install Homebrew Installer Script -Rubygems +RubyGems macOS