├── .github └── workflows │ ├── CD.yml │ └── CI.yml ├── .gitignore ├── MVVM.podspec ├── MVVM ├── Assets │ └── .gitkeep └── Classes │ ├── Animators │ ├── Animator.swift │ ├── AnimatorDelegate.swift │ └── AnimatorTypes.swift │ ├── Core │ ├── Base │ │ ├── BasePages │ │ │ ├── BaseCollectionPage.swift │ │ │ ├── BaseCollectionView.swift │ │ │ ├── BaseListPage.swift │ │ │ ├── BasePage.swift │ │ │ ├── BaseTabBarPage.swift │ │ │ ├── BaseUIPage.swift │ │ │ └── BaseWebView.swift │ │ ├── Models │ │ │ └── Model.swift │ │ ├── Pages │ │ │ ├── CollectionPage.swift │ │ │ ├── DrawerPage.swift │ │ │ ├── ListPage.swift │ │ │ ├── NavigationPage.swift │ │ │ ├── Page.swift │ │ │ └── PresenterPage.swift │ │ ├── SideMenu │ │ │ ├── SideMenu.swift │ │ │ └── SideMenuAnimator.swift │ │ ├── ViewModel │ │ │ ├── BaseViewModel.swift │ │ │ └── ViewModels.swift │ │ └── Views │ │ │ ├── BaseView.swift │ │ │ ├── CollectionView.swift │ │ │ ├── ListView.swift │ │ │ └── Views.swift │ ├── Controls │ │ ├── AbstractView.swift │ │ └── LocalHud.swift │ └── Layouts │ │ ├── AbsoluteLayout.swift │ │ ├── ScrollLayout.swift │ │ └── StackLayout.swift │ ├── Extensions │ ├── ReactiveExtensions │ │ ├── BindingOperators.swift │ │ ├── MPMediaPickerController+Rx.swift │ │ ├── MoyaProvider+Rx.swift │ │ ├── Observable+Ext.swift │ │ ├── Observable+Response.swift │ │ ├── ObservableConvertibleTypeExtensions.swift │ │ ├── ObservableType+ObjectMapper.swift │ │ ├── Response+ObjectMapper.swift │ │ ├── Single+ObjectMapper.swift │ │ ├── Single+Response.swift │ │ ├── UIBarButtonItemExtensions.swift │ │ ├── UIControlExtensions.swift │ │ ├── UIGestureRecognizerExtensions.swift │ │ ├── UIImagePickerController+Rx.swift │ │ ├── UIImageViewExtensions.swift │ │ ├── UILabelExtensions.swift │ │ ├── UIResponder+Rx.swift │ │ ├── UIScrollViewExtensions.swift │ │ ├── UISwitchExtensions.swift │ │ ├── UITabBarItemExtensions.swift │ │ ├── UITableViewCellExtensions.swift │ │ ├── UIViewController+Rx.swift │ │ ├── UIViewExtensions.swift │ │ └── WKWebViewExtensions.swift │ └── UIKitExtensions │ │ ├── ArrayExtensions.swift │ │ ├── NSErrorExtensions.swift │ │ ├── NSObjectExtensions.swift │ │ ├── OptionalExtensions.swift │ │ ├── StringExtensions.swift │ │ ├── UIApplicationExtensions.swift │ │ ├── UICollectionViewExtension.swift │ │ ├── UIColorExtensions.swift │ │ ├── UIEdgeInsetsExtensions.swift │ │ ├── UIImageExtensions.swift │ │ ├── UINavigationItemExtensions.swift │ │ ├── UIViewControllerExtensions.swift │ │ └── UIViewExntesions.swift │ ├── Global │ ├── Font.swift │ ├── Global.swift │ ├── Protocols.swift │ ├── Scheduler.swift │ └── SectionList.swift │ ├── Managers │ └── DependencyManager.swift │ └── Services │ ├── AlertService.swift │ ├── KeyChainService.swift │ ├── LocaleService.swift │ ├── MailService.swift │ ├── NavigationService.swift │ ├── NetworkService.swift │ ├── ReachabilityService.swift │ ├── ShareService.swift │ └── StorageService.swift ├── MVVMExample ├── .swiftlint.yml ├── MVVMExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── MVVMExample.xcscheme ├── MVVMExample.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MVVMExample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── 8957173.png │ │ │ ├── Contents.json │ │ │ ├── Icon-60.png │ │ │ ├── Icon-60@2x.png │ │ │ ├── Icon-60@3x.png │ │ │ ├── Icon-Small-1.png │ │ │ ├── Icon-Small-20.png │ │ │ ├── Icon-Small.png │ │ │ ├── Icon-Small@2x-1.png │ │ │ ├── Icon-Small@2x.png │ │ │ ├── Icon-Small@3x.png │ │ │ ├── Icon-Spotlight-40.png │ │ │ ├── Icon-Spotlight-40@2x-1.png │ │ │ ├── Icon-Spotlight-40@2x.png │ │ │ ├── Icon-Spotlight-40@3x.png │ │ │ ├── Icon-Spotlight-41.png │ │ │ ├── Icon-Spotlight-42.png │ │ │ ├── Icon-Spotlight-76.png │ │ │ ├── Icon-iPadPro76@2x.png │ │ │ ├── Icon-iPadPro@2x.png │ │ │ ├── Icon.png │ │ │ └── Icon@2x.png │ │ ├── ColorAssets │ │ │ ├── Contents.json │ │ │ └── MVVMBackgroundColors.colorset │ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Resource │ │ │ ├── Contents.json │ │ │ ├── default-contact.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── default-contact.png │ │ │ │ ├── default-contact@2x.png │ │ │ │ └── default-contact@3x.png │ │ │ └── icon-back.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── icon-back.png │ │ │ │ ├── icon-back@2x.png │ │ │ │ └── icon-back@3x.png │ │ └── Timeline │ │ │ ├── Contents.json │ │ │ ├── ic_comments.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_comments.png │ │ │ ├── ic_comments@2x.png │ │ │ └── ic_comments@3x.png │ │ │ ├── ic_like.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_like.png │ │ │ ├── ic_like@2x.png │ │ │ └── ic_like@3x.png │ │ │ ├── ic_like_selected.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_like_selected.png │ │ │ ├── ic_like_selected@2x.png │ │ │ └── ic_like_selected@3x.png │ │ │ ├── ic_reaction.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_reaction.png │ │ │ ├── ic_reaction@2x.png │ │ │ └── ic_reaction@3x.png │ │ │ ├── ic_share_post.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_share_post.png │ │ │ ├── ic_share_post@2x.png │ │ │ └── ic_share_post@3x.png │ │ │ ├── icon_notification.imageset │ │ │ ├── Contents.json │ │ │ ├── icon_notification.png │ │ │ ├── icon_notification@2x.png │ │ │ └── icon_notification@3x.png │ │ │ ├── icon_notification_selected.imageset │ │ │ ├── Contents.json │ │ │ ├── icon_notification_selected.png │ │ │ ├── icon_notification_selected@2x.png │ │ │ └── icon_notification_selected@3x.png │ │ │ ├── tab_home.imageset │ │ │ ├── Contents.json │ │ │ ├── tab_home.png │ │ │ ├── tab_home@2x.png │ │ │ └── tab_home@3x.png │ │ │ ├── tab_home_selected.imageset │ │ │ ├── Contents.json │ │ │ ├── tab_home_selected.png │ │ │ ├── tab_home_selected@2x.png │ │ │ └── tab_home_selected@3x.png │ │ │ ├── tab_message.imageset │ │ │ ├── Contents.json │ │ │ ├── tab_message.png │ │ │ ├── tab_message@2x.png │ │ │ └── tab_message@3x.png │ │ │ ├── tab_message_selected.imageset │ │ │ ├── Contents.json │ │ │ ├── tab_message_selected.png │ │ │ ├── tab_message_selected@2x.png │ │ │ └── tab_message_selected@3x.png │ │ │ ├── tab_profile.imageset │ │ │ ├── Contents.json │ │ │ ├── tab_profile.png │ │ │ ├── tab_profile@2x.png │ │ │ └── tab_profile@3x.png │ │ │ └── tab_profile_selected.imageset │ │ │ ├── Contents.json │ │ │ ├── tab_profile_selected.png │ │ │ ├── tab_profile_selected@2x.png │ │ │ └── tab_profile_selected@3x.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ ├── Localizable.strings │ │ └── Main.storyboard │ ├── Common │ │ └── UIBarButton │ │ │ ├── UIBarItems.swift │ │ │ └── UIBarItems.xib │ ├── CoreData │ │ ├── CoreDataManager.swift │ │ └── MVVMExample.xcdatamodeld │ │ │ ├── .xccurrentversion │ │ │ └── MVVMExample.xcdatamodel │ │ │ └── contents │ ├── Info.plist │ ├── Localization │ │ └── LocalizeStringConfigs.swift │ ├── Models │ │ ├── ContactModel.swift │ │ ├── FlickrSearchModel.swift │ │ ├── IntroductionModel.swift │ │ ├── ListPageModel.swift │ │ ├── MenuModel.swift │ │ └── TransitionContentModel.swift │ ├── Page │ │ ├── BaseWebKitExample │ │ │ ├── Alert │ │ │ │ ├── AlertWebPage.swift │ │ │ │ ├── AlertWebPage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── AlertWebPageViewModel.swift │ │ │ ├── Authentication │ │ │ │ ├── AuthenticationWebPage.swift │ │ │ │ ├── AuthenticationWebPage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── AuthenticationWebViewModel.swift │ │ │ ├── ConfirmAlert │ │ │ │ ├── ConfirmAlertWebPage.swift │ │ │ │ ├── ConfirmAlertWebPage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── ConfirmAlertWebViewModel.swift │ │ │ ├── EvaluateJavaScript │ │ │ │ ├── EvaluateJavaScriptWebPage.swift │ │ │ │ ├── EvaluateJavaScriptWebPage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── EvaluateJavaScriptWebViewModel.swift │ │ │ ├── FailNavigation │ │ │ │ ├── FailNavigationWebPage.swift │ │ │ │ ├── FailNavigationWebPage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── FailNavigationWebViewModel.swift │ │ │ ├── UserContentController │ │ │ │ ├── ViewModel │ │ │ │ │ └── WebKitExamplePageViewModel.swift │ │ │ │ ├── WebKitExamplePage.swift │ │ │ │ └── WebKitExamplePage.xib │ │ │ ├── ViewModel │ │ │ │ └── WebKitPageViewModel.swift │ │ │ ├── WebKitExamplesPage.swift │ │ │ └── WebKitExamplesPage.xib │ │ ├── DataBindingExamples │ │ │ ├── AuthenticationView │ │ │ │ ├── AuthenticationPage.swift │ │ │ │ └── AuthenticationPage.xib │ │ │ ├── CustomControl │ │ │ │ ├── CustomControlPage.swift │ │ │ │ ├── CustomControlPage.xib │ │ │ │ ├── SegmentView │ │ │ │ │ └── SegmentedView.swift │ │ │ │ └── ViewModel │ │ │ │ │ └── CustomControlViewPageModel.swift │ │ │ ├── DataBindingExamplesPage.swift │ │ │ ├── DataBindingExamplesPage.xib │ │ │ ├── PageController │ │ │ │ ├── UIPageExample.swift │ │ │ │ ├── UIPageExample.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── UIPageExampleViewModel.swift │ │ │ ├── TabbarController │ │ │ │ ├── TabPage │ │ │ │ │ ├── TabPage.swift │ │ │ │ │ ├── TabPage.xib │ │ │ │ │ └── TabPageViewModel │ │ │ │ │ │ └── TabPageViewModel.swift │ │ │ │ ├── TabbarView │ │ │ │ │ ├── TabbarView.swift │ │ │ │ │ ├── TabbarView.xib │ │ │ │ │ └── TabbarViewModel.swift │ │ │ │ ├── TabbarViewController.swift │ │ │ │ ├── TimelinePage │ │ │ │ │ ├── Cells │ │ │ │ │ │ ├── Activity │ │ │ │ │ │ │ ├── ActivityCell.swift │ │ │ │ │ │ │ ├── ActivityCell.xib │ │ │ │ │ │ │ └── CellViewModel │ │ │ │ │ │ │ │ └── ActivityCellViewModel.swift │ │ │ │ │ │ └── TimeLine │ │ │ │ │ │ │ ├── CellViewModel │ │ │ │ │ │ │ └── TimelineCellViewModel.swift │ │ │ │ │ │ │ ├── TimeLineCell.swift │ │ │ │ │ │ │ └── TimeLineCell.xib │ │ │ │ │ ├── DummyModel │ │ │ │ │ │ ├── ActivityModel.swift │ │ │ │ │ │ ├── TimelineModel.swift │ │ │ │ │ │ ├── TimelineResponseModel.swift │ │ │ │ │ │ └── UserInfoModel.swift │ │ │ │ │ ├── TimelinePage.swift │ │ │ │ │ ├── TimelinePage.xib │ │ │ │ │ └── ViewModel │ │ │ │ │ │ └── TimelinePageViewModel.swift │ │ │ │ └── ViewModel │ │ │ │ │ └── TabbarControllerViewModel.swift │ │ │ ├── ValidatePage │ │ │ │ ├── ValidatePage.swift │ │ │ │ ├── ValidatePage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── ValidatePageViewModel.swift │ │ │ └── ViewModel │ │ │ │ └── DataBindingExamplesViewModel.swift │ │ ├── Introduction │ │ │ ├── IntroductionPage.swift │ │ │ ├── IntroductionPage.xib │ │ │ └── ViewModel │ │ │ │ └── IntroductionViewModel.swift │ │ ├── LocalizationExample │ │ │ ├── LocalizationPage.swift │ │ │ ├── LocalizationPage.xib │ │ │ └── ViewModel │ │ │ │ └── LocalizationPageViewModel.swift │ │ ├── MVVMExamples │ │ │ ├── CollectionPage │ │ │ │ ├── Cell │ │ │ │ │ ├── CellViewModel │ │ │ │ │ │ └── SimpleCollectionViewDellModel.swift │ │ │ │ │ ├── SimpleCollectionViewCell.swift │ │ │ │ │ └── SimpleCollectionViewCell.xib │ │ │ │ ├── Header │ │ │ │ │ ├── HeaderCollectionView.swift │ │ │ │ │ ├── HeaderCollectionView.xib │ │ │ │ │ └── ViewModel │ │ │ │ │ │ └── HeaderCollectionViewModel.swift │ │ │ │ ├── SimpleCollectionPage.swift │ │ │ │ ├── SimpleCollectionPage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── CollectionPageViewModel.swift │ │ │ ├── ContactPage │ │ │ │ ├── ContactCell │ │ │ │ │ ├── CellViewModel │ │ │ │ │ │ └── ContactCellViewModel.swift │ │ │ │ │ ├── ContactTableViewCell.swift │ │ │ │ │ └── ContactTableViewCell.xib │ │ │ │ ├── ContactListPage.swift │ │ │ │ ├── ContactListPage.xib │ │ │ │ ├── EditContactPage │ │ │ │ │ ├── ContactEditPage.swift │ │ │ │ │ ├── ContactEditPage.xib │ │ │ │ │ └── ViewModel │ │ │ │ │ │ └── ContactEditPageViewModel.swift │ │ │ │ └── ViewModel │ │ │ │ │ └── ContactListPageViewModel.swift │ │ │ ├── DynamicCollectionPage │ │ │ │ ├── Cell │ │ │ │ │ ├── CellViewModel │ │ │ │ │ │ └── DynamicCollectionCellModel.swift │ │ │ │ │ ├── DynamicCollectionViewCell.swift │ │ │ │ │ └── DynamicCollectionViewCell.xib │ │ │ │ ├── DynamicCollectionPage.swift │ │ │ │ ├── DynamicCollectionPage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── DynamicCollectionPageViewModel.swift │ │ │ ├── DynamicListPage │ │ │ │ ├── DynamicListPage.swift │ │ │ │ ├── DynamicListPage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── DynamicListPageViewModel.swift │ │ │ ├── FlickrSearchPage │ │ │ │ ├── Cell │ │ │ │ │ ├── CellViewModel │ │ │ │ │ │ └── FlickrCellViewModel.swift │ │ │ │ │ ├── FlickrImageCell.swift │ │ │ │ │ └── FlickrImageCell.xib │ │ │ │ ├── FlickrImageSearchPage.swift │ │ │ │ ├── FlickrImageSearchPage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── FlickrImageSearchPageViewModel.swift │ │ │ ├── HeaderFooterListPage │ │ │ │ ├── HeaderFooterListPage.swift │ │ │ │ └── HeaderFooterListPage.xib │ │ │ ├── ImagePicker │ │ │ │ ├── ImagePickerPage.swift │ │ │ │ ├── ImagePickerPage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── ImagePickerViewModel.swift │ │ │ ├── MVVMExamplePage.swift │ │ │ ├── MVVMExamplePage.xib │ │ │ ├── SectionListPage │ │ │ │ ├── Cell │ │ │ │ │ ├── ImageCell │ │ │ │ │ │ ├── CellViewModel │ │ │ │ │ │ │ └── SectionImageCellViewModel.swift │ │ │ │ │ │ ├── SectionImageCell.swift │ │ │ │ │ │ └── SectionImageCell.xib │ │ │ │ │ └── TextCell │ │ │ │ │ │ ├── CellViewModel │ │ │ │ │ │ └── SectionTextCellViewModel.swift │ │ │ │ │ │ ├── SectionTextCell.swift │ │ │ │ │ │ └── SectionTextCell.xib │ │ │ │ ├── Section │ │ │ │ │ ├── Footer │ │ │ │ │ │ ├── SectionFooterListView.swift │ │ │ │ │ │ └── SectionFooterListView.xib │ │ │ │ │ ├── Header │ │ │ │ │ │ ├── SectionHeaderListView.swift │ │ │ │ │ │ └── SectionHeaderListView.xib │ │ │ │ │ └── ViewModel │ │ │ │ │ │ └── SectionHeaderViewViewModel.swift │ │ │ │ ├── SectionListPage.swift │ │ │ │ ├── SectionListPage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── SectionListPageViewModel.swift │ │ │ ├── SimpleListPage │ │ │ │ ├── Cell │ │ │ │ │ ├── CellViewModel │ │ │ │ │ │ └── SimpleListPageCellViewModel.swift │ │ │ │ │ ├── SimpleTableCell.swift │ │ │ │ │ └── SimpleTableCell.xib │ │ │ │ ├── ListPageExamplePage.swift │ │ │ │ ├── ListPageExamplePage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── ListPageExampleViewModel.swift │ │ │ └── ViewModel │ │ │ │ └── MvvmExamplesPageViewModel.swift │ │ ├── ServiceExamples │ │ │ ├── AlertService │ │ │ │ ├── AlertServicePage.swift │ │ │ │ ├── AlertServicePage.xib │ │ │ │ └── ViewModal │ │ │ │ │ └── AlertServiceViewModel.swift │ │ │ ├── MoyaProvider │ │ │ │ ├── MoyaProviderServicePage.swift │ │ │ │ ├── MoyaProviderServicePage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── MoyaProviderServicePageViewModel.swift │ │ │ ├── NetworkServiceExample │ │ │ │ ├── NetworkServicePage.swift │ │ │ │ ├── NetworkServicePage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── NetworkServicePageViewModel.swift │ │ │ ├── Reachability │ │ │ │ ├── ReachabilityPage.swift │ │ │ │ ├── ReachabilityPage.xib │ │ │ │ └── ViewModel │ │ │ │ │ └── ReachabilityPageViewModel.swift │ │ │ ├── ServiceExamplesPage.swift │ │ │ ├── ServiceExamplesPage.xib │ │ │ └── ViewModel │ │ │ │ └── ServiceExamplesViewModel.swift │ │ ├── StoreKit │ │ │ ├── StoreKitPage.swift │ │ │ ├── StoreKitPage.xib │ │ │ └── ViewModel │ │ │ │ └── StoreKitPageViewModel.swift │ │ ├── TableOfContent │ │ │ ├── MenuTableViewCell.swift │ │ │ ├── MenuTableViewCell.xib │ │ │ ├── TableOfContentsPage.swift │ │ │ ├── TableOfContentsPage.xib │ │ │ └── ViewModel │ │ │ │ └── TableOfContentViewModel.swift │ │ └── TransitionExamplesPage │ │ │ ├── TransitionExamplesPage.swift │ │ │ ├── TransitionExamplesPage.xib │ │ │ ├── TransitionPage │ │ │ ├── TransitionContentPage.swift │ │ │ └── TransitionContentPage.xib │ │ │ └── ViewModel │ │ │ └── TransitionExamplesPageViewModel.swift │ ├── Services │ │ ├── AlertService │ │ │ └── AlertServices.swift │ │ ├── MailService │ │ │ └── MailServiceExt.swift │ │ └── NetworkServices │ │ │ ├── AlamofireService │ │ │ ├── APIService.swift │ │ │ ├── NetworkService+Flickr.swift │ │ │ ├── NetworkService+Timeline.swift │ │ │ └── NetworkServices.swift │ │ │ └── MoyaService │ │ │ ├── MoyaAPIService.swift │ │ │ └── MoyaNetworkService.swift │ ├── SystemConfiguration │ │ ├── ColorConfig.swift │ │ ├── Constants.swift │ │ └── DeviceManager.swift │ ├── ViewController.swift │ ├── en.lproj │ │ └── Localizable.strings │ └── ja.lproj │ │ ├── LaunchScreen.strings │ │ ├── Localizable.strings │ │ └── Main.strings ├── MVVMExampleTests │ ├── ContactPage │ │ └── ContactPageTests.swift │ ├── Info.plist │ ├── MVVMExampleTests.swift │ ├── ModelTest │ │ └── ModelTests.swift │ └── NetworkService │ │ └── NetworkServiceTests.swift ├── MVVMExampleUITests │ ├── Info.plist │ └── MVVMExampleUITests.swift ├── Podfile ├── config │ ├── export-config-debug.plist │ └── export-config.plist └── fastlane │ ├── .env.secret │ ├── Appfile │ ├── FREADME.md │ ├── Fastfile │ ├── Pluginfile │ ├── report.xml │ └── temp │ ├── content.txt │ └── releaseNote.txt └── README.md /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | branches: 10 | - master 11 | paths: 12 | - './MVVM' 13 | 14 | jobs: 15 | # SwiftLint: 16 | # runs-on: ubuntu-latest 17 | # steps: 18 | # - uses: actions/checkout@v2 19 | # - name: GitHub Action for SwiftLint with 20 | # uses: norio-nomura/action-swiftlint@3.2.1 21 | # env: 22 | # DIFF_BASE: ${{ github.base_ref }} 23 | # - name: GitHub Action for SwiftLint (Different working directory) 24 | # uses: norio-nomura/action-swiftlint@3.2.1 25 | # env: 26 | # WORKING_DIRECTORY: Source 27 | 28 | Test: 29 | runs-on: macOS-latest 30 | steps: 31 | - uses: actions/checkout@v1 32 | - name: List available Xcode versions 33 | run: ls /Applications | grep Xcode 34 | - name: List available devices 35 | run: xcrun simctl list 36 | - name: Select Xcode 37 | run: sudo xcode-select -switch 38 | /Applications/Xcode_12.4.app && 39 | /usr/bin/xcodebuild -version 40 | - name: Install Dependencies 41 | working-directory: ./MVVMExample 42 | run: pod install 43 | - name: Run unit tests 44 | run: xcodebuild clean test -workspace 'MVVMExample/MVVMExample.xcworkspace' 45 | -scheme 'MVVMExample' -destination 'platform=iOS Simulator,name=iPhone 12 Pro Max,OS=latest' 46 | test-without-building | xcpretty && exit ${PIPESTATUS[0]} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 26 | # Carthage/Checkouts 27 | 28 | Carthage/ 29 | Carthage/Build 30 | 31 | 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 35 | # 36 | # Note: if you ignore the Pods directory, make sure to uncomment 37 | # `pod install` in .travis.yml 38 | # 39 | MVVMExample/Pods/ 40 | MVVMExample/Podfile.lock 41 | *.xcarchive -------------------------------------------------------------------------------- /MVVM.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint MVVM.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'MVVM' 11 | s.version = '1.0.0' 12 | s.summary = 'A MVVM library for iOS Swift.' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = <<-DESC 21 | A MVVM library for iOS Swift, including interfaces for View, ViewModel and Model, DI and Services 22 | DESC 23 | 24 | s.homepage = 'https://github.com/phamminhtien305/mvvm.git' 25 | # s.screenshots = '', '' 26 | s.license = { :type => 'MIT', :file => 'LICENSE' } 27 | s.author = { 'TienPM' => 'phamminhtien305@gmail.com' } 28 | s.source = { :git => 'https://github.com/phamminhtien305/mvvm.git', :tag => s.version.to_s } 29 | s.swift_version = '5.0' 30 | # s.social_media_url = 'https://twitter.com/phamminhtien305' 31 | 32 | s.ios.deployment_target = '14.0' 33 | 34 | s.source_files = 'MVVM/Classes/**/*' 35 | 36 | # s.resource_bundles = { 37 | # 'MVVMBase' => ['MVVM/Assets/*.png'] 38 | # } 39 | 40 | # s.public_header_files = 'Pod/Classes/**/*.h' 41 | s.frameworks = 'UIKit' 42 | 43 | s.dependency 'RxSwift' 44 | s.dependency 'RxCocoa' 45 | s.dependency 'Action' 46 | s.dependency 'Alamofire' 47 | s.dependency 'SDWebImage' 48 | s.dependency 'ObjectMapper' 49 | s.dependency 'PureLayout' 50 | s.dependency 'Moya' 51 | s.dependency 'ReachabilitySwift' 52 | s.dependency 'MagicalRecord' 53 | s.dependency 'KeychainAccess' 54 | s.dependency 'RNCryptor' 55 | s.dependency 'RxOptional' 56 | 57 | end 58 | -------------------------------------------------------------------------------- /MVVM/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVM/Assets/.gitkeep -------------------------------------------------------------------------------- /MVVM/Classes/Animators/Animator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Animator.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | 8 | open class Animator: NSObject, UIViewControllerAnimatedTransitioning { 9 | public var isPresenting = false 10 | public var duration: TimeInterval = 0.25 11 | 12 | public convenience init(withDuration duration: TimeInterval?, isPresenting present: Bool = false) { 13 | self.init() 14 | self.isPresenting = isPresenting 15 | if let duration = duration { 16 | self.duration = duration 17 | } 18 | } 19 | 20 | open func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 21 | return 1 22 | } 23 | 24 | open func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 25 | fatalError("Subclassess have to impleted this method") 26 | } 27 | 28 | public class func rectMovedIn(_ rect: CGRect, magnitude: CGFloat) -> CGRect { 29 | return CGRect(x: rect.origin.x + magnitude, 30 | y: rect.origin.y + magnitude, 31 | width: rect.size.width - magnitude * 2, 32 | height: rect.size.height - magnitude * 2) 33 | } 34 | 35 | public func snapshot(view: UIView) -> UIImage? { 36 | UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, UIScreen.main.scale) 37 | view.layer.render(in: UIGraphicsGetCurrentContext()!) 38 | let img = UIGraphicsGetImageFromCurrentImageContext() 39 | UIGraphicsEndImageContext() 40 | return img 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /MVVM/Classes/Animators/AnimatorDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatorDelegate.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | 8 | public class AnimatorDelegate: NSObject { 9 | let animator: Animator 10 | public init(withAnimator animator: Animator) { 11 | self.animator = animator 12 | super.init() 13 | } 14 | } 15 | 16 | extension AnimatorDelegate: UIViewControllerTransitioningDelegate { 17 | public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 18 | animator.isPresenting = true 19 | return animator 20 | } 21 | public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 22 | animator.isPresenting = false 23 | return animator 24 | } 25 | 26 | public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { 27 | return PresentationController(presentedViewController: presented, presenting: presenting) 28 | } 29 | } 30 | 31 | class PresentationController: UIPresentationController { 32 | override var shouldRemovePresentersView: Bool { return true } 33 | } 34 | -------------------------------------------------------------------------------- /MVVM/Classes/Core/Base/Models/Model.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Model.swift 3 | // MVVM 4 | // 5 | 6 | import Foundation 7 | import ObjectMapper 8 | 9 | open class Model: NSObject, Mappable { 10 | required public init?(map: Map) { 11 | super.init() 12 | mapping(map: map) 13 | } 14 | open func mapping(map: Map) {} 15 | } 16 | 17 | open class HeaderFooterModel: Model { 18 | open var title = "" 19 | open var footer = "" 20 | open var desc = "" 21 | 22 | public convenience init(withTitle title: String, desc: String, footer: String) { 23 | self.init(JSON: ["title": title, "desc": desc, "footer": footer])! 24 | } 25 | 26 | open override func mapping(map: Map) { 27 | title <- map["title"] 28 | desc <- map["desc"] 29 | footer <- map["footer"] 30 | } 31 | } 32 | 33 | public extension Model { 34 | static func fromJSON(_ JSON: Any?) -> T? { 35 | return Mapper().map(JSONObject: JSON) 36 | } 37 | 38 | static func fromJSON(_ JSONString: String) -> T? { 39 | return Mapper().map(JSONString: JSONString) 40 | } 41 | 42 | static func fromJSON(_ data: Data) -> T? { 43 | if let JSONString = String(data: data, encoding: .utf8) { 44 | return Mapper().map(JSONString: JSONString) 45 | } 46 | return nil 47 | } 48 | 49 | /* 50 | JSON to model array 51 | */ 52 | 53 | static func fromJSONArray(_ JSON: Any?) -> [T] { 54 | return Mapper().mapArray(JSONObject: JSON) ?? [] 55 | } 56 | 57 | static func fromJSONArray(_ JSONString: String) -> [T] { 58 | return Mapper().mapArray(JSONString: JSONString) ?? [] 59 | } 60 | 61 | static func fromJSONArray(_ data: Data) -> [T] { 62 | if let JSONString = String(data: data, encoding: .utf8) { 63 | return Mapper().mapArray(JSONString: JSONString) ?? [] 64 | } 65 | return [] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /MVVM/Classes/Core/Base/Pages/NavigationPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationPage.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | import RxSwift 8 | 9 | open class NavigationPage: UINavigationController, UIGestureRecognizerDelegate, ITransitionView, IDestroyable { 10 | public var animatorDelegate: AnimatorDelegate? 11 | public var disposeBag: DisposeBag? = DisposeBag() 12 | /** 13 | Request to update status bar content color 14 | */ 15 | public var statusBarStyle: UIStatusBarStyle = .default { 16 | didSet { setNeedsStatusBarAppearanceUpdate() } 17 | } 18 | 19 | open override var preferredStatusBarStyle: UIStatusBarStyle { 20 | return statusBarStyle 21 | } 22 | 23 | open override func viewDidLoad() { 24 | super.viewDidLoad() 25 | delegate = self 26 | interactivePopGestureRecognizer?.delegate = self 27 | } 28 | 29 | public func destroy() { 30 | viewControllers.forEach { ($0 as? IDestroyable)?.destroy() } 31 | } 32 | } 33 | 34 | extension NavigationPage: UINavigationControllerDelegate { 35 | public func navigationController(_ navigationController: UINavigationController, 36 | animationControllerFor operation: UINavigationController.Operation, 37 | from fromVC: UIViewController, 38 | to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { 39 | var animatorDelegate: AnimatorDelegate? 40 | switch operation { 41 | case .push: 42 | animatorDelegate = (toVC as? ITransitionView)?.animatorDelegate 43 | 44 | case .pop: 45 | animatorDelegate = (fromVC as? ITransitionView)?.animatorDelegate 46 | 47 | default: 48 | animatorDelegate = nil 49 | } 50 | 51 | animatorDelegate?.animator.isPresenting = operation == .push 52 | return animatorDelegate?.animator 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MVVM/Classes/Core/Controls/AbstractView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbstractView.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | 8 | open class AbstractView: UIView { 9 | public static func nibName() -> String { 10 | return String(describing: self) 11 | } 12 | 13 | public init() { 14 | super.init(frame: .zero) 15 | initialize() 16 | } 17 | 18 | public override init(frame: CGRect) { 19 | super.init(frame: frame) 20 | initialize() 21 | } 22 | 23 | public required init?(coder aDecoder: NSCoder) { 24 | super.init(coder: aDecoder) 25 | initialize() 26 | } 27 | 28 | open func initialize() {} 29 | open func setupView() {} 30 | } 31 | 32 | open class AbstractControlView: UIControl { 33 | public init() { 34 | super.init(frame: .zero) 35 | setupView() 36 | } 37 | 38 | public override init(frame: CGRect) { 39 | super.init(frame: frame) 40 | setupView() 41 | } 42 | 43 | public required init?(coder aDecoder: NSCoder) { 44 | super.init(coder: aDecoder) 45 | setupView() 46 | } 47 | 48 | open func setupView() {} 49 | } 50 | -------------------------------------------------------------------------------- /MVVM/Classes/Core/Controls/LocalHud.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineLoaderView.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | import RxSwift 8 | import RxCocoa 9 | 10 | extension Reactive where Base: LocalHud { 11 | public var show: Binder { 12 | return Binder(base) { view, value in 13 | if value { 14 | view.show() 15 | } else { 16 | view.hide() 17 | } 18 | } 19 | } 20 | } 21 | 22 | open class LocalHud: UIView { 23 | /// Subclasses override this method to style and re-layout components 24 | open func setupView() { } 25 | open func setupView(color: UIColor, message: String?, font: UIFont?) {} 26 | /// Subclasses override this method to setup a custom show animation if needed 27 | open func show() { } 28 | /// Subclasses override this method to setup a custom hide animation if needed 29 | open func hide() { } 30 | } 31 | 32 | class ActivityIndicatorHub: LocalHud { 33 | let label = UILabel() 34 | let indicatorView = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.medium) 35 | 36 | override func setupView(color: UIColor = .black, message: String? = "Loading", font: UIFont? = nil) { 37 | indicatorView.hidesWhenStopped = true 38 | indicatorView.color = color 39 | addSubview(indicatorView) 40 | indicatorView.autoAlignAxis(toSuperviewAxis: .vertical) 41 | indicatorView.autoPinEdge(toSuperviewEdge: .top) 42 | label.textColor = color 43 | label.font = font ?? Font.system.normal(withSize: 14) 44 | label.text = message 45 | addSubview(label) 46 | label.autoPinEdge(.top, to: .bottom, of: indicatorView, withOffset: 3) 47 | label.autoAlignAxis(toSuperviewAxis: .vertical) 48 | label.autoPinEdge(toSuperviewEdge: .bottom) 49 | // layout self 50 | autoCenterInSuperview() 51 | } 52 | 53 | override func show() { 54 | isHidden = false 55 | indicatorView.startAnimating() 56 | } 57 | 58 | override func hide() { 59 | isHidden = true 60 | indicatorView.stopAnimating() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/ReactiveExtensions/Observable+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Observable+Ext.swift 3 | // MVVM 4 | // 5 | // Created by tienpm on 11/24/20. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | import RxCocoa 11 | 12 | public extension ObservableType { 13 | func asDriverOnErrorJustComplete() -> Driver { 14 | return asDriver { _ in 15 | return Driver.empty() 16 | } 17 | } 18 | 19 | func mapToVoid() -> Observable { 20 | return map { _ in } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/ReactiveExtensions/UIBarButtonItemExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIBarButtonItemExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | import RxSwift 8 | import RxCocoa 9 | 10 | public extension Reactive where Base: UIBarButtonItem { 11 | var image: Binder { 12 | return Binder(base) { $0.image = $1 } 13 | } 14 | 15 | var title: Binder { 16 | return Binder(base) { $0.title = $1 } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/ReactiveExtensions/UIGestureRecognizerExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIGestureRecognizerExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | import RxSwift 8 | import RxCocoa 9 | import Action 10 | 11 | public extension Reactive where Base: UIGestureRecognizer { 12 | var isEnabled: Binder { 13 | return Binder(base) { $0.isEnabled = $1 } 14 | } 15 | } 16 | 17 | public extension UIGestureRecognizer { 18 | /// Bind action with input transformation 19 | func bind(to action: Action, inputTransform: @escaping (UIGestureRecognizer) -> (Input)) { 20 | // This effectively disposes of any existing subscriptions. 21 | unbindAction() 22 | 23 | // For each tap event, use the inputTransform closure to provide an Input value to the action 24 | rx.event 25 | .map { _ in inputTransform(self) } 26 | .bind(to: action.inputs) 27 | .disposed(by: actionDisposeBag) 28 | 29 | // Bind the enabled state of the control to the enabled state of the action 30 | action 31 | .enabled 32 | .bind(to: rx.isEnabled) 33 | .disposed(by: actionDisposeBag) 34 | } 35 | 36 | /// Bind action with static input 37 | func bind(to action: Action, input: Input) { 38 | bind(to: action) { _ in input } 39 | } 40 | 41 | /// Unbinds any existing action, disposing of all subscriptions. 42 | func unbindAction() { 43 | resetActionDisposeBag() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/ReactiveExtensions/UIResponder+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIResponder+Keyboard.swift 3 | // MVVM 4 | // 5 | // Created by tienpm on 11/13/20. 6 | // 7 | 8 | import Foundation 9 | import RxCocoa 10 | import RxSwift 11 | 12 | public class KeyboardManager: NSObject { 13 | open class func keyboardHeight() -> Observable { 14 | return Observable.from([ 15 | NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification) 16 | .map { notification -> CGFloat in 17 | (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height ?? 0 18 | }, 19 | NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification) 20 | .map { _ -> CGFloat in 21 | 0 22 | } 23 | ]) 24 | .merge() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/ReactiveExtensions/UIScrollViewExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollViewExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | import RxSwift 8 | 9 | public extension Reactive where Base: UIScrollView { 10 | func endReach(_ distance: CGFloat) -> Observable { 11 | return Observable.create { observer in 12 | return self.base.rx.contentOffset.subscribe(onNext: { offset in 13 | let scrollViewHeight = self.base.frame.size.height 14 | let scrollContentSizeHeight = self.base.contentSize.height 15 | let scrollOffset = offset.y 16 | 17 | let scrollSize = scrollOffset + scrollViewHeight 18 | 19 | // at the bottom 20 | if scrollSize >= scrollContentSizeHeight - distance { 21 | observer.onNext(()) 22 | } 23 | }) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/ReactiveExtensions/UISwitchExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UISwitchExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | import RxSwift 8 | import RxCocoa 9 | 10 | public extension Reactive where Base: UISwitch { 11 | var onTintColor: Binder { 12 | return Binder(base) { $0.onTintColor = $1 } 13 | } 14 | 15 | var isOn: ControlProperty { 16 | return UIControl.toProperty(control: self.base, 17 | getter: { view in 18 | view.isOn 19 | }, setter: { view, value in 20 | if value != view.isOn { 21 | view.setOn(value, animated: true) 22 | } 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/ReactiveExtensions/UITabBarItemExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITabBarItemExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | import RxSwift 8 | import RxCocoa 9 | 10 | public extension Reactive where Base: UITabBarItem { 11 | var image: Binder { 12 | return Binder(self.base) { $0.image = $1 } 13 | } 14 | 15 | var title: Binder { 16 | return Binder(self.base) { $0.title = $1 } 17 | } 18 | 19 | var badge: Binder { 20 | return Binder(self.base) { control, value in 21 | if value <= 0 { 22 | control.badgeValue = nil 23 | } else { 24 | control.badgeValue = "\(value)" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/ReactiveExtensions/UITableViewCellExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableViewCellExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | import RxSwift 8 | import RxCocoa 9 | 10 | public extension Reactive where Base: UITableViewCell { 11 | var accessoryType: Binder { 12 | return Binder(self.base) { $0.accessoryType = $1 } 13 | } 14 | 15 | var selectionStyle: Binder { 16 | return Binder(self.base) { $0.selectionStyle = $1 } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/ReactiveExtensions/UIViewExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | import RxSwift 8 | import RxCocoa 9 | 10 | public extension Reactive where Base: UIView { 11 | var backgroundColor: Binder { 12 | return Binder(base) { $0.backgroundColor = $1 } 13 | } 14 | 15 | var tintColor: Binder { 16 | return Binder(base) { $0.tintColor = $1 } 17 | } 18 | 19 | var borderColor: Binder { 20 | return Binder(base) { $0.layer.borderColor = $1.cgColor } 21 | } 22 | 23 | var borderWidth: Binder { 24 | return Binder(base) { $0.layer.borderWidth = $1 } 25 | } 26 | 27 | var cornerRadius: Binder { 28 | return Binder(base) { $0.cornerRadius = $1 } 29 | } 30 | 31 | var tapGesture: ControlEvent { 32 | var tap: UITapGestureRecognizer! = base.getGesture() 33 | if tap == nil { 34 | tap = UITapGestureRecognizer() 35 | base.addGestureRecognizer(tap) 36 | } 37 | 38 | return tap.rx.event 39 | } 40 | 41 | var panGesture: ControlEvent { 42 | var pan: UIPanGestureRecognizer! = base.getGesture() 43 | if pan == nil { 44 | pan = UIPanGestureRecognizer() 45 | base.addGestureRecognizer(pan) 46 | } 47 | 48 | return pan.rx.event 49 | } 50 | 51 | var pinchGesture: ControlEvent { 52 | var pinch: UIPinchGestureRecognizer! = base.getGesture() 53 | if pinch == nil { 54 | pinch = UIPinchGestureRecognizer() 55 | base.addGestureRecognizer(pinch) 56 | } 57 | 58 | return pinch.rx.event 59 | } 60 | 61 | var longPressGesture: ControlEvent { 62 | var longPress: UILongPressGestureRecognizer! = base.getGesture() 63 | if longPress == nil { 64 | longPress = UILongPressGestureRecognizer() 65 | base.addGestureRecognizer(longPress) 66 | } 67 | 68 | return longPress.rx.event 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/UIKitExtensions/ArrayExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import Foundation 7 | 8 | public extension Array { 9 | /// Chunk array into smaller parts 10 | func chunked(by chunkSize: Int) -> [[Element]] { 11 | return stride(from: 0, to: self.count, by: chunkSize).map { 12 | Array(self[$0..() -> [T] where T.ModelElement == Element { 20 | return compactMap { 21 | T(model: $0) 22 | } 23 | } 24 | 25 | func toBaseCellViewModels() -> [T] { 26 | return compactMap { 27 | T(model: $0) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/UIKitExtensions/NSErrorExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSErrorExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | 8 | extension NSError { 9 | private static var domain: String { 10 | return Bundle.main.bundleIdentifier ?? "dd.mvvm.error" 11 | } 12 | 13 | static var unknown: NSError { 14 | return NSError(domain: domain, code: 1000, userInfo: [NSLocalizedDescriptionKey: "Unknown error occurrs."]) 15 | } 16 | 17 | static var mappingError: NSError { 18 | return NSError(domain: domain, code: 1001, userInfo: [NSLocalizedDescriptionKey: "Failed to transform from json to Model."]) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/UIKitExtensions/NSObjectExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSObjectExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import Foundation 7 | import RxSwift 8 | 9 | struct AssociatedKeys { 10 | static var Action = "rx_action" 11 | static var DisposeBag = "rx_disposeBag" 12 | } 13 | 14 | // Note: Actions performed in this extension are _not_ locked 15 | // So be careful! 16 | extension NSObject { 17 | // A dispose bag to be used exclusively for the instance's rx.action. 18 | var actionDisposeBag: DisposeBag { 19 | var disposeBag: DisposeBag 20 | 21 | if let lookup = objc_getAssociatedObject(self, &AssociatedKeys.DisposeBag) as? DisposeBag { 22 | disposeBag = lookup 23 | } else { 24 | disposeBag = DisposeBag() 25 | objc_setAssociatedObject(self, &AssociatedKeys.DisposeBag, disposeBag, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 26 | } 27 | 28 | return disposeBag 29 | } 30 | 31 | // Resets the actionDisposeBag to nil, disposeing of any subscriptions within it. 32 | func resetActionDisposeBag() { 33 | objc_setAssociatedObject(self, &AssociatedKeys.DisposeBag, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 34 | } 35 | 36 | // Uses objc_sync on self to perform a locked operation. 37 | func doLocked(_ closure: () -> Void) { 38 | objc_sync_enter(self); defer { objc_sync_exit(self) } 39 | closure() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/UIKitExtensions/OptionalExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | 8 | public extension Optional where Wrapped == String { 9 | var isNilOrEmpty: Bool { 10 | return self == nil || self!.isEmpty 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/UIKitExtensions/StringExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | import CommonCrypto 8 | 9 | public extension String { 10 | func toURL() -> URL? { 11 | return URL(string: self) 12 | } 13 | 14 | func toURLRequest() -> URLRequest? { 15 | if let url = toURL() { 16 | return URLRequest(url: url) 17 | } 18 | return nil 19 | } 20 | 21 | func toHex() -> Int? { 22 | return Int(self, radix: 16) 23 | } 24 | 25 | func trim() -> String { 26 | return trimmingCharacters(in: .whitespacesAndNewlines) 27 | } 28 | 29 | func sha1() -> String { 30 | let data = self.data(using: String.Encoding.utf8)! 31 | var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH)) 32 | 33 | data.withUnsafeBytes { dataBytes in 34 | _ = CC_SHA1(dataBytes, CC_LONG(data.count), &digest) 35 | } 36 | 37 | let hexBytes = digest.map { String(format: "%02hhx", $0) } 38 | return hexBytes.joined() 39 | } 40 | 41 | var md5: String { 42 | let data = Data(self.utf8) 43 | let hash = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> [UInt8] in 44 | var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) 45 | CC_MD5(bytes.baseAddress, CC_LONG(data.count), &hash) 46 | return hash 47 | } 48 | return hash.map { String(format: "%02x", $0) }.joined() 49 | } 50 | 51 | var localized: String { 52 | return LocalizeService.shared.localized(self) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/UIKitExtensions/UIApplicationExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplicationExtensions.swift 3 | // MVVM 4 | // 5 | // Created by pham.minh.tien on 2/18/21. 6 | // 7 | import UIKit 8 | 9 | public extension UIApplication { 10 | class func getKeyWindow() -> UIWindow? { 11 | if #available(iOS 13.0, *) { 12 | return UIApplication.shared.connectedScenes 13 | .filter({ 14 | $0.activationState == .foregroundActive 15 | }) 16 | .map({ 17 | $0 as? UIWindowScene 18 | }) 19 | .compactMap({ 20 | $0 21 | }) 22 | .first?.windows.first(where: { 23 | $0.isKeyWindow 24 | }) 25 | } else { 26 | return UIApplication.shared.keyWindow 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/UIKitExtensions/UICollectionViewExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionViewExtension.swift 3 | // Action 4 | // 5 | // Created by pham.minh.tien on 4/30/20. 6 | // 7 | 8 | import UIKit 9 | import RxSwift 10 | 11 | extension UICollectionView { 12 | open func register(collectionViewCell: T.Type) where T: BaseCollectionCell { 13 | register(T.nib, forCellWithReuseIdentifier: T.identifier) 14 | } 15 | 16 | open func register(headerType: T.Type) where T: BaseHeaderCollectionView { 17 | register(T.nib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: T.identifier) 18 | } 19 | 20 | open func register(footerType: T.Type) where T: BaseHeaderCollectionView { 21 | register(T.nib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: T.identifier) 22 | } 23 | } 24 | 25 | extension UITableView { 26 | open func register(cellType: T.Type) where T: BaseTableCell { 27 | register(T.nib, forCellReuseIdentifier: T.identifier) 28 | } 29 | 30 | open func register(headerType: T.Type) where T: BaseHeaderTableView { 31 | register(T.nib, forHeaderFooterViewReuseIdentifier: T.identifier) 32 | } 33 | 34 | open func register(footerType: T.Type) where T: BaseHeaderTableView { 35 | register(T.nib, forHeaderFooterViewReuseIdentifier: T.identifier) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/UIKitExtensions/UIColorExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColorExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | 8 | // swiftlint:disable identifier_name 9 | public extension UIColor { 10 | convenience init(hex: Int) { 11 | self.init(hex: hex, a: 1.0) 12 | } 13 | 14 | convenience init(hex: Int, a: CGFloat) { 15 | self.init(r: (hex >> 16) & 0xff, g: (hex >> 8) & 0xff, b: hex & 0xff, a: a) 16 | } 17 | 18 | convenience init(r: Int, g: Int, b: Int) { 19 | self.init(r: r, g: g, b: b, a: 1.0) 20 | } 21 | 22 | convenience init(r: Int, g: Int, b: Int, a: CGFloat) { 23 | self.init(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: a) 24 | } 25 | 26 | convenience init(hexString: String) { 27 | let hexString = hexString.replacingOccurrences(of: "#", with: "") 28 | let scanner = Scanner(string: hexString) 29 | scanner.scanLocation = 0 30 | var rgbValue: UInt64 = 0 31 | scanner.scanHexInt64(&rgbValue) 32 | let r = (rgbValue & 0xff0000) >> 16 33 | let g = (rgbValue & 0xff00) >> 8 34 | let b = rgbValue & 0xff 35 | self.init(red: CGFloat(r) / 0xff, green: CGFloat(g) / 0xff, blue: CGFloat(b) / 0xff, alpha: 1.0) 36 | } 37 | 38 | static func fromHex(_ hexString: String) -> UIColor { 39 | return UIColor(hexString: hexString) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/UIKitExtensions/UIEdgeInsetsExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIEdgeInsetsExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | 8 | public extension UIEdgeInsets { 9 | @available(*, deprecated, renamed: "all") 10 | static func equally(_ padding: CGFloat) -> UIEdgeInsets { 11 | return UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding) 12 | } 13 | 14 | static func all(_ padding: CGFloat) -> UIEdgeInsets { 15 | return UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding) 16 | } 17 | 18 | @available(*, deprecated, renamed: "symmetric") 19 | static func topBottom(_ topBottom: CGFloat, leftRight: CGFloat) -> UIEdgeInsets { 20 | return UIEdgeInsets(top: topBottom, left: leftRight, bottom: topBottom, right: leftRight) 21 | } 22 | 23 | static func symmetric(horizontal: CGFloat = 0, vertical: CGFloat = 0) -> UIEdgeInsets { 24 | return UIEdgeInsets(top: vertical, left: horizontal, bottom: vertical, right: horizontal) 25 | } 26 | 27 | static func only(top: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0, right: CGFloat = 0) -> UIEdgeInsets { 28 | return UIEdgeInsets(top: top, left: left, bottom: bottom, right: right) 29 | } 30 | 31 | @available(*, deprecated, renamed: "only") 32 | static func top(_ value: CGFloat) -> UIEdgeInsets { 33 | return UIEdgeInsets(top: value, left: 0, bottom: 0, right: 0) 34 | } 35 | 36 | @available(*, deprecated, renamed: "only") 37 | static func left(_ value: CGFloat) -> UIEdgeInsets { 38 | return UIEdgeInsets(top: 0, left: value, bottom: 0, right: 0) 39 | } 40 | 41 | @available(*, deprecated, renamed: "only") 42 | static func bottom(_ value: CGFloat) -> UIEdgeInsets { 43 | return UIEdgeInsets(top: 0, left: 0, bottom: value, right: 0) 44 | } 45 | 46 | @available(*, deprecated, renamed: "only") 47 | static func right(_ value: CGFloat) -> UIEdgeInsets { 48 | return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: value) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/UIKitExtensions/UIImageExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKitExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | 8 | public extension UIImage { 9 | /// Create image from mono color 10 | static func from(color: UIColor) -> UIImage { 11 | let size = CGSize(width: 1, height: 1) 12 | return from(color: color, withSize: size) 13 | } 14 | 15 | /// Create image from mono color with specific size and corner radius 16 | static func from(color: UIColor, 17 | withSize size: CGSize, 18 | cornerRadius: CGFloat = 0) -> UIImage { 19 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 20 | let path = UIBezierPath(roundedRect: CGRect(x: 0, 21 | y: 0, 22 | width: size.width, 23 | height: size.height), 24 | cornerRadius: cornerRadius) 25 | path.addClip() 26 | color.setFill() 27 | path.fill() 28 | let image = UIGraphicsGetImageFromCurrentImageContext() 29 | UIGraphicsEndImageContext() 30 | return image! 31 | } 32 | 33 | func resizeImage(scale: CGFloat) -> UIImage { 34 | let newSize = CGSize(width: self.size.width * scale, 35 | height: self.size.height * scale) 36 | let rect = CGRect(origin: CGPoint.zero, size: newSize) 37 | 38 | UIGraphicsBeginImageContext(newSize) 39 | self.draw(in: rect) 40 | let newImage = UIGraphicsGetImageFromCurrentImageContext() 41 | UIGraphicsEndImageContext() 42 | return newImage! 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MVVM/Classes/Extensions/UIKitExtensions/UINavigationItemExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationItemExtensions.swift 3 | // MVVM 4 | // 5 | 6 | import UIKit 7 | 8 | extension UINavigationItem { 9 | @discardableResult 10 | @objc 11 | open func setTitle(_ title: String? = nil, 12 | textColor: UIColor = .white, 13 | alignment: NSTextAlignment = .center, 14 | font: UIFont = Font.system.bold(withSize: 18)) -> UILabel { 15 | let titleLbl = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 44)) 16 | titleLbl.textColor = textColor 17 | titleLbl.text = title 18 | titleLbl.textAlignment = alignment 19 | titleLbl.font = font 20 | titleView = titleLbl 21 | return titleLbl 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MVVM/Classes/Global/Scheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Scheduler.swift 3 | // MVVM 4 | // 5 | 6 | import Foundation 7 | import RxSwift 8 | 9 | public class Scheduler { 10 | public static let shared = Scheduler() 11 | 12 | public let backgroundScheduler: ImmediateSchedulerType 13 | public let mainScheduler: SerialDispatchQueueScheduler 14 | 15 | init() { 16 | let operationQueue = OperationQueue() 17 | operationQueue.maxConcurrentOperationCount = 5 18 | operationQueue.qualityOfService = QualityOfService.userInitiated 19 | 20 | backgroundScheduler = OperationQueueScheduler(operationQueue: operationQueue) 21 | mainScheduler = MainScheduler.instance 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MVVM/Classes/Services/KeyChainService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyChainService.swift 3 | // MVVM 4 | // 5 | // Created by tienpm on 11/13/20. 6 | // 7 | 8 | import Foundation 9 | import KeychainAccess 10 | import RNCryptor 11 | 12 | enum KeyChainConfig { 13 | static let privateKey: String = "KeychainPrivate-Key" 14 | static let passwordKey: String = "Password-Key" 15 | } 16 | 17 | public class KeyChainService { 18 | private static let keychain = Keychain(service: Bundle.main.bundleIdentifier!) 19 | 20 | static func generatePrivateKey() { 21 | if keychain[KeyChainConfig.privateKey].isNilOrEmpty { 22 | let uuid = UUID().uuidString 23 | keychain[KeyChainConfig.passwordKey] = uuid 24 | } 25 | } 26 | 27 | static func encryption(password: String) { 28 | guard let data = password.data(using: .utf8), let key = keychain[KeyChainConfig.privateKey] else { 29 | return 30 | } 31 | let passwordEncrypted = RNCryptor.encrypt(data: data, withPassword: key) 32 | UserDefaults.standard.set(passwordEncrypted, forKey: KeyChainConfig.passwordKey) 33 | } 34 | 35 | static func getPassword() -> String? { 36 | return decryption() 37 | } 38 | 39 | private static func decryption() -> String? { 40 | do { 41 | guard let passwordEncrypted = UserDefaults.standard.data(forKey: KeyChainConfig.passwordKey), let key = keychain[KeyChainConfig.privateKey] else { 42 | return nil 43 | } 44 | let passwordData = try RNCryptor.decrypt(data: passwordEncrypted, withPassword: key) 45 | return String(data: passwordData, encoding: .utf8) 46 | } catch { 47 | debugPrint(error) 48 | return nil 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /MVVM/Classes/Services/LocaleService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocaleManager.swift 3 | // Action 4 | // 5 | // Created by pham.minh.tien on 8/4/20. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | import RxCocoa 11 | 12 | public class LocalizeService { 13 | private static let kCurrentLocale = "CurrentLocale" 14 | private static let kDefaultLocale = "en" 15 | 16 | static let shared = LocalizeService() 17 | 18 | public var rxLocaleState = BehaviorRelay(value: "") 19 | 20 | public init() { 21 | if UserDefaults.standard.value(forKey: LocalizeService.kCurrentLocale) == nil { 22 | UserDefaults.standard.set(LocalizeService.kDefaultLocale, forKey: LocalizeService.kCurrentLocale) 23 | UserDefaults.standard.synchronize() 24 | } 25 | } 26 | 27 | public func getCurrentLocale() -> String { 28 | if let locale = UserDefaults.standard.value(forKey: LocalizeService.kCurrentLocale) as? String { 29 | return locale 30 | } 31 | return LocalizeService.kDefaultLocale 32 | } 33 | 34 | public func setCurrentLocale(_ locale: String) { 35 | UserDefaults.standard.set(locale, forKey: LocalizeService.kCurrentLocale) 36 | UserDefaults.standard.synchronize() 37 | self.rxLocaleState.accept(locale) 38 | } 39 | 40 | func localized(_ key: String) -> String { 41 | let locale = LocalizeService.shared.getCurrentLocale() 42 | let enBundlePath = Bundle.main.path(forResource: locale, ofType: "lproj") 43 | guard let bundle = Bundle(path: enBundlePath!) else { 44 | return "" 45 | } 46 | return NSLocalizedString(key, tableName: nil, bundle: bundle, value: key, comment: key) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /MVVM/Classes/Services/MailService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MailService.swift 3 | // Action 4 | // 5 | // Created by pham.minh.tien on 8/3/20. 6 | // 7 | 8 | import Foundation 9 | import MessageUI 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public class MailService: NSObject, MFMailComposeViewControllerDelegate { 14 | static var shared = MailService() 15 | public var rxMailComposeState = BehaviorRelay(value: nil) 16 | public var rxMailSettingValidate = BehaviorRelay(value: "") 17 | 18 | public func canSendEmailAndAlert() -> Bool { 19 | if MFMailComposeViewController.canSendMail() { 20 | return true 21 | } else { 22 | return false 23 | } 24 | } 25 | 26 | public func sendMailTo(listEmail emails: [String], 27 | withSubject subject: String, 28 | withMessage message: String, 29 | withModalType modalType: UIModalPresentationStyle = .fullScreen) { 30 | if MailService.shared.canSendEmailAndAlert() { 31 | let mailer = MFMailComposeViewController() 32 | mailer.mailComposeDelegate = self 33 | mailer.modalPresentationStyle = modalType 34 | mailer.setToRecipients(emails) 35 | mailer.setSubject(subject) 36 | mailer.setMessageBody(message, isHTML: false) 37 | let window = UIApplication.shared.keyWindow 38 | guard let rootViewContorller = window?.rootViewController else { 39 | return 40 | } 41 | rootViewContorller.present(mailer, animated: true) 42 | } else { 43 | rxMailSettingValidate.accept("Please set up mail account in order to send email") 44 | } 45 | } 46 | 47 | public func mailComposeController(_ controller: MFMailComposeViewController, 48 | didFinishWith result: MFMailComposeResult, 49 | error: Error?) { 50 | rxMailComposeState.accept(result) 51 | controller.dismiss(animated: true, completion: nil) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MVVM/Classes/Services/ReachabilityService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReachabilityService.swift 3 | // Action 4 | // 5 | // Created by dinh.tung on 7/17/20. 6 | // 7 | 8 | import Foundation 9 | import Reachability 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public class ReachabilityService { 14 | public static let share = ReachabilityService() 15 | private var reachability: Reachability? 16 | public var connectState = BehaviorRelay(value: nil) 17 | private var option: String? 18 | 19 | public func startReachability(_ option: String = "") { 20 | self.option = option 21 | do { 22 | reachability = try Reachability() 23 | try reachability?.startNotifier() 24 | } catch { 25 | print("Unable to start notifier") 26 | } 27 | 28 | reachability?.whenReachable = { reachability in 29 | print("NETWORK: connection did change \(reachability.connection.description)") 30 | if option == "alert" { 31 | } else if option == "status bar" { 32 | } 33 | 34 | switch reachability.connection { 35 | case .wifi: 36 | self.connectState.accept(.wifi) 37 | 38 | case .cellular: 39 | self.connectState.accept(.cellular) 40 | 41 | default: 42 | self.connectState.accept(.unavailable) 43 | } 44 | } 45 | 46 | reachability?.whenUnreachable = { _ in 47 | self.connectState.accept(.unavailable) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MVVMExample/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | # - identifier_name 3 | - vertical_parameter_alignment 4 | - orphaned_doc_comment 5 | opt_in_rules: 6 | # - anyobject_protocol 7 | # - array_init 8 | # - attributes 9 | # - closure_end_indentation 10 | # - closure_spacing 11 | # - collection_alignment 12 | # - conditional_returns_on_newline 13 | # - contains_over_first_not_nil 14 | # - convenience_type 15 | # - discouraged_optional_boolean 16 | # - discouraged_optional_collection 17 | # - empty_count 18 | # - empty_string 19 | # - explicit_init 20 | # - explicit_self 21 | # - first_where 22 | # - force_unwrapping 23 | # - implicit_return 24 | # - implicitly_unwrapped_optional 25 | # - let_var_whitespace 26 | # - literal_expression_end_indentation 27 | # - multiline_arguments 28 | # - multiline_function_chains 29 | # - multiline_literal_brackets 30 | # - multiline_parameters 31 | # - operator_usage_whitespace 32 | # - overridden_super_call 33 | # - prefixed_toplevel_constant 34 | # - prohibited_super_call 35 | # - redundant_nil_coalescing 36 | # - redundant_type_annotation 37 | # - sorted_first_last 38 | # - switch_case_on_newline 39 | # - toggle_bool 40 | # - unneeded_parentheses_in_closure_argument 41 | # - unused_import 42 | # - vertical_whitespace_between_cases 43 | # - vertical_whitespace_closing_braces 44 | # - vertical_whitespace_opening_braces 45 | 46 | line_length: 200 47 | #force_unwrapping: 48 | # severity: error 49 | #force_cast: 50 | # severity: warning 51 | excluded: 52 | - Pods/ 53 | - MVVMExampleTests/ 54 | - MVVMExampleUITests/ 55 | - /UI/Launch/LBXScan/ 56 | - Carthage/ 57 | - Frameworks/ 58 | - Kickstarter-iOS.playground/ 59 | - Library/Strings.swift 60 | - bin/strings.swift 61 | - bin/StringsScript/Sources/StringsScriptCore/Secrets.swift 62 | - vendor 63 | included: 64 | - ../MVVM 65 | reporter: "xcode" 66 | 67 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/8957173.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/8957173.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-60.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Small-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Small-1.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Small-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Small-20.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Small.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x-1.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-41.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-42.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-76.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-iPadPro76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-iPadPro76@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-iPadPro@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon-iPadPro@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/AppIcon.appiconset/Icon@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/ColorAssets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/ColorAssets/MVVMBackgroundColors.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "1.000", 13 | "alpha" : "1.000", 14 | "blue" : "1.000", 15 | "green" : "1.000" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Resource/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Resource/default-contact.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "default-contact.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "default-contact@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "default-contact@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Resource/default-contact.imageset/default-contact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Resource/default-contact.imageset/default-contact.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Resource/default-contact.imageset/default-contact@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Resource/default-contact.imageset/default-contact@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Resource/default-contact.imageset/default-contact@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Resource/default-contact.imageset/default-contact@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Resource/icon-back.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-back.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon-back@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "icon-back@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Resource/icon-back.imageset/icon-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Resource/icon-back.imageset/icon-back.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Resource/icon-back.imageset/icon-back@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Resource/icon-back.imageset/icon-back@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Resource/icon-back.imageset/icon-back@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Resource/icon-back.imageset/icon-back@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_comments.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_comments.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_comments@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_comments@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_comments.imageset/ic_comments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_comments.imageset/ic_comments.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_comments.imageset/ic_comments@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_comments.imageset/ic_comments@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_comments.imageset/ic_comments@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_comments.imageset/ic_comments@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_like.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_like@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_like@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like.imageset/ic_like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like.imageset/ic_like.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like.imageset/ic_like@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like.imageset/ic_like@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like.imageset/ic_like@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like.imageset/ic_like@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like_selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_like_selected.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_like_selected@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_like_selected@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like_selected.imageset/ic_like_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like_selected.imageset/ic_like_selected.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like_selected.imageset/ic_like_selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like_selected.imageset/ic_like_selected@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like_selected.imageset/ic_like_selected@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_like_selected.imageset/ic_like_selected@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_reaction.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_reaction.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_reaction@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_reaction@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_reaction.imageset/ic_reaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_reaction.imageset/ic_reaction.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_reaction.imageset/ic_reaction@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_reaction.imageset/ic_reaction@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_reaction.imageset/ic_reaction@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_reaction.imageset/ic_reaction@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_share_post.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_share_post.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_share_post@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_share_post@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_share_post.imageset/ic_share_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_share_post.imageset/ic_share_post.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_share_post.imageset/ic_share_post@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_share_post.imageset/ic_share_post@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_share_post.imageset/ic_share_post@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/ic_share_post.imageset/ic_share_post@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_notification.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "icon_notification@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "icon_notification@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification.imageset/icon_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification.imageset/icon_notification.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification.imageset/icon_notification@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification.imageset/icon_notification@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification.imageset/icon_notification@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification.imageset/icon_notification@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification_selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icon_notification_selected.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "icon_notification_selected@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "icon_notification_selected@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification_selected.imageset/icon_notification_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification_selected.imageset/icon_notification_selected.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification_selected.imageset/icon_notification_selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification_selected.imageset/icon_notification_selected@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification_selected.imageset/icon_notification_selected@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/icon_notification_selected.imageset/icon_notification_selected@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "tab_home.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "tab_home@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "tab_home@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home.imageset/tab_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home.imageset/tab_home.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home.imageset/tab_home@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home.imageset/tab_home@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home.imageset/tab_home@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home.imageset/tab_home@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home_selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "tab_home_selected.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "tab_home_selected@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "tab_home_selected@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home_selected.imageset/tab_home_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home_selected.imageset/tab_home_selected.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home_selected.imageset/tab_home_selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home_selected.imageset/tab_home_selected@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home_selected.imageset/tab_home_selected@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_home_selected.imageset/tab_home_selected@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "tab_message.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "tab_message@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "tab_message@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message.imageset/tab_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message.imageset/tab_message.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message.imageset/tab_message@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message.imageset/tab_message@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message.imageset/tab_message@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message.imageset/tab_message@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message_selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "tab_message_selected.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "tab_message_selected@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "tab_message_selected@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message_selected.imageset/tab_message_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message_selected.imageset/tab_message_selected.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message_selected.imageset/tab_message_selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message_selected.imageset/tab_message_selected@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message_selected.imageset/tab_message_selected@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_message_selected.imageset/tab_message_selected@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "tab_profile.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "tab_profile@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "tab_profile@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile.imageset/tab_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile.imageset/tab_profile.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile.imageset/tab_profile@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile.imageset/tab_profile@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile.imageset/tab_profile@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile.imageset/tab_profile@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile_selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "tab_profile_selected.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "tab_profile_selected@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "tab_profile_selected@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile_selected.imageset/tab_profile_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile_selected.imageset/tab_profile_selected.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile_selected.imageset/tab_profile_selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile_selected.imageset/tab_profile_selected@2x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile_selected.imageset/tab_profile_selected@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tienpm-0557/mvvm/37a4f4e78f7360264478d4fa48dd694f5b7c50f2/MVVMExample/MVVMExample/Assets.xcassets/Timeline/tab_profile_selected.imageset/tab_profile_selected@3x.png -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Base.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | MVVMExample 4 | 5 | Created by pham.minh.tien on 8/4/20. 6 | Copyright © 2020 Sun*. All rights reserved. 7 | */ 8 | 9 | //MARK: Table Of Contents 10 | strTableOfContents = "Table Of Contents"; 11 | 12 | strLocalizePageTitle = "Localization"; 13 | strTestMessage = "Tôi thấy truyền hình rất có tính giáo dục. Mỗi khi ai đó bật ti vi lên, tôi lại sang phòng khác và đọc sách."; 14 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/CoreData/CoreDataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataManager.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 10/1/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MagicalRecord 11 | 12 | class CoreDataManager: NSObject { 13 | class func setupCoreDataStack() { 14 | MagicalRecord.setupCoreDataStack() 15 | MagicalRecord.setupAutoMigratingCoreDataStack() 16 | } 17 | 18 | class func saveContext() { 19 | NSManagedObjectContext.mr_default().mr_saveToPersistentStoreAndWait() 20 | } 21 | // MARK: Core data helper 22 | class func archiveNSDataFromObject(_ params: Any!, 23 | withKey key: String) -> Data! { 24 | if params == nil { 25 | return nil 26 | } 27 | let data = NSMutableData() 28 | let archiver = NSKeyedArchiver(requiringSecureCoding: true) 29 | archiver.encode(params as AnyObject, forKey: key) 30 | archiver.finishEncoding() 31 | return data as Data 32 | } 33 | 34 | class func unarchiveNSData(withData data: Data, 35 | withKey key: String) -> Any? { 36 | do { 37 | let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data) 38 | let jsonData = unarchiver.decodeObject(forKey: key) 39 | return jsonData 40 | } catch { 41 | return nil 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/CoreData/MVVMExample.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | MVVMExample.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/CoreData/MVVMExample.xcdatamodeld/MVVMExample.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | MVVM 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | com.mvvm.template 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | NSCameraUsageDescription 31 | This app requires to access your camera 32 | NSPhotoLibraryUsageDescription 33 | This app requires to access your photo library 34 | UILaunchStoryboardName 35 | LaunchScreen 36 | UIRequiredDeviceCapabilities 37 | 38 | armv7 39 | 40 | UISupportedInterfaceOrientations 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UISupportedInterfaceOrientations~ipad 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationPortraitUpsideDown 50 | UIInterfaceOrientationLandscapeLeft 51 | UIInterfaceOrientationLandscapeRight 52 | 53 | cfBundleVersion 54 | 1.0 55 | 56 | 57 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Localization/LocalizeStringConfigs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizeStringConfigs.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 8/4/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct LocalizedStringConfigs { 12 | static let strLocalizePageTitle = "strLocalizePageTitle" 13 | static let strTableOfContents = "strTableOfContents" 14 | } 15 | 16 | struct LocalizedStringMessage { 17 | static let strTestMessage = "strTestMessage" 18 | } 19 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Models/ContactModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/24/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ObjectMapper 11 | import MVVM 12 | 13 | class ContactModel: Model { 14 | var name = "" 15 | var phone = "" 16 | 17 | convenience init() { 18 | self.init(JSON: [String: Any]())! 19 | } 20 | 21 | override func mapping(map: Map) { 22 | name <- map["name"] 23 | phone <- map["phone"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Models/IntroductionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntroductionModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/24/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MVVM 11 | import ObjectMapper 12 | 13 | class IntroductionModel: Model { 14 | var title = "" 15 | var desc = "" 16 | var url = "" 17 | 18 | convenience init(withTitle title: String, desc: String, url: String) { 19 | self.init(JSON: ["title": title, "desc": desc, "url": url])! 20 | } 21 | 22 | override func mapping(map: Map) { 23 | title <- map["title"] 24 | desc <- map["desc"] 25 | url <- map["url"] 26 | } 27 | } 28 | 29 | extension NSURL { 30 | func isAbleFormatImage() -> Bool { 31 | guard let pathExtension = self.pathExtension else { 32 | return false 33 | } 34 | if pathExtension.contains("png") || 35 | pathExtension.contains("jpeg") || 36 | pathExtension.contains("jpg") { 37 | return true 38 | } 39 | return false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Models/ListPageModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListPageModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ObjectMapper 11 | import MVVM 12 | 13 | class SimpleModel: Model { 14 | var title = "" 15 | 16 | convenience init?(withTitle title: String) { 17 | self.init(JSON: ["title": title]) 18 | } 19 | 20 | override func mapping(map: Map) { 21 | title <- map["title"] 22 | } 23 | } 24 | 25 | class NumberModel: Model { 26 | var number = Int.random(in: 0..<200000) 27 | } 28 | 29 | class SectionTextModel: NumberModel { 30 | var title = "" 31 | var desc = "" 32 | 33 | convenience init(withTitle title: String, desc: String) { 34 | self.init(JSON: ["title": title, "desc": desc])! 35 | } 36 | 37 | override func mapping(map: Map) { 38 | title <- map["title"] 39 | desc <- map["desc"] 40 | } 41 | } 42 | 43 | class SectionImageModel: NumberModel { 44 | var imageUrl: URL? 45 | 46 | convenience init?(withUrl url: String) { 47 | self.init(JSON: ["url": url]) 48 | } 49 | 50 | override func mapping(map: Map) { 51 | imageUrl <- (map["url"], URLTransform()) 52 | } 53 | } 54 | 55 | class HeaderFooterModel: Model { 56 | var title = "" 57 | var footer = "" 58 | var desc = "" 59 | 60 | convenience init(withTitle title: String, desc: String, footer: String) { 61 | self.init(JSON: ["title": title, "desc": desc, "footer": footer])! 62 | } 63 | 64 | override func mapping(map: Map) { 65 | title <- map["title"] 66 | desc <- map["desc"] 67 | footer <- map["footer"] 68 | } 69 | } 70 | 71 | class TabbarModel: Model { 72 | private(set) var title = "" 73 | private(set) var index = 0 74 | private(set) var hex = "" 75 | 76 | convenience init?(withTitle title: String) { 77 | self.init(JSON: ["title": title]) 78 | } 79 | 80 | override func mapping(map: Map) { 81 | title <- map["title"] 82 | index <- map["index"] 83 | hex <- map["hex"] 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Models/MenuModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuModel.swift 3 | // MVVM_Example 4 | // 5 | 6 | import Foundation 7 | import MVVM 8 | import ObjectMapper 9 | 10 | class MenuModel: Model { 11 | var title = "" 12 | var desc = "" 13 | 14 | convenience init(withTitle title: String, desc: String) { 15 | self.init(JSON: ["title": title, "desc": desc])! 16 | } 17 | 18 | override func mapping(map: Map) { 19 | title <- map["title"] 20 | desc <- map["desc"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Models/TransitionContentModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionContentModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 8/3/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import ObjectMapper 12 | 13 | class TransitionContentModel: Model { 14 | var title = "" 15 | var desc = "" 16 | var url = "" 17 | var background = "" 18 | 19 | convenience init(withTitle title: String, desc: String, url: String, withBGColor hexColor: String = "#FFFFFF") { 20 | self.init(JSON: ["title": title, "desc": desc, "url": url, "background": hexColor])! 21 | } 22 | 23 | override func mapping(map: Map) { 24 | title <- map["title"] 25 | desc <- map["desc"] 26 | url <- map["url"] 27 | background <- map["background"] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/BaseWebKitExample/Alert/AlertWebPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertWebPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AlertWebPage: IntroductionPage { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | // Do any additional setup after loading the view. 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/BaseWebKitExample/Alert/ViewModel/AlertWebPageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertWebPageViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import MVVM 10 | import RxCocoa 11 | import WebKit 12 | 13 | class AlertWebPageViewModel: IntroductionPageViewModel { 14 | override func react() { 15 | super.react() 16 | let title = (self.model as? IntroductionModel)?.title ?? "Alert Panel With message" 17 | let html = """ 18 | 19 | 20 | 21 |






22 | 23 |

Click the button bellow to display an alert box.

24 | 25 | 26 | 27 | 32 | 33 | 34 | 35 | """ 36 | rxPageTitle.accept(title) 37 | rxSourceType.accept(WebViewSuorceType.html.rawValue) 38 | rxSource.accept(html) 39 | } 40 | 41 | override func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { 42 | let alert = UIAlertController(title: "runJavaScriptAlertPanelWithMessage", 43 | message: message, 44 | preferredStyle: .alert) 45 | alert.addAction(UIAlertAction(title: "OK", 46 | style: .default, 47 | handler: { _ in 48 | completionHandler() 49 | })) 50 | navigationService.push(to: alert, options: .modal()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/BaseWebKitExample/Authentication/AuthenticationWebPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticationWebPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AuthenticationWebPage: IntroductionPage { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | // Do any additional setup after loading the view. 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/BaseWebKitExample/Authentication/ViewModel/AuthenticationWebViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticationWebViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import MVVM 10 | import RxCocoa 11 | import WebKit 12 | 13 | class AuthenticationWebViewModel: IntroductionPageViewModel { 14 | override func react() { 15 | super.react() 16 | let title = (self.model as? IntroductionModel)?.title ?? "Authentication" 17 | let url = URL(string: "https://jigsaw.w3.org/HTTP/Basic/")! 18 | rxPageTitle.accept(title) 19 | rxURL.accept(url) 20 | } 21 | 22 | override func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 23 | /* 24 | The correct credentials are: 25 | user = guest 26 | password = guest 27 | 28 | You might want to start with the invalid credentials to get a sense of how this code works 29 | */ 30 | let credential = URLCredential(user: "guest", password: "guest", persistence: URLCredential.Persistence.forSession) 31 | challenge.sender?.use(credential, for: challenge) 32 | completionHandler(URLSession.AuthChallengeDisposition.useCredential, credential) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/BaseWebKitExample/ConfirmAlert/ConfirmAlertWebPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfirmAlertWebPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ConfirmAlertWebPage: IntroductionPage { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/BaseWebKitExample/EvaluateJavaScript/EvaluateJavaScriptWebPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EvaluateJavaScriptWebPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Action 11 | import RxSwift 12 | import RxCocoa 13 | 14 | class EvaluateJavaScriptWebPage: IntroductionPage { 15 | let addBtn = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) 16 | lazy var addAction: Action = { 17 | return Action { .just(self.add()) } 18 | }() 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | enableBackButton = true 23 | // Do any additional setup after loading the view. 24 | navigationItem.rightBarButtonItem = addBtn 25 | } 26 | 27 | override func bindViewAndViewModel() { 28 | super.bindViewAndViewModel() 29 | addBtn.rx.bind(to: self.addAction, input: ()) 30 | } 31 | 32 | func add() { 33 | self.evaluateJavaScript("presentAlert()") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/BaseWebKitExample/EvaluateJavaScript/ViewModel/EvaluateJavaScriptWebViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EvaluateJavaScriptWebViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import MVVM 10 | import RxCocoa 11 | import WebKit 12 | 13 | class EvaluateJavaScriptWebViewModel: IntroductionPageViewModel { 14 | override func react() { 15 | super.react() 16 | let title = (self.model as? IntroductionModel)?.title ?? "Evaluate JavaScript" 17 | let html = """ 18 | 19 | 20 | 21 | Invoke Javascript function 22 | 23 | 24 | 25 |

Invoke Javascript function

26 |

Just Press 'Add' at top right corner.

27 |

After that, pay attention to your console.

28 | 29 | 34 | 35 | 36 | 37 | """ 38 | rxPageTitle.accept(title) 39 | rxSourceType.accept(WebViewSuorceType.html.rawValue) 40 | rxSource.accept(html) 41 | } 42 | 43 | override func webView(_ webView: WKWebView, evaluateJavaScript: (event: Any?, error: Error?)?) { 44 | if let event = evaluateJavaScript?.event as? String { 45 | let alert = UIAlertController(title: "Evaluate Java Script", message: event, preferredStyle: .alert) 46 | alert.addAction(UIAlertAction(title: "Ok", style: .cancel, handler: nil)) 47 | navigationService.push(to: alert, options: .modal()) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/BaseWebKitExample/FailNavigation/FailNavigationWebPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FailNavigationWebPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FailNavigationWebPage: IntroductionPage { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | // Do any additional setup after loading the view. 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/BaseWebKitExample/FailNavigation/ViewModel/FailNavigationWebViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FailNavigationWebViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import MVVM 10 | import RxCocoa 11 | import WebKit 12 | 13 | class FailNavigationWebViewModel: IntroductionPageViewModel { 14 | override func react() { 15 | super.react() 16 | let title = (self.model as? IntroductionModel)?.title ?? "Fail Provisional Navigation" 17 | let url = URL(string: "https://thiswebsiteisnotexisting.com")! 18 | rxPageTitle.accept(title) 19 | rxURL.accept(url) 20 | } 21 | 22 | override func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { 23 | let alert = UIAlertController(title: "FailProvisionalNavigation", message: error.localizedDescription, preferredStyle: .alert) 24 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 25 | navigationService.push(to: alert, options: .modal()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/BaseWebKitExample/UserContentController/ViewModel/WebKitExamplePageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebKitExamplePageViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 22/04/2021. 6 | // Copyright © 2021 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import WebKit 13 | 14 | class WebKitExamplePageViewModel: BaseWebViewModel, WKScriptMessageHandler { 15 | let rxPageTitle = BehaviorRelay(value: "") 16 | override func react() { 17 | super.react() 18 | let title = (self.model as? MenuModel)?.title ?? "Confirm Alert" 19 | 20 | let html = """ 21 | 22 | 23 | 24 |






25 |

Click the button bellow to send a your message.

26 | 27 | 28 | 29 |

30 | 31 | 37 | 38 | 39 | 40 | """ 41 | rxPageTitle.accept(title) 42 | rxSourceType.accept(WebViewSuorceType.html.rawValue) 43 | rxSource.accept(html) 44 | } 45 | 46 | override func webView(_ webView: WKWebView, estimatedProgress: Double) { 47 | self.rxEstimatedProgress.accept(estimatedProgress) 48 | print("DEBUG: estimatedProgress \(estimatedProgress)") 49 | } 50 | 51 | func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { 52 | let alert = UIAlertController(title: "Evaluate Java Script", 53 | message: (message.body as? String) ?? "", 54 | preferredStyle: .alert) 55 | 56 | alert.addAction(UIAlertAction(title: "Ok", 57 | style: .cancel, 58 | handler: nil)) 59 | 60 | navigationService.push(to: alert, options: .modal()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/BaseWebKitExample/UserContentController/WebKitExamplePage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HandleUserContentControllerWebPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 22/04/2021. 6 | // Copyright © 2021 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | 12 | class WebKitExamplePage: BaseWebView { 13 | override func bindViewAndViewModel() { 14 | super.bindViewAndViewModel() 15 | guard let viewModel = self.viewModel as? WebKitExamplePageViewModel else { 16 | return 17 | } 18 | viewModel.rxSource ~> self.wkWebView.rx.sourceHtml => disposeBag 19 | viewModel.rxPageTitle ~> rx.title => disposeBag 20 | /// Simple solution is Adopt a WKScriptMessageHandler in View Model 21 | self.wkWebView.configuration.userContentController.add(viewModel, name: "RxWebKitScriptMessageHandler") 22 | /// You can user `configuration.userContentController.rx.scriptMessage(forName:)` instead WKScriptMessageHandler 23 | /// You can received your message with `subcriptMessage(forName: )`. 24 | /* 25 | self.wkWebView.configuration.userContentController.rx.scriptMessage(forName: "RxWebKitScriptMessageHandler").bind(onNext: { message in 26 | let alert = UIAlertController(title: "Evaluate Java Script", 27 | message: (message.body as? String) ?? "", 28 | preferredStyle: .alert) 29 | 30 | alert.addAction(UIAlertAction(title: "Ok", 31 | style: .cancel, 32 | handler: nil)) 33 | 34 | self.navigationService.push(to: alert, options: .modal()) 35 | 36 | }) 37 | */ 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/BaseWebKitExample/UserContentController/WebKitExamplePage.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/BaseWebKitExample/WebKitExamplesPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebKitExamplesPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class WebKitExamplesPage: TableOfContentsPage { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | // Do any additional setup after loading the view. 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/AuthenticationView/AuthenticationPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticationPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 6/1/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | 12 | class AuthenticationPage: BasePage { 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view. 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/AuthenticationView/AuthenticationPage.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/CustomControl/CustomControlPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomControlPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 6/2/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | 12 | class CustomControlPage: BasePage { 13 | let segmentedView = SegmentedView(withTitles: ["Tab 1", "Tab 2", "Tab 3"]) 14 | let label = UILabel() 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | } 19 | 20 | override func initialize() { 21 | super.initialize() 22 | enableBackButton = true 23 | view.addSubview(segmentedView) 24 | segmentedView.autoPinEdge(toSuperviewSafeArea: .top) 25 | segmentedView.autoPinEdge(toSuperviewEdge: .leading) 26 | segmentedView.autoPinEdge(toSuperviewEdge: .trailing) 27 | view.addSubview(label) 28 | label.autoPinEdge(.top, to: .bottom, of: segmentedView, withOffset: 50) 29 | label.autoAlignAxis(toSuperviewAxis: .vertical) 30 | } 31 | 32 | override func bindViewAndViewModel() { 33 | guard let viewModel = viewModel as? CustomControlViewPageModel else { 34 | return 35 | } 36 | viewModel.rxPageTitle ~> rx.title => disposeBag 37 | viewModel.rxSelectedIndex <~> segmentedView.rx.selectedIndex => disposeBag 38 | viewModel.rxSelectedText ~> label.rx.text => disposeBag 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/CustomControl/CustomControlPage.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/CustomControl/ViewModel/CustomControlViewPageModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomControlViewPageModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 6/2/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | import Action 14 | 15 | class CustomControlViewPageModel: BaseViewModel { 16 | let rxPageTitle = BehaviorRelay(value: "") 17 | let rxSelectedIndex = BehaviorRelay(value: 0) 18 | let rxSelectedText = BehaviorRelay(value: nil) 19 | 20 | override func react() { 21 | guard let model = self.model as? MenuModel else { 22 | return 23 | } 24 | rxPageTitle.accept(model.title) 25 | rxSelectedIndex.map { "You have selected Tab \($0 + 1)" } ~> rxSelectedText => disposeBag 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/DataBindingExamplesPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataBindingExamplesPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | import Action 14 | 15 | class DataBindingExamplesPage: TableOfContentsPage { 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | // Do any additional setup after loading the view. 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/PageController/UIPageExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIPageExample.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 9/4/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | 12 | class UIPageExample: BaseUIPage { 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | // Do any additional setup after loading the view. 17 | } 18 | 19 | override func initialize() { 20 | super.initialize() 21 | enableBackButton = true 22 | } 23 | 24 | override func bindViewAndViewModel() { 25 | super.bindViewAndViewModel() 26 | guard let viewModel = self.viewModel as? UIPageExampleViewModel else { 27 | return 28 | } 29 | viewModel.rxPageTitle ~> self.rx.title => disposeBag 30 | } 31 | 32 | override func getItemSource() -> ReactiveCollection? { 33 | guard let viewModel = viewModel as? UIPageExampleViewModel else { 34 | return nil 35 | } 36 | return viewModel.itemsSource 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/PageController/UIPageExample.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/PageController/ViewModel/UIPageExampleViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIPageExampleViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 9/4/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | 14 | class UIPageExampleViewModel: BaseUIPageViewModel { 15 | let rxPageTitle = BehaviorRelay(value: "") 16 | 17 | override func react() { 18 | super.react() 19 | isInfinity = false 20 | setupPage() 21 | guard let model = self.model as? MenuModel else { 22 | return 23 | } 24 | rxPageTitle.accept(model.title) 25 | } 26 | 27 | func setupPage() { 28 | let model0 = TabbarModel(JSON: ["title": "Vivid Sky Blue", "hex": "4CC9F0"]) 29 | let vc0 = TabPage(viewModel: TabPageViewModel(model: model0)) 30 | let page0 = UIPageItem(model: model0, viewController: vc0) 31 | 32 | let model1 = TabbarModel(JSON: ["title": "Skype Blue Crayola", "hex": "75DBFA"]) 33 | let vc1 = TabPage(viewModel: TabPageViewModel(model: model1)) 34 | let page1 = UIPageItem(model: self.model, viewController: vc1) 35 | 36 | let model2 = TabbarModel(JSON: ["title": "Vivid Sky Blue", "hex": "4CC9F0"]) 37 | let vc2 = TabPage(viewModel: TabPageViewModel(model: model2)) 38 | let page2 = UIPageItem(model: model2, viewController: vc2) 39 | 40 | let model3 = TabbarModel(JSON: ["title": "Skype Blue Crayola", "hex": "75DBFA"]) 41 | let vc3 = TabPage(viewModel: TabPageViewModel(model: model3)) 42 | let page3 = UIPageItem(model: model3, viewController: vc3) 43 | 44 | let model4 = TabbarModel(JSON: ["title": "Vivid Sky Blue", "hex": "4CC9F0"]) 45 | let vc4 = TabPage(viewModel: TabPageViewModel(model: model4)) 46 | let page4 = UIPageItem(model: model4, viewController: vc4) 47 | itemsSource.append([page0, page1, page2, page3, page4]) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/TabbarController/TabPage/TabPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 9/4/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | 12 | class TabPage: BasePage { 13 | @IBOutlet private weak var hexLb: UILabel! 14 | @IBOutlet private weak var nameLb: UILabel! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | // Do any additional setup after loading the view. 20 | } 21 | 22 | override func initialize() { 23 | super.initialize() 24 | enableBackButton = true 25 | } 26 | 27 | override func bindViewAndViewModel() { 28 | super.bindViewAndViewModel() 29 | guard let viewModel = self.viewModel as? TabPageViewModel else { 30 | return 31 | } 32 | viewModel.rxTitle ~> self.rx.title => disposeBag 33 | viewModel.rxBackgroundHex.subscribe(onNext: { value in 34 | if let hex = value { 35 | self.view.backgroundColor = UIColor(hexString: hex) 36 | } 37 | }) => disposeBag 38 | viewModel.rxName ~> nameLb.rx.text => disposeBag 39 | viewModel.rxBackgroundHex ~> hexLb.rx.text => disposeBag 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/TabbarController/TabPage/TabPageViewModel/TabPageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabPageViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 9/4/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | import Action 14 | 15 | class TabPageViewModel: BaseViewModel { 16 | let rxTitle = BehaviorRelay(value: "") 17 | let rxName = BehaviorRelay(value: nil) 18 | let rxBackgroundHex = BehaviorRelay(value: "89DDF7") 19 | override func react() { 20 | super.react() 21 | guard let model = self.model as? TabbarModel else { 22 | return 23 | } 24 | rxTitle.accept(model.title) 25 | rxName.accept(model.title) 26 | if !model.hex.isEmpty { 27 | rxBackgroundHex.accept(model.hex) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/TabbarController/TabbarView/TabbarViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabbarViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 8/31/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class TabbarViewModel: BaseViewModel {} 15 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/TabbarController/TimelinePage/Cells/Activity/ActivityCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityCell.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 9/17/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | 14 | class ActivityCell: BaseTableCell { 15 | @IBOutlet private weak var tweetsLb: UILabel! 16 | @IBOutlet private weak var followingLb: UILabel! 17 | @IBOutlet private weak var followerLb: UILabel! 18 | @IBOutlet private weak var likesLb: UILabel! 19 | 20 | override func awakeFromNib() { 21 | super.awakeFromNib() 22 | // Initialization code 23 | } 24 | override func bindViewAndViewModel() { 25 | super.bindViewAndViewModel() 26 | guard let viewModel = self.viewModel as? ActivityCellViewModel else { 27 | return 28 | } 29 | viewModel.rxTweets ~> tweetsLb.rx.text => disposeBag 30 | viewModel.rxFollowing ~> followingLb.rx.text => disposeBag 31 | viewModel.rxFollower ~> followerLb.rx.text => disposeBag 32 | viewModel.rxLikes ~> likesLb.rx.text => disposeBag 33 | } 34 | 35 | override func initialize() { 36 | super.initialize() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/TabbarController/TimelinePage/Cells/Activity/CellViewModel/ActivityCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityCellViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 9/17/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | import Action 14 | 15 | class ActivityCellViewModel: BaseCellViewModel { 16 | let rxTweets = BehaviorRelay(value: "") 17 | let rxFollowing = BehaviorRelay(value: "") 18 | let rxFollower = BehaviorRelay(value: "") 19 | let rxLikes = BehaviorRelay(value: "") 20 | 21 | override func react() { 22 | super.react() 23 | guard let model = self.model as? ActivityModel else { 24 | return 25 | } 26 | rxTweets.accept("\(model.tweets)") 27 | rxFollowing.accept("\(model.following)") 28 | rxFollower.accept("\(model.follower)") 29 | rxLikes.accept("\(model.likes)") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/TabbarController/TimelinePage/DummyModel/ActivityModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 9/17/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import MVVM 10 | import ObjectMapper 11 | import SwiftyJSON 12 | 13 | class ActivityModel: Model { 14 | var title = "" 15 | var tweets: Int = 0 16 | var following: Int = 0 17 | var follower: Int = 0 18 | var likes: Int = 0 19 | 20 | convenience init() { 21 | let dummyData = ["title": "Activity", "desc": "Description"] 22 | self.init(JSON: dummyData)! 23 | } 24 | 25 | override func mapping(map: Map) { 26 | title <- map["title"] 27 | tweets <- map["tweets"] 28 | following <- map["following"] 29 | follower <- map["follower"] 30 | likes <- map["likes"] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/TabbarController/TimelinePage/DummyModel/UserInfoModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserInfoModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 9/18/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import ObjectMapper 12 | 13 | class UserInfoModel: Model { 14 | var displayName: String = "" 15 | var userId: Int = 0 16 | var avatar: String = "" 17 | var username: Int = 0 18 | 19 | override func mapping(map: Map) { 20 | username <- map["username"] 21 | userId <- map["id"] 22 | displayName <- map["displayName"] 23 | avatar <- map["avatar"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/TabbarController/TimelinePage/ViewModel/TimelinePageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimelinePageViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by tienpm on 9/16/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | import Action 14 | 15 | class TimelinePageViewModel: BaseListViewModel { 16 | let alertService: IAlertService = DependencyManager.shared.getService() 17 | var networkService: NetworkService? 18 | var tmpBag: DisposeBag? 19 | let rxTille = BehaviorRelay(value: "") 20 | lazy var getDataAction: Action = { 21 | return Action { .just(self.getData()) } 22 | }() 23 | lazy var loadMoreAction: Action = { 24 | return Action { .just(self.loadMore()) } 25 | }() 26 | 27 | override func react() { 28 | super.react() 29 | guard let model = self.model as? TabbarModel else { 30 | return 31 | } 32 | rxTille.accept(model.title) 33 | networkService = DependencyManager.shared.getService() 34 | } 35 | 36 | private func getData() { 37 | self.networkService?.loadTimeline(withPage: self.page, withLimit: self.limit) 38 | .map(prepareSources) 39 | .subscribe(onSuccess: {[weak self] results in 40 | if let data = results { 41 | self?.itemsSource.append(data, animated: false) 42 | } 43 | }, onFailure: { _ in 44 | }) => tmpBag 45 | } 46 | 47 | private func loadMore() { 48 | print("DEBUG: Loading more") 49 | } 50 | 51 | private func prepareSources(_ response: TimelineResponseModel?) -> [BaseCellViewModel]? { 52 | guard let response = response else { 53 | return [] 54 | } 55 | if response.stat == .badRequest { 56 | alertService.presentOkayAlert(title: "Error", 57 | message: "\(response.message)\nPlease be sure to provide your own SECRET key from MTLAB.") 58 | } 59 | return response.timelines 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/TabbarController/ViewModel/TabbarControllerViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabbarControllerViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 8/31/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class TabbarControllerViewModel: BaseViewModel { 15 | let rxSelectedIndex = BehaviorRelay(value: 0) 16 | override func react() { 17 | super.react() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/DataBindingExamples/ValidatePage/ViewModel/ValidatePageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValidatePageViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 6/1/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | import Action 14 | 15 | class ValidatePageViewModel: BaseViewModel { 16 | let rxPageTitle = BehaviorRelay(value: "") 17 | let rxHelloText = BehaviorRelay(value: nil) 18 | let rxEmail = BehaviorRelay(value: nil) 19 | let rxPass = BehaviorRelay(value: nil) 20 | let rxSubmitButtonEnabled = BehaviorRelay(value: false) 21 | lazy var submitAction: Action = { 22 | return Action(enabledIf: self.rxSubmitButtonEnabled.asObservable()) { 23 | let email = self.rxEmail.value ?? "" 24 | let pass = self.rxPass.value ?? "" 25 | self.alertService.presentOkayAlert(title: "Submit Button Clicked!", 26 | message: "You have just clicked on submit button. Here are your credentials:\nEmail: \(email)\nPass: \(pass)") 27 | return .just(()) 28 | } 29 | }() 30 | 31 | let alertService: IAlertService = DependencyManager.shared.getService() 32 | 33 | override func react() { 34 | guard let model = self.model as? MenuModel else { 35 | return 36 | } 37 | rxPageTitle.accept(model.title) 38 | Observable.combineLatest(rxEmail, rxPass) { email, pass -> Bool in 39 | return !email.isNilOrEmpty && !pass.isNilOrEmpty 40 | } ~> rxSubmitButtonEnabled => disposeBag // One-way binding is donated by ~> 41 | rxEmail.filter { $0 != nil }.map { "Hello, \($0!)" } ~> rxHelloText => disposeBag // One-way binding is donated by ~> 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/Introduction/ViewModel/IntroductionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntroductionViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import MVVM 10 | import RxCocoa 11 | import WebKit 12 | class IntroductionPageViewModel: BaseWebViewModel { 13 | let rxPageTitle = BehaviorRelay(value: "") 14 | let rxURL = BehaviorRelay(value: nil) 15 | 16 | override func react() { 17 | super.react() 18 | let title = (self.model as? IntroductionModel)?.title ?? "Table Of Contents" 19 | let url = URL(string: "https://tienpm-0557.github.io/mvvm")! 20 | 21 | rxPageTitle.accept(title) 22 | rxURL.accept(url) 23 | } 24 | 25 | override func webView(_ webView: WKWebView, estimatedProgress: Double) { 26 | self.rxEstimatedProgress.accept(estimatedProgress) 27 | print("DEBUG: estimatedProgress \(estimatedProgress)") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/LocalizationExample/LocalizationPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizationPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 8/4/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | 14 | class LocalizationPage: BasePage { 15 | @IBOutlet private var localizeLb: UILabel! 16 | @IBOutlet private var englishBtn: UIButton! 17 | @IBOutlet private var japaneseBtn: UIButton! 18 | 19 | override func initialize() { 20 | super.initialize() 21 | self.enableBackButton = true 22 | localizeLb.text = LocalizedStringMessage.strTestMessage.localized 23 | } 24 | 25 | override func bindViewAndViewModel() { 26 | super.bindViewAndViewModel() 27 | guard let viewModel = self.viewModel as? LocalizationPageViewModel else { 28 | return 29 | } 30 | viewModel.rxPageTitle ~> rx.title => disposeBag 31 | englishBtn.rx.bind(to: viewModel.rxEnglishAction, input: ()) 32 | japaneseBtn.rx.bind(to: viewModel.rxJPAction, input: ()) 33 | } 34 | 35 | override func onUpdateLocalize() { 36 | super.onUpdateLocalize() 37 | print("DEBUG: Page onUpdateLocalize") 38 | localizeLb.text = LocalizedStringMessage.strTestMessage.localized 39 | self.navigationItem.backBarButtonItem?.title = "" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/LocalizationExample/ViewModel/LocalizationPageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizationPageViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 8/4/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import MVVM 10 | import Action 11 | import RxSwift 12 | import RxCocoa 13 | 14 | class LocalizationPageViewModel: BaseViewModel { 15 | private var localizationService: LocalizeService? 16 | 17 | var rxPageTitle = BehaviorRelay(value: "") 18 | 19 | lazy var rxJPAction: Action = { 20 | return Action { .just(self.selectJPLocale()) } 21 | }() 22 | 23 | lazy var rxEnglishAction: Action = { 24 | return Action { .just(self.selectedEngLocale()) } 25 | }() 26 | 27 | override func react() { 28 | super.react() 29 | localizationService = DependencyManager.shared.getService() 30 | rxPageTitle.accept(LocalizedStringConfigs.strLocalizePageTitle.localized) 31 | } 32 | 33 | private func selectJPLocale() { 34 | localizationService?.setCurrentLocale("ja") 35 | } 36 | 37 | private func selectedEngLocale() { 38 | localizationService?.setCurrentLocale("en") 39 | } 40 | 41 | override func onUpdateLocalize() { 42 | super.onUpdateLocalize() 43 | print("DEBUG: PageModel onUpdateLocalize") 44 | rxPageTitle.accept(LocalizedStringConfigs.strLocalizePageTitle.localized) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/CollectionPage/Cell/CellViewModel/SimpleCollectionViewDellModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleCollectionViewDellModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/17/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class SimpleCollectionViewDellModel: BaseCellViewModel { 15 | let rxTitle = BehaviorRelay(value: nil) 16 | let rxDesc = BehaviorRelay(value: nil) 17 | 18 | override func react() { 19 | guard let model = model as? SectionTextModel else { 20 | return 21 | } 22 | rxTitle.accept(model.title) 23 | rxDesc.accept(model.desc) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/CollectionPage/Cell/SimpleCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleCollectionViewCell.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/17/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | 12 | class SimpleCollectionViewCell: BaseCollectionCell { 13 | let titleLbl = UILabel() 14 | 15 | override func initialize() { 16 | cornerRadius = 5 17 | backgroundColor = .black 18 | titleLbl.textColor = .white 19 | titleLbl.numberOfLines = 0 20 | titleLbl.font = Font.system.bold(withSize: 17) 21 | let layout = StackLayout().direction(.vertical).children([ 22 | titleLbl 23 | ]) 24 | contentView.addSubview(layout) 25 | layout.autoPinEdgesToSuperviewEdges(with: .all(5)) 26 | } 27 | 28 | override func bindViewAndViewModel() { 29 | super.bindViewAndViewModel() 30 | guard let viewModel = viewModel as? SimpleCollectionViewDellModel else { 31 | return 32 | } 33 | viewModel.rxTitle ~> titleLbl.rx.text => disposeBag 34 | } 35 | 36 | override class func getSize(withItem data: Any?) -> CGSize? { 37 | let screenSize = UIScreen.main.bounds 38 | return CGSize(width: (screenSize.width - 30) / 2, height: 60) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/CollectionPage/Header/HeaderCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderCollectionView.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/17/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | 14 | class HeaderCollectionView: BaseHeaderCollectionView { 15 | @IBOutlet private weak var titleLbl: UILabel! 16 | 17 | override class func headerSize(withItem item: BaseViewModel) -> CGSize { 18 | let screenSize = UIScreen.main.bounds 19 | return CGSize(width: screenSize.width, height: 50) 20 | } 21 | 22 | override func bindViewAndViewModel() { 23 | guard let viewModel = viewModel as? SectionHeaderViewViewModel else { 24 | return 25 | } 26 | viewModel.rxTitle ~> titleLbl.rx.text => disposeBag 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/CollectionPage/Header/ViewModel/HeaderCollectionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderCollectionViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/17/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import MVVM 10 | import RxSwift 11 | import RxCocoa 12 | import Action 13 | 14 | class HeaderCollectionViewModel: BaseViewModel { 15 | let rxTitle = BehaviorRelay(value: nil) 16 | let rxFooter = BehaviorRelay(value: nil) 17 | let rxDesc = BehaviorRelay(value: nil) 18 | 19 | override func react() { 20 | guard let model = model as? HeaderFooterModel else { 21 | return 22 | } 23 | rxTitle.accept(model.title) 24 | rxFooter.accept(model.footer) 25 | rxDesc.accept(model.desc) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/CollectionPage/SimpleCollectionPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleCollectionPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/17/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class SimpleCollectionPage: BaseCollectionPage { 15 | let addBtn = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | // Do any additional setup after loading the view. 20 | navigationItem.rightBarButtonItem = addBtn 21 | } 22 | 23 | override func bindViewAndViewModel() { 24 | super.bindViewAndViewModel() 25 | guard let viewModel = viewModel as? CollectionPageViewModel else { 26 | return 27 | } 28 | 29 | viewModel.rxPageTitle ~> self.rx.title => disposeBag 30 | addBtn.rx.bind(to: viewModel.addAction, input: ()) 31 | } 32 | 33 | override func getItemSource() -> RxCollection? { 34 | guard let viewModel = viewModel as? CollectionPageViewModel else { 35 | return nil 36 | } 37 | return viewModel.itemsSource 38 | } 39 | 40 | override func registerNibWithColletion(_ collectionView: UICollectionView) { 41 | collectionView.register(collectionViewCell: SimpleCollectionViewCell.self) 42 | collectionView.register(headerType: HeaderCollectionView.self) 43 | } 44 | 45 | override func cellIdentifier(_ cellViewModel: Any, _ isClass: Bool = false) -> String { 46 | return isClass ? SimpleCollectionViewCell.className : SimpleCollectionViewCell.identifier 47 | } 48 | 49 | override func headerIdentifier(_ headerViewModel: Any, _ returnClassName: Bool = false) -> String? { 50 | return returnClassName ? HeaderCollectionView.className : HeaderCollectionView.identifier 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/ContactPage/ContactCell/CellViewModel/ContactCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactCellViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/22/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class ContactCellViewModel: BaseCellViewModel { 15 | let rxName = BehaviorRelay(value: nil) 16 | let rxPhone = BehaviorRelay(value: nil) 17 | 18 | override func react() { 19 | super.react() 20 | modelChanged() 21 | } 22 | 23 | override func modelChanged() { 24 | super.modelChanged() 25 | guard let model = self.model as? ContactModel else { 26 | return 27 | } 28 | rxName.accept(model.name) 29 | rxPhone.accept(model.phone) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/ContactPage/ContactCell/ContactTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactTableViewCell.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/22/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import Action 12 | import RxCocoa 13 | import RxSwift 14 | 15 | class ContactTableViewCell: BaseTableCell { 16 | @IBOutlet private weak var avatarIv: UIImageView! 17 | @IBOutlet private weak var nameLbl: UILabel! 18 | @IBOutlet private weak var phoneLbl: UILabel! 19 | 20 | override func awakeFromNib() { 21 | super.awakeFromNib() 22 | avatarIv.image = UIImage(named: "default-contact") 23 | nameLbl.numberOfLines = 0 24 | nameLbl.font = Font.system.bold(withSize: 17) 25 | phoneLbl.numberOfLines = 0 26 | phoneLbl.font = Font.system.normal(withSize: 15) 27 | } 28 | 29 | override func initialize() { 30 | super.initialize() 31 | } 32 | 33 | open override class func height(withItem item: BaseCellViewModel) -> CGFloat { 34 | return 70.0 35 | } 36 | 37 | override func bindViewAndViewModel() { 38 | guard let viewModel = viewModel as? ContactCellViewModel else { 39 | return 40 | } 41 | viewModel.rxName ~> nameLbl.rx.text => disposeBag 42 | viewModel.rxPhone ~> phoneLbl.rx.text => disposeBag 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/ContactPage/ContactListPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactListPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/22/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | 12 | class ContactListPage: BaseListPage { 13 | let addBtn = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | enableBackButton = true 18 | navigationItem.rightBarButtonItem = addBtn 19 | // Do any additional setup after loading the view. 20 | 21 | } 22 | 23 | override func setupTableView(_ tableView: UITableView) { 24 | super.setupTableView(tableView) 25 | tableView.separatorStyle = .singleLine 26 | tableView.estimatedRowHeight = 70 27 | tableView.register(cellType: ContactTableViewCell.self) 28 | } 29 | 30 | override func bindViewAndViewModel() { 31 | super.bindViewAndViewModel() 32 | guard let viewModel = self.viewModel as? ContactListPageViewModel else { 33 | return 34 | } 35 | addBtn.rx.bind(to: viewModel.addAction, input: ()) 36 | } 37 | 38 | override func cellIdentifier(_ cellViewModel: Any, _ returnClassName: Bool = false) -> String { 39 | return returnClassName ? ContactTableViewCell.className : ContactTableViewCell.identifier 40 | } 41 | 42 | override func getItemSource() -> RxCollection? { 43 | guard let viewModel = viewModel as? ContactListPageViewModel else { 44 | return nil 45 | } 46 | return viewModel.itemsSource 47 | } 48 | 49 | override func destroy() { 50 | super.destroy() 51 | viewModel?.destroy() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/ContactPage/EditContactPage/ContactEditPage.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/ContactPage/EditContactPage/ViewModel/ContactEditPageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactEditPageViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/22/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import Action 12 | import RxSwift 13 | import RxCocoa 14 | 15 | class ContactEditPageViewModel: BaseViewModel { 16 | lazy var cancelAction: Action = { 17 | return Action { .just(self.navigationService.pop(with: PopOptions(popType: .dismissPopup)))} 18 | }() 19 | 20 | lazy var saveAction: Action = { 21 | return Action(enabledIf: self.rxSaveEnabled.asObservable()) { 22 | return self.save() 23 | } 24 | }() 25 | 26 | let rxName = BehaviorRelay(value: nil) 27 | let rxPhone = BehaviorRelay(value: nil) 28 | let rxSaveEnabled = BehaviorRelay(value: false) 29 | 30 | var isFirstEnabled: Observable { 31 | return Observable.combineLatest(rxName, rxPhone) { name, phone -> Bool in 32 | return !name.isNilOrEmpty && !phone.isNilOrEmpty 33 | } 34 | } 35 | 36 | override func react() { 37 | super.react() 38 | Observable.combineLatest(rxName, rxPhone) { name, phone -> Bool in 39 | return !name.isNilOrEmpty && !phone.isNilOrEmpty 40 | } ~> rxSaveEnabled => disposeBag 41 | 42 | /// For Edit contact 43 | guard let model = self.model as? ContactModel else { 44 | return 45 | } 46 | rxName.accept(model.name) 47 | rxPhone.accept(model.phone) 48 | } 49 | 50 | func save() -> Observable { 51 | let contact = ContactModel() 52 | contact.name = rxName.value ?? "" 53 | contact.phone = rxPhone.value ?? "" 54 | navigationService.pop(with: PopOptions(popType: .dismissPopup)) 55 | return .just(contact) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/DynamicCollectionPage/Cell/CellViewModel/DynamicCollectionCellModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicCollectionCellModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/17/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | class DynamicCollectionCellModel: BaseCellViewModel { 14 | let rxTitle = BehaviorRelay(value: nil) 15 | let rxDesc = BehaviorRelay(value: nil) 16 | 17 | override func react() { 18 | guard let model = model as? SectionTextModel else { 19 | return 20 | } 21 | rxTitle.accept(model.title) 22 | rxDesc.accept(model.desc) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/DynamicCollectionPage/Cell/DynamicCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicCollectionViewCell.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/17/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | 14 | class DynamicCollectionViewCell: BaseCollectionCell { 15 | @IBOutlet private weak var titleLbl: UILabel! 16 | @IBOutlet private weak var descLbl: UILabel! 17 | 18 | override func awakeFromNib() { 19 | super.awakeFromNib() 20 | // Initialization code 21 | } 22 | 23 | override class func getSize(withItem data: Any?) -> CGSize? { 24 | let screenSize = UIScreen.main.bounds 25 | return CGSize(width: screenSize.width - 20, height: 100) 26 | } 27 | 28 | override func bindViewAndViewModel() { 29 | guard let viewModel = viewModel as? DynamicCollectionCellModel else { 30 | return 31 | } 32 | viewModel.rxTitle ~> titleLbl.rx.text => disposeBag 33 | viewModel.rxDesc ~> descLbl.rx.text => disposeBag 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/DynamicCollectionPage/DynamicCollectionPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicCollectionPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/17/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | import Action 14 | 15 | class DynamicCollectionPage: BaseCollectionPage { 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | // Do any additional setup after loading the view. 19 | } 20 | 21 | override func initialize() { 22 | super.initialize() 23 | allowLoadmoreData = true 24 | } 25 | 26 | override func bindViewAndViewModel() { 27 | super.bindViewAndViewModel() 28 | guard let viewModel = self.viewModel as? DynamicCollectionPageViewModel else { 29 | return 30 | } 31 | viewModel.rxPageTitle ~> rx.title => disposeBag 32 | viewModel.rxState ~> self.rx.state => disposeBag 33 | } 34 | 35 | override func registerNibWithColletion(_ collectionView: UICollectionView) { 36 | collectionView.register(collectionViewCell: DynamicCollectionViewCell.self) 37 | } 38 | 39 | override func cellIdentifier(_ cellViewModel: Any, _ isClass: Bool = false) -> String { 40 | return isClass ? DynamicCollectionViewCell.className : DynamicCollectionViewCell.identifier 41 | } 42 | 43 | override func getItemSource() -> RxCollection? { 44 | guard let viewModel = viewModel as? DynamicCollectionPageViewModel else { 45 | return nil 46 | } 47 | return viewModel.itemsSource 48 | } 49 | 50 | override func destroy() { 51 | super.destroy() 52 | viewModel?.destroy() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/DynamicListPage/DynamicListPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicListPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/16/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | import Action 14 | 15 | class DynamicListPage: BaseListPage { 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | // Do any additional setup after loading the view. 21 | enableBackButton = true 22 | } 23 | 24 | override func setupTableView(_ tableView: UITableView) { 25 | super.setupTableView(tableView) 26 | /// separatorStyle disabled in BaseListPage 27 | tableView.separatorStyle = .singleLine 28 | tableView.estimatedRowHeight = 200 29 | tableView.register(cellType: SimpleTableCell.self) 30 | allowLoadmoreData = true 31 | } 32 | 33 | override func bindViewAndViewModel() { 34 | super.bindViewAndViewModel() 35 | guard let viewModel = self.viewModel as? DynamicListPageViewModel else { 36 | return 37 | } 38 | viewModel.rxPageTitle ~> rx.title => disposeBag 39 | viewModel.rxState ~> self.rx.state => disposeBag 40 | } 41 | 42 | override func cellIdentifier(_ cellViewModel: Any, _ returnClassName: Bool = false) -> String { 43 | return returnClassName ? SimpleTableCell.className : SimpleTableCell.identifier 44 | } 45 | 46 | override func getItemSource() -> RxCollection? { 47 | guard let viewModel = viewModel as? DynamicListPageViewModel else { 48 | return nil 49 | } 50 | return viewModel.itemsSource 51 | } 52 | 53 | override func destroy() { 54 | super.destroy() 55 | viewModel?.destroy() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/DynamicListPage/ViewModel/DynamicListPageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicListPageViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/16/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class DynamicListPageViewModel: BaseListViewModel { 15 | let rxPageTitle = BehaviorRelay(value: "") 16 | 17 | override func react() { 18 | super.react() 19 | fetchData() 20 | let title = (self.model as? MenuModel)?.title ?? "Dynamic UITableView" 21 | rxPageTitle.accept(title) 22 | rxState.subscribe(onNext: { state in 23 | self.rxPageTitle.accept(title + " \(state)") 24 | }) => disposeBag 25 | } 26 | 27 | func fetchData() { 28 | self.rxState.accept(.loadingData) 29 | add() 30 | self.rxState.accept(.normal) 31 | } 32 | 33 | override func loadMoreContent() { 34 | DispatchQueue.main.asyncAfter(deadline: .now()) { 35 | self.rxState.accept(.loadingMore) 36 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 37 | self.add() 38 | self.rxState.accept(.normal) 39 | } 40 | } 41 | } 42 | 43 | private func add() { 44 | var reuslt = [SimpleListPageCellViewModel]() 45 | for _ in 1...10 { 46 | let number = Int.random(in: 1000...10000) 47 | let title = "This is your random number: \(number)" 48 | let cvm = SimpleListPageCellViewModel(model: SimpleModel(withTitle: title)) 49 | reuslt.append(cvm) 50 | } 51 | itemsSource.append(reuslt, animated: false) 52 | } 53 | 54 | override func selectedItemDidChange(_ cellViewModel: BaseCellViewModel, _ indexPath: IndexPath) { 55 | /// navigationService 56 | /// Use navigation service for push view controller into Navigation. 57 | /// In case View is Root. Navigation service will present viewcontroller. 58 | print("DEBUG: selectedItemDidChange \(cellViewModel.indexPath?.row ?? -1)") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/FlickrSearchPage/Cell/CellViewModel/FlickrCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlickrCellViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/23/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | class FlickrCellViewModel: BaseCellViewModel { 14 | let rxImage = BehaviorRelay(value: NetworkImage()) 15 | let rxTitle = BehaviorRelay(value: nil) 16 | 17 | override func react() { 18 | guard let model = self.model as? FlickrPhotoModel else { 19 | return 20 | } 21 | rxImage.accept(NetworkImage(withURL: model.imageUrl, placeholder: UIImage.from(color: .black))) 22 | rxTitle.accept(model.title) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/FlickrSearchPage/Cell/FlickrImageCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlickrImageCell.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/23/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class FlickrImageCell: BaseCollectionCell { 15 | @IBOutlet private weak var contentImage: UIImageView! 16 | @IBOutlet private weak var titleLb: UILabel! 17 | 18 | override func awakeFromNib() { 19 | super.awakeFromNib() 20 | // Initialization code 21 | } 22 | 23 | override class func getSize(withItem data: Any?) -> CGSize? { 24 | let screenSize = UIScreen.main.bounds 25 | return CGSize(width: (screenSize.width - 30) / 2, height: (screenSize.width - 30) * 3 / 5) 26 | } 27 | 28 | override func bindViewAndViewModel() { 29 | super.bindViewAndViewModel() 30 | guard let viewModel = self.viewModel as? FlickrCellViewModel else { 31 | return 32 | } 33 | viewModel.rxImage ~> contentImage.rx.networkImage => disposeBag 34 | viewModel.rxTitle ~> titleLb.rx.text => disposeBag 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/HeaderFooterListPage/HeaderFooterListPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderFooterListPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/16/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class HeaderFooterListPage: SectionListPage { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | // Do any additional setup after loading the view. 16 | } 17 | 18 | override func setupTableView(_ tableView: UITableView) { 19 | super.setupTableView(tableView) 20 | tableView.register(footerType: SectionFooterListView.self) 21 | } 22 | 23 | override func footerIdentifier(_ footerViewModel: Any, _ returnClassName: Bool = false) -> String? { 24 | return returnClassName ? SectionFooterListView.className : SectionFooterListView.identifier 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/ImagePicker/ViewModel/ImagePickerViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 8/5/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | import Action 14 | 15 | class ImagePickerViewModel: BaseViewModel { 16 | let rxTitle = BehaviorRelay(value: "") 17 | let rxDescription = BehaviorRelay(value: "What's on your mind?") 18 | let rxImage = BehaviorRelay(value: nil) 19 | let rxCanPost = BehaviorRelay(value: false) 20 | 21 | lazy var postAction: Action = { 22 | return Action(enabledIf: self.rxCanPost.asObservable()) { 23 | return .just(self.post()) 24 | } 25 | }() 26 | 27 | override func react() { 28 | super.react() 29 | rxImage.subscribe(onNext: { _ in 30 | }) => disposeBag 31 | 32 | Observable.combineLatest(rxTitle, rxDescription, rxImage) { title, desc, image in 33 | return !(title?.isEmpty ?? false) && !(desc?.isEmpty ?? false) && (image != nil) 34 | } ~> rxCanPost => disposeBag 35 | } 36 | 37 | private func post() {} 38 | } 39 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/MVVMExamplePage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MVVMExamplePage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MVVMExamplePage: TableOfContentsPage { 12 | override func initialize() { 13 | super.initialize() 14 | } 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | /// Custom your UIBarButtonItem 19 | self.navigationItem.leftBarButtonItem = UIBarButtonItem.barButton(withContentInsets: UIEdgeInsets(top: 0, left: -5, bottom: 0, right: 0), 20 | withTitle: "MVVM", 21 | withImage: "icon-back", 22 | withTitleColor: UIColor(hexString: "2ECC71"), 23 | withAction: self.backAction) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/SectionListPage/Cell/ImageCell/CellViewModel/SectionImageCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionImageCellViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/15/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | 14 | class SectionImageCellViewModel: BaseCellViewModel { 15 | let rxImage = BehaviorRelay(value: NetworkImage()) 16 | 17 | override func react() { 18 | guard let model = model as? SectionImageModel else { 19 | return 20 | } 21 | rxImage.accept(NetworkImage(withURL: model.imageUrl, placeholder: .from(color: .lightGray))) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/SectionListPage/Cell/ImageCell/SectionImageCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionImageCell.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/15/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | 12 | class SectionImageCell: BaseTableCell { 13 | @IBOutlet private weak var netImageView: UIImageView! 14 | 15 | override func awakeFromNib() { 16 | super.awakeFromNib() 17 | // Initialization code 18 | netImageView.contentMode = .scaleAspectFill 19 | netImageView.clipsToBounds = true 20 | contentView.addSubview(netImageView) 21 | netImageView.autoMatch(.height, 22 | to: .width, 23 | of: netImageView, 24 | withMultiplier: 9 / 16.0) 25 | netImageView.autoPinEdgesToSuperviewEdges(with: .symmetric(horizontal: 15, vertical: 10)) 26 | } 27 | 28 | override func bindViewAndViewModel() { 29 | guard let viewModel = viewModel as? SectionImageCellViewModel else { 30 | return 31 | } 32 | viewModel.rxImage ~> netImageView.rx.networkImage => disposeBag 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/SectionListPage/Cell/TextCell/CellViewModel/SectionTextCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionTextCellViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/15/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class SectionTextCellViewModel: BaseCellViewModel { 15 | let rxTitle = BehaviorRelay(value: nil) 16 | let rxDesc = BehaviorRelay(value: nil) 17 | 18 | override func react() { 19 | guard let model = model as? SectionTextModel else { 20 | return 21 | } 22 | rxTitle.accept(model.title) 23 | rxDesc.accept(model.desc) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/SectionListPage/Cell/TextCell/SectionTextCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionTextCell.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/15/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class SectionTextCell: BaseTableCell { 15 | @IBOutlet private weak var titleLbl: UILabel! 16 | @IBOutlet private weak var descLbl: UILabel! 17 | @IBOutlet private weak var layout: StackLayout! 18 | 19 | override func awakeFromNib() { 20 | super.awakeFromNib() 21 | // Initialization code 22 | titleLbl.numberOfLines = 0 23 | titleLbl.font = Font.system.bold(withSize: 17) 24 | descLbl.numberOfLines = 0 25 | descLbl.font = Font.system.normal(withSize: 15) 26 | layout.autoPinEdgesToSuperviewEdges(with: .all(5)) 27 | } 28 | 29 | override func setSelected(_ selected: Bool, animated: Bool) { 30 | super.setSelected(selected, animated: animated) 31 | 32 | // Configure the view for the selected state 33 | } 34 | 35 | override func initialize() { 36 | super.initialize() 37 | } 38 | 39 | override func bindViewAndViewModel() { 40 | guard let viewModel = self.viewModel as? SectionTextCellViewModel else { 41 | return 42 | } 43 | viewModel.rxTitle ~> titleLbl.rx.text => disposeBag 44 | viewModel.rxDesc ~> descLbl.rx.text => disposeBag 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/SectionListPage/Section/Footer/SectionFooterListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionFooterListView.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/16/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | 14 | class SectionFooterListView: BaseHeaderTableView { 15 | @IBOutlet private weak var titleLbl: UILabel! 16 | @IBOutlet private weak var descLbl: UILabel! 17 | /* 18 | // Only override draw() if you perform custom drawing. 19 | // An empty implementation adversely affects performance during animation. 20 | override func draw(_ rect: CGRect) { 21 | // Drawing code 22 | } 23 | */ 24 | override class func height(withItem item: BaseViewModel) -> CGFloat { 25 | return 60 26 | } 27 | 28 | override func awakeFromNib() { 29 | super.awakeFromNib() 30 | self.backgroundView?.backgroundColor = .groupTableViewBackground 31 | } 32 | 33 | override func bindViewAndViewModel() { 34 | guard let viewModel = viewModel as? SectionHeaderViewViewModel else { 35 | return 36 | } 37 | 38 | viewModel.rxFooter ~> titleLbl.rx.text => disposeBag 39 | viewModel.rxDesc ~> descLbl.rx.text => disposeBag 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/SectionListPage/Section/Header/SectionHeaderListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionHeaderListView.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/15/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | 14 | class SectionHeaderListView: BaseHeaderTableView { 15 | @IBOutlet private weak var titleLbl: UILabel! 16 | @IBOutlet private weak var addBtn: UIButton! 17 | 18 | override class func height(withItem item: BaseViewModel) -> CGFloat { 19 | return 30 20 | } 21 | 22 | override func awakeFromNib() { 23 | super.awakeFromNib() 24 | self.backgroundView?.backgroundColor = .groupTableViewBackground 25 | } 26 | 27 | override func bindViewAndViewModel() { 28 | guard let viewModel = viewModel as? SectionHeaderViewViewModel else { 29 | return 30 | } 31 | viewModel.rxTitle ~> titleLbl.rx.text => disposeBag 32 | addBtn.rx.bind(to: viewModel.addAction, input: ()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/SectionListPage/Section/ViewModel/SectionHeaderViewViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionHeaderViewViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/15/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import MVVM 10 | import RxSwift 11 | import RxCocoa 12 | import Action 13 | 14 | class SectionHeaderViewViewModel: BaseViewModel { 15 | lazy var addAction: Action = { 16 | return Action { .just(self) } 17 | }() 18 | 19 | let rxTitle = BehaviorRelay(value: nil) 20 | let rxFooter = BehaviorRelay(value: nil) 21 | let rxDesc = BehaviorRelay(value: nil) 22 | 23 | override func react() { 24 | guard let model = model as? HeaderFooterModel else { 25 | return 26 | } 27 | rxTitle.accept(model.title) 28 | rxFooter.accept(model.footer) 29 | rxDesc.accept(model.desc) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/SimpleListPage/Cell/CellViewModel/SimpleListPageCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleListPageCellViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/15/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxCocoa 11 | import MVVM 12 | 13 | class SimpleListPageCellViewModel: BaseCellViewModel { 14 | let rxTitle = BehaviorRelay(value: nil) 15 | override func react() { 16 | guard let viewModel = model as? SimpleModel else { 17 | return 18 | } 19 | rxTitle.accept(viewModel.title) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/SimpleListPage/Cell/SimpleTableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleTableCell.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxCocoa 11 | import MVVM 12 | 13 | class SimpleTableCell: BaseTableCell { 14 | @IBOutlet private weak var titleLbl: UILabel! 15 | 16 | override func awakeFromNib() { 17 | super.awakeFromNib() 18 | // Initialization code 19 | titleLbl.autoPinEdgesToSuperviewEdges(with: .symmetric(horizontal: 15, vertical: 10)) 20 | } 21 | 22 | override func initialize() { 23 | /// Avoid use outlet property here. The function is called before load lib. 24 | /// Update outlet into awakeFromNib 25 | selectionStyle = .none 26 | } 27 | 28 | override func bindViewAndViewModel() { 29 | guard let viewModel = viewModel as? SimpleListPageCellViewModel else { 30 | return 31 | } 32 | viewModel.rxTitle ~> titleLbl.rx.text => disposeBag 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/SimpleListPage/ListPageExamplePage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListPageExamplePage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | import Action 14 | 15 | class ListPageExamplePage: BaseListPage { 16 | let addBtn = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | enableBackButton = true 21 | // Do any additional setup after loading the view. 22 | navigationItem.rightBarButtonItem = addBtn 23 | } 24 | 25 | override func setupTableView(_ tableView: UITableView) { 26 | super.setupTableView(tableView) 27 | /// separatorStyle disabled in BaseListPage 28 | tableView.separatorStyle = .singleLine 29 | tableView.estimatedRowHeight = 200 30 | tableView.register(cellType: SimpleTableCell.self) 31 | } 32 | 33 | override func bindViewAndViewModel() { 34 | super.bindViewAndViewModel() 35 | guard let viewModel = viewModel as? ListPageExampleViewModel else { 36 | return 37 | } 38 | viewModel.rxPageTitle ~> rx.title => disposeBag 39 | addBtn.rx.bind(to: viewModel.addAction, input: ()) 40 | } 41 | 42 | override func cellIdentifier(_ cellViewModel: Any, _ returnClassName: Bool = false) -> String { 43 | return returnClassName ? SimpleTableCell.className : SimpleTableCell.identifier 44 | } 45 | 46 | override func getItemSource() -> RxCollection? { 47 | guard let viewModel = viewModel as? ListPageExampleViewModel else { 48 | return nil 49 | } 50 | return viewModel.itemsSource 51 | } 52 | 53 | override func destroy() { 54 | super.destroy() 55 | viewModel?.destroy() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/MVVMExamples/SimpleListPage/ViewModel/ListPageExampleViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListPageExampleViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | import Action 14 | 15 | class ListPageExampleViewModel: BaseListViewModel { 16 | let rxPageTitle = BehaviorRelay(value: "") 17 | 18 | lazy var addAction: Action = { 19 | return Action { 20 | .just(self.add()) 21 | } 22 | }() 23 | 24 | override func react() { 25 | super.react() 26 | let title = (self.model as? MenuModel)?.title ?? "Simple UITableView" 27 | rxPageTitle.accept(title) 28 | } 29 | 30 | private func add() { 31 | let number = Int.random(in: 1000...10000) 32 | let title = "This is your random number: \(number)" 33 | let cvm = SimpleListPageCellViewModel(model: SimpleModel(withTitle: title)) 34 | itemsSource.append(cvm) 35 | } 36 | 37 | override func selectedItemDidChange(_ cellViewModel: BaseCellViewModel, _ indexPath: IndexPath) { 38 | /// navigationService 39 | /// Use navigation service for push view controller into Navigation. 40 | /// In case View is Root. Navigation service will present viewcontroller. 41 | print("DEBUG: selectedItemDidChange \(cellViewModel.indexPath?.row ?? -1)") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/ServiceExamples/AlertService/AlertServicePage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertServicePage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 7/15/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | 12 | class AlertServicePage: BasePage { 13 | @IBOutlet private weak var okayAlertBtn: UIButton! 14 | @IBOutlet private weak var submitAlertBtn: UIButton! 15 | @IBOutlet private weak var actionSheetAlertBtn: UIButton! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | // Do any additional setup after loading the view. 20 | } 21 | 22 | override func initialize() { 23 | super.initialize() 24 | /// guard let viewModel = self.viewModel as? AlertServiceViewModel else { return } 25 | enableBackButton = true 26 | DependencyManager.shared.registerService(Factory { AlertService() }) 27 | } 28 | 29 | override func bindViewAndViewModel() { 30 | super.bindViewAndViewModel() 31 | guard let viewModel = self.viewModel as? AlertServiceViewModel else { 32 | return 33 | } 34 | okayAlertBtn.rx.bind(to: viewModel.okayAlertAction, input: ()) 35 | submitAlertBtn.rx.bind(to: viewModel.submitAlertAction, input: ()) 36 | actionSheetAlertBtn.rx.bind(to: viewModel.actionSheetAlertAction, input: ()) 37 | viewModel.rxConfirmAction.subscribe(onNext: { confirm in 38 | print("DEBUG: Confirm action did change \(confirm)") 39 | }) => disposeBag 40 | viewModel.rxOkayAction.subscribe(onNext: { confirm in 41 | print("DEBUG: Okay Action did change \(confirm)") 42 | }) => disposeBag 43 | viewModel.rxActionSheetAction.subscribe(onNext: { result in 44 | print("DEBUG: Action Sheet did change \(result)") 45 | }) => disposeBag 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/ServiceExamples/ServiceExamplesPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceExamplesPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | import Action 14 | 15 | class ServiceExamplesPage: TableOfContentsPage { 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | // Do any additional setup after loading the view. 19 | } 20 | 21 | override func initialize() { 22 | super.initialize() 23 | DependencyManager.shared.registerService(Factory { MailService() }) 24 | DependencyManager.shared.registerService(Factory { ShareService() }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/StoreKit/StoreKitPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreKitPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 8/4/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxCocoa 11 | import MVVM 12 | import RxSwift 13 | 14 | class StoreKitPage: BasePage { 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | // Do any additional setup after loading the view. 19 | } 20 | 21 | override func initialize() { 22 | super.initialize() 23 | guard let cellModel = viewModel?.model as? MenuModel else { 24 | return 25 | } 26 | self.rx.title.onNext(cellModel.title) 27 | } 28 | 29 | override func bindViewAndViewModel() { 30 | super.bindViewAndViewModel() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/StoreKit/ViewModel/StoreKitPageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreKitPageViewModel.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 8/4/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | import Action 14 | 15 | class StoreKitPageViewModel: BaseViewModel {} 16 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/TableOfContent/MenuTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuTableViewCell.swift 3 | // MVVM_Example 4 | // 5 | // Created by pham.minh.tien on 5/3/20. 6 | // Copyright © 2020 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class MenuTableViewCell: BaseTableCell { 15 | @IBOutlet private weak var titleLbl: UILabel! 16 | @IBOutlet private weak var descLbl: UILabel! 17 | 18 | override func awakeFromNib() { 19 | super.awakeFromNib() 20 | // Initialization code 21 | accessoryType = .disclosureIndicator 22 | } 23 | 24 | override func initialize() { 25 | accessoryType = .disclosureIndicator 26 | selectionStyle = .none 27 | } 28 | 29 | override func bindViewAndViewModel() { 30 | guard let viewModel = viewModel as? MenuTableCellViewModel else { 31 | return 32 | } 33 | viewModel.rxTitle ~> titleLbl.rx.text => disposeBag 34 | viewModel.rxDesc ~> descLbl.rx.text => disposeBag 35 | } 36 | } 37 | 38 | class MenuTableCellViewModel: BaseCellViewModel { 39 | let rxTitle = BehaviorRelay(value: nil) 40 | let rxDesc = BehaviorRelay(value: nil) 41 | 42 | override func react() { 43 | var title = "" 44 | var desc = "" 45 | if let introModel = model as? IntroductionModel { 46 | title = introModel.title 47 | desc = introModel.desc 48 | } else if let menuModel = model as? MenuModel { 49 | title = menuModel.title 50 | desc = menuModel.desc 51 | } 52 | 53 | rxTitle.accept(title) 54 | rxDesc.accept(desc) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/TableOfContent/TableOfContentsPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NonGenericExampleMenusPage.swift 3 | // MVVM_Example 4 | // 5 | // Created by pham.minh.tien on 5/3/20. 6 | // Copyright © 2020 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | import Action 14 | 15 | class TableOfContentsPage: BaseListPage { 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | // Do any additional setup after loading the view. 19 | enableBackButton = !(navigationController?.viewControllers.count == 1) 20 | } 21 | 22 | override func setupTableView(_ tableView: UITableView) { 23 | super.setupTableView(tableView) 24 | tableView.estimatedRowHeight = 200 25 | tableView.separatorStyle = .singleLine 26 | tableView.register(cellType: MenuTableViewCell.self) 27 | } 28 | // Register event: Connect view to ViewModel. 29 | override func bindViewAndViewModel() { 30 | super.bindViewAndViewModel() 31 | guard let viewModel = viewModel as? TableOfContentViewModelType else { 32 | return 33 | } 34 | viewModel.inputs.rxPageTitle ~> rx.title => disposeBag 35 | } 36 | 37 | override func destroy() { 38 | super.destroy() 39 | viewModel?.destroy() 40 | } 41 | 42 | override func cellIdentifier(_ cellViewModel: Any, _ returnClassName: Bool = false) -> String { 43 | return MenuTableViewCell.identifier(returnClassName) 44 | } 45 | 46 | override func getItemSource() -> RxCollection? { 47 | guard let viewModel = viewModel as? TableOfContentViewModelType else { 48 | return nil 49 | } 50 | return viewModel.outputs.itemsSource 51 | } 52 | 53 | /// Not recommended for use. override selectedItemDidChange on ViewModel. 54 | override func selectedItemDidChange(_ cellViewModel: Any, _ indexPath: IndexPath) {} 55 | } 56 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Page/TransitionExamplesPage/TransitionPage/TransitionContentPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionContentPage.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 8/3/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxCocoa 12 | import RxSwift 13 | import Action 14 | 15 | class TransitionContentPage: BasePage { 16 | @IBOutlet private weak var label: UILabel! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | // Do any additional setup after loading the view. 22 | } 23 | 24 | override func bindViewAndViewModel() { 25 | super.bindViewAndViewModel() 26 | guard let model = self.viewModel?.model as? TransitionContentModel else { 27 | return 28 | } 29 | self.rx.title.onNext(model.title) 30 | self.view.backgroundColor = UIColor(hexString: model.background) 31 | } 32 | 33 | override func initialize() { 34 | super.initialize() 35 | enableBackButton = true 36 | guard let model = self.viewModel?.model as? TransitionContentModel else { 37 | return 38 | } 39 | 40 | label.text = model.desc 41 | } 42 | 43 | override func onBack(_ sender: AnyObject) { 44 | if navigationController?.presentingViewController != nil { 45 | navigationService.pop(with: PopOptions(popType: .dismiss, animated: true)) 46 | } else { 47 | super.onBack(sender) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Services/AlertService/AlertServices.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertServices.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 7/15/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MVVM 11 | import RxSwift 12 | 13 | extension AlertService { 14 | @discardableResult 15 | public func presentPMConfirmAlert(title: String?, message: String?, yesText: String = "Yes", noText: String = "No") -> Single { 16 | return Single.create { single in 17 | let alertPage = AlertPage(title: title, message: message, preferredStyle: .alert) 18 | let yesAction = UIAlertAction(title: yesText, style: .cancel) { _ in 19 | single(.success(true)) 20 | } 21 | /// Set value for action 22 | yesAction.setValue(UIColor.red, forKey: "titleTextColor") 23 | let noAction = UIAlertAction(title: noText, style: .default) { _ in 24 | single(.success(false)) 25 | } 26 | alertPage.addAction(yesAction) 27 | alertPage.addAction(noAction) 28 | alertPage.show() 29 | return Disposables.create { alertPage.hide() } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Services/MailService/MailServiceExt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MailService.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 8/3/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MVVM 11 | import RxSwift 12 | import RxCocoa 13 | import Action 14 | 15 | /// Implement more public function for mail service 16 | extension MailService {} 17 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Services/NetworkServices/AlamofireService/NetworkService+Timeline.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkService+Timeline.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 9/17/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MVVM 11 | import RxSwift 12 | import SwiftyJSON 13 | 14 | extension NetworkService { 15 | func loadTimeline(withPage page: Int, withLimit limit: Int) -> Single { 16 | let parameters: [String: Any] = [ 17 | "page": page, 18 | "limie": limit 19 | ] 20 | 21 | return Single.create { single in 22 | let result = self.request(withService: APIService.loadTimeline(parameters: parameters), 23 | withHash: true, 24 | usingCache: true) 25 | .map({ jsonData -> TimelineResponseModel? in 26 | /// Implement logic maping if need. 27 | /// Maping API Response to FlickrSearchResponse object. 28 | if let result = jsonData.result, 29 | let dictionary = JSON(result).dictionaryObject, 30 | let flickrSearchResponse = TimelineResponseModel(JSON: dictionary) { 31 | flickrSearchResponse.response = JSON(result) 32 | self.curlString.accept(jsonData.cURLString()) 33 | return flickrSearchResponse 34 | } 35 | return nil 36 | }) 37 | .subscribe(onSuccess: { dataResponse in 38 | if let response = dataResponse { 39 | single(.success(response)) 40 | } else { 41 | let err = NSError(domain: "", code: 404, userInfo: ["message": "Data not fount"]) 42 | single(.failure(err)) 43 | } 44 | }, onError: { error in 45 | single(.failure(error)) 46 | }) 47 | 48 | return result 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/Services/NetworkServices/MoyaService/MoyaAPIService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoyaAPIService.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 8/4/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MVVM 11 | import Moya 12 | import RxSwift 13 | import RxCocoa 14 | import ObjectMapper 15 | import SwiftyJSON 16 | 17 | // 1: 18 | enum MoyaAPIService { 19 | // MARK: - API Flickr Search 20 | case flickrSearch(keyword: String, page: Int) 21 | // MARK: - API 22 | } 23 | 24 | extension MoyaAPIService: TargetType { 25 | var headers: [String: String]? { 26 | return nil 27 | } 28 | 29 | var baseURL: URL { return URL(string: "https://api.flickr.com/services/rest")! } 30 | var path: String { 31 | switch self { 32 | case .flickrSearch: 33 | return "" 34 | } 35 | } 36 | 37 | var method: Moya.Method { 38 | switch self { 39 | case .flickrSearch: 40 | return .get 41 | } 42 | } 43 | 44 | var parameterEncoding: ParameterEncoding { 45 | return JSONEncoding.default 46 | } 47 | 48 | var sampleData: Data { 49 | return Data() 50 | } 51 | 52 | var task: Task { 53 | switch self { 54 | case .flickrSearch(let keyword, let page): 55 | let parameters: [String: Any] = [ 56 | "method": "flickr.photos.search", 57 | "api_key": "3cd9dc83d39977c383fd1bf7039e455b", // please provide your API key 58 | "format": "json", 59 | "nojsoncallback": 1, 60 | "page": page, 61 | "per_page": 10, 62 | "text": keyword 63 | ] 64 | return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/SystemConfiguration/ColorConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorConfig.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 9/4/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIColor { 13 | static let tabbarTitleColor = UIColor(hexString: "1C2826") 14 | static let tabbarTitleSelectedColor = UIColor(hexString: "0875F7") 15 | static let tabbarBackgroundColor = UIColor.white 16 | static let tabbarBackgroundSelectedColor = UIColor.white 17 | } 18 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/SystemConfiguration/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/24/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | enum SystemConfiguration { 13 | static let name = Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String ?? "" 14 | static let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString" as String) ?? "1.0" 15 | static let buildIndex = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) ?? "1" 16 | static let bundleID = Bundle.main.bundleIdentifier 17 | static let requestTimeOut = 60 18 | static let NavigationBarHeight = CGFloat(DeviceManager.DeviceType.isIphoneX ? 80 : 60) 19 | static let TabbarHeight = CGFloat(DeviceManager.DeviceType.isIphoneX ? 85 : 50) 20 | } 21 | 22 | func logD(_ message: String, function: String = #function) { 23 | #if !NDEBUG 24 | let formatter = DateFormatter() 25 | formatter.dateFormat = "HH:mm:ss" 26 | let date = formatter.string(from: NSDate() as Date) 27 | print("\(date) Func: \(function) : \(message)") 28 | #endif 29 | } 30 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // MVVMExample 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | // Do any additional setup after loading the view. 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | MVVMExample 4 | 5 | Created by pham.minh.tien on 8/4/20. 6 | Copyright © 2020 Sun*. All rights reserved. 7 | */ 8 | //MARK: Table Of Contents 9 | strTableOfContents = "Table Of Contents"; 10 | 11 | strLocalizePageTitle = "Localization"; 12 | 13 | strTestMessage = "I find television very educating. Every time somebody turns on the set, I go into the other room and read a book."; 14 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/ja.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/ja.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | MVVMExample 4 | 5 | Created by pham.minh.tien on 8/4/20. 6 | Copyright © 2020 Sun*. All rights reserved. 7 | */ 8 | 9 | //MARK: Table Of Contents 10 | strTableOfContents = "目次"; 11 | 12 | strLocalizePageTitle = "ローカリゼーション"; 13 | strTestMessage = "テレビはとても教育的だと思う。誰かがセットをつけるたびに、私は他の部屋に行って本を読んだ。"; 14 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExample/ja.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExampleTests/MVVMExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MVVMExampleTests.swift 3 | // MVVMExampleTests 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import RxSwift 11 | import RxCocoa 12 | import RxTest 13 | import MVVM 14 | @testable import MVVMExample 15 | 16 | class MVVMExampleTests: XCTestCase { 17 | 18 | override func setUp() { 19 | // Put setup code here. This method is called before the invocation of each test method in the class. 20 | } 21 | 22 | override func tearDown() { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | } 25 | 26 | func testPerformanceExample() { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExampleUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MVVMExample/MVVMExampleUITests/MVVMExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MVVMExampleUITests.swift 3 | // MVVMExampleUITests 4 | // 5 | // Created by pham.minh.tien on 5/14/20. 6 | // Copyright © 2020 Sun*. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class MVVMExampleUITests: XCTestCase { 12 | override func setUp() { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDown() { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /MVVMExample/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '14.0' 3 | 4 | target 'MVVMExample' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for MVVMExample 9 | pod 'MVVM', :path => '../' 10 | pod 'SwiftyJSON' 11 | pod 'SwiftLint' 12 | 13 | target 'MVVMExampleTests' do 14 | inherit! :search_paths 15 | # Pods for testing 16 | pod 'RxBlocking' 17 | pod 'RxTest' 18 | pod 'XCGLogger' 19 | pod 'SwiftDate' 20 | end 21 | 22 | target 'MVVMExampleUITests' do 23 | # Pods for testing 24 | end 25 | 26 | end 27 | 28 | post_install do |pi| 29 | pi.pods_project.targets.each do |t| 30 | t.build_configurations.each do |config| 31 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /MVVMExample/config/export-config-debug.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | provisioningProfiles 6 | 7 | com.mvvm.template 8 | MVVMExample 9 | 10 | teamID 11 | 747LG25T2M 12 | CFBundlePackageType 13 | APPL 14 | method 15 | development 16 | 17 | 18 | -------------------------------------------------------------------------------- /MVVMExample/config/export-config.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | destination 6 | export 7 | method 8 | app-store 9 | CFBundlePackageType 10 | APPL 11 | provisioningProfiles 12 | 13 | com.mvvm.template 14 | Template_Dist 15 | 16 | teamID 17 | 747LG25T2M 18 | 19 | 20 | -------------------------------------------------------------------------------- /MVVMExample/fastlane/.env.secret: -------------------------------------------------------------------------------- 1 | # Configtation for iOS 2 | 3 | # SCHEME_NAME_IOS 4 | # Description: The name of workspace 5 | 6 | # IPA_NAME 7 | # Description: The name of the resulting ipa file 8 | 9 | # EXPORT_METHOD 10 | # Description: Method used to export the archive. 11 | # Valid values are: ["app-store", "ad-hoc", "package", "enterprise", "development", "developer-id"] 12 | # Default value: development 13 | 14 | # CHATWORK_API_TOKEN 15 | # Description: Chat bot API token for send release note to group. 16 | # CHATWORK_ROOM_ID 17 | # Description: Chatwork group ID. Send message to chat work group. 18 | 19 | # DISTRIBUTION 20 | # Requirements Install 21 | # Firebase CLI 22 | # sudo fastlane add_plugin firebase_app_distribution 23 | # sudo curl -sL https://firebase.tools | bash 24 | 25 | 26 | # -------------- SETTING PROJECT --------------- 27 | SCHEME_NAME_IOS = 'MVVMExample' 28 | IPA_NAME = 'MVVMExample' 29 | EXPORT_METHOD_DEV = 'development' 30 | EXPORT_METHOD_STG = 'development' 31 | EXPORT_METHOD_PROD = 'ad-hoc' 32 | 33 | FIREBASE_IOS_APP_DEV= 'PROVIDE_YOUR_FIREBASE_APP_DEV_ID' 34 | FIREBASE_IOS_APP_STG= 'PROVIDE_YOUR_FIREBASE_APP_STG_ID' 35 | FIREBASE_IOS_APP_PROD= 'PROVIDE_YOUR_FIREBASE_APP_PROD_ID' 36 | 37 | # DEFAULT TESTER GROUP SETTING 38 | FIREBASE_GROUP_TESTER_DEV ='Provide your group test development on Firebase' 39 | FIREBASE_GROUP_TESTER_STG ='Provide your group test staging on Firebase' 40 | FIREBASE_GROUP_TESTER_PROD ='Provide your group test production on Firebase' 41 | 42 | 43 | # OPTIONAL: Config Infomation 44 | PATH_FILE_CONFIG_DEV = '../Config/Debug.xcconfig' 45 | PATH_FILE_CONFIG_STG = '../Config/Staging.xcconfig' 46 | PATH_FILE_CONFIG_PRO = '../Config/Release.xcconfig' 47 | 48 | # OPTIONAL: Config Notification 49 | # Chatwork 50 | CHATWORK_API_TOKEN = '' 51 | CHATWORK_ROOM_ID = '' 52 | CHATWORK_SEND_TO_TESTER= '' 53 | -------------------------------------------------------------------------------- /MVVMExample/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | # 2 | # For more information about the Appfile, see: 3 | # https://docs.fastlane.tools/advanced/#appfile 4 | # app_identifier "[Provide your bundle id]" 5 | 6 | app_identifier 'com.mtlab.mvvm' 7 | # Apple ID 8 | apple_id 'funylab@gmail.com' 9 | # Team ID 10 | team_name 'Nguyen Hang' 11 | team_id '747LG25T2M' 12 | 13 | -------------------------------------------------------------------------------- /MVVMExample/fastlane/FREADME.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew cask install fastlane` 16 | 17 | # Available Actions 18 | ## iOS 19 | ### ios develop 20 | ``` 21 | fastlane ios develop 22 | ``` 23 | * Beta Distribution develop: Build ipa with develop env. Upload file ipa to firebase distribution. Send message to chatwork group. 24 | 25 | * Run: fastlane ios develop 26 | 27 | * Input release note 28 | 29 | * Input END to finish release note 30 | ### ios staging 31 | ``` 32 | fastlane ios staging 33 | ``` 34 | * Beta Distribution Staging: Build ipa with staging env. Upload file ipa to firebase distribution. Send message to chatwork group. 35 | ### ios production 36 | ``` 37 | fastlane ios production 38 | ``` 39 | * Beta Distribution Production: Build ipa with prod env. Upload file ipa to firebase distribution. Send message to chatwork group. 40 | ### ios uploadFirebaseDevelop 41 | ``` 42 | fastlane ios uploadFirebaseDevelop 43 | ``` 44 | 45 | ### ios uploadFirebaseStaging 46 | ``` 47 | fastlane ios uploadFirebaseStaging 48 | ``` 49 | 50 | ### ios uploadFirebaseProduction 51 | ``` 52 | fastlane ios uploadFirebaseProduction 53 | ``` 54 | 55 | 56 | ---- 57 | 58 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 59 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 60 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 61 | -------------------------------------------------------------------------------- /MVVMExample/fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | # Add firebase app distribution plugin 6 | fastlane add_plugin firebase_app_distribution 7 | -------------------------------------------------------------------------------- /MVVMExample/fastlane/report.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /MVVMExample/fastlane/temp/content.txt: -------------------------------------------------------------------------------- 1 | 2 | New Binary has been uploaded successfully to Firebase! 3 | Platform : IOS 4 | BuildType : develop 5 | Note : 2020-09-24 10:48:20 Test Fastlane develop 6 | -------------------------------------------------------------------------------- /MVVMExample/fastlane/temp/releaseNote.txt: -------------------------------------------------------------------------------- 1 | 2020-09-24 10:48:20 Test Fastlane develop --------------------------------------------------------------------------------