├── .gitignore ├── BoxOffice_SwiftUI.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── BoxOffice iOS Widgets.xcscheme │ └── BoxOffice.xcscheme ├── README.md ├── iOS Widgets ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── WidgetBackground.colorset │ │ └── Contents.json ├── BoxOfficeWidgetBundle.swift ├── Info.plist └── SampleWidget.swift ├── iOS ├── BoxOfficeApp.swift ├── Info.plist ├── Resources │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-40.png │ │ │ ├── Icon-40@2x.png │ │ │ ├── Icon-40@3x.png │ │ │ ├── Icon-60@2x.png │ │ │ ├── Icon-60@3x.png │ │ │ ├── Icon-72.png │ │ │ ├── Icon-72@2x.png │ │ │ ├── Icon-76.png │ │ │ ├── Icon-76@2x.png │ │ │ ├── Icon-83.5@2x.png │ │ │ ├── Icon-Small-50.png │ │ │ ├── Icon-Small-50@2x.png │ │ │ ├── Icon-Small.png │ │ │ ├── Icon-Small@2x.png │ │ │ ├── Icon-Small@3x.png │ │ │ ├── Icon.png │ │ │ ├── Icon@2x.png │ │ │ ├── NotificationIcon@2x.png │ │ │ ├── NotificationIcon@3x.png │ │ │ ├── NotificationIcon~ipad.png │ │ │ ├── NotificationIcon~ipad@2x.png │ │ │ └── ios-marketing.png │ │ ├── Connect_logo_w.imageset │ │ │ ├── Connect_logo_w.png │ │ │ ├── Connect_logo_w@2x.png │ │ │ ├── Connect_logo_w@3x.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── ic_12.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_12.png │ │ │ ├── ic_12@2x.png │ │ │ └── ic_12@3x.png │ │ ├── ic_15.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_15.png │ │ │ ├── ic_15@2x.png │ │ │ └── ic_15@3x.png │ │ ├── ic_19.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_19.png │ │ │ ├── ic_19@2x.png │ │ │ └── ic_19@3x.png │ │ ├── ic_allages.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_allages.png │ │ │ ├── ic_allages@2x.png │ │ │ └── ic_allages@3x.png │ │ ├── ic_star_large.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_star_large.png │ │ │ ├── ic_star_large@2x.png │ │ │ └── ic_star_large@3x.png │ │ ├── ic_star_large_full.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_star_large_full.png │ │ │ ├── ic_star_large_full@2x.png │ │ │ └── ic_star_large_full@3x.png │ │ ├── ic_star_large_half.imageset │ │ │ ├── Contents.json │ │ │ ├── ic_star_large_half.png │ │ │ ├── ic_star_large_half@2x.png │ │ │ └── ic_star_large_half@3x.png │ │ └── img_splash.imageset │ │ │ ├── Contents.json │ │ │ ├── img_splash.png │ │ │ ├── img_splash@2x.png │ │ │ └── img_splash@3x.png │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json └── Sources │ ├── Common │ ├── Cache.swift │ ├── DIContainer.swift │ ├── Grade.swift │ ├── SortMethod.swift │ └── StarType.swift │ ├── Extension │ ├── DateFormatter+.swift │ └── NumberFormatter+.swift │ ├── Model │ ├── Comment.swift │ ├── CommentPostingResponseModel.swift │ ├── CommentsResponseModel.swift │ ├── MovieDetailResponseModel.swift │ └── MoviesResponseModel.swift │ ├── Network │ ├── APIError.swift │ ├── HTTPMethod.swift │ ├── Manager │ │ ├── MockNetworkManager.swift │ │ ├── NetworkManager.swift │ │ └── NetworkManagerProtocol.swift │ ├── Service │ │ ├── APIService.swift │ │ ├── APIServiceProtocol.swift │ │ └── MockAPIService.swift │ ├── Target.swift │ ├── Target │ │ ├── CommentPostingTarget.swift │ │ ├── CommentsTarget.swift │ │ ├── MovieTarget.swift │ │ └── MoviesTarget.swift │ └── TargetVersion.swift │ └── View │ ├── CommentPostingView.swift │ ├── CommentPostingViewModel.swift │ ├── Extension │ ├── MovieMainViewNavigationBarStyle.swift │ └── Text+.swift │ ├── MovieDetailView.swift │ ├── MovieDetailViewModel.swift │ ├── MovieGridItemView.swift │ ├── MovieGridItemViewModel.swift │ ├── MovieGridView.swift │ ├── MovieListItemView.swift │ ├── MovieListItemViewModel.swift │ ├── MovieListView.swift │ ├── MovieMainView.swift │ ├── MovieMainViewModel.swift │ ├── MovieRetryView.swift │ └── Parts │ ├── CircularProgressView.swift │ ├── MultipleSpacer.swift │ ├── PosterImage.swift │ └── StarRatingBar.swift ├── images ├── 1.png ├── 2.png ├── 3.png ├── 4.png └── 5.png └── preview.gif /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/xcode,swift 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=xcode,swift 4 | 5 | ### Swift ### 6 | # Xcode 7 | # 8 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 9 | 10 | ## User settings 11 | xcuserdata/ 12 | 13 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 14 | *.xcscmblueprint 15 | *.xccheckout 16 | 17 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 18 | build/ 19 | DerivedData/ 20 | *.moved-aside 21 | *.pbxuser 22 | !default.pbxuser 23 | *.mode1v3 24 | !default.mode1v3 25 | *.mode2v3 26 | !default.mode2v3 27 | *.perspectivev3 28 | !default.perspectivev3 29 | 30 | ## Obj-C/Swift specific 31 | *.hmap 32 | 33 | ## App packaging 34 | *.ipa 35 | *.dSYM.zip 36 | *.dSYM 37 | 38 | ## Playgrounds 39 | timeline.xctimeline 40 | playground.xcworkspace 41 | 42 | # Swift Package Manager 43 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 44 | # Packages/ 45 | # Package.pins 46 | # Package.resolved 47 | # *.xcodeproj 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | 54 | # CocoaPods 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # Pods/ 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 64 | # Carthage/Checkouts 65 | 66 | Carthage/Build/ 67 | 68 | # Accio dependency management 69 | Dependencies/ 70 | .accio/ 71 | 72 | # fastlane 73 | # It is recommended to not store the screenshots in the git repo. 74 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 75 | # For more information about the recommended setup visit: 76 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 77 | 78 | fastlane/report.xml 79 | fastlane/Preview.html 80 | fastlane/screenshots/**/*.png 81 | fastlane/test_output 82 | 83 | # Code Injection 84 | # After new code Injection tools there's a generated folder /iOSInjectionProject 85 | # https://github.com/johnno1962/injectionforxcode 86 | 87 | iOSInjectionProject/ 88 | 89 | ### Xcode ### 90 | # Xcode 91 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 92 | 93 | 94 | 95 | 96 | ## Gcc Patch 97 | /*.gcno 98 | 99 | ### Xcode Patch ### 100 | *.xcodeproj/* 101 | !*.xcodeproj/project.pbxproj 102 | !*.xcodeproj/xcshareddata/ 103 | !*.xcworkspace/contents.xcworkspacedata 104 | **/xcshareddata/WorkspaceSettings.xcsettings 105 | 106 | # End of https://www.toptal.com/developers/gitignore/api/xcode,swift 107 | -------------------------------------------------------------------------------- /BoxOffice_SwiftUI.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 190679F5268E0EDC004C6FEC /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 19340CE92680BE5E00E88186 /* WidgetKit.framework */; }; 11 | 190679F6268E0EDC004C6FEC /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 19340CEB2680BE5E00E88186 /* SwiftUI.framework */; }; 12 | 190679FC268E0EDE004C6FEC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 190679FB268E0EDE004C6FEC /* Assets.xcassets */; }; 13 | 19067A02268E0EDE004C6FEC /* BoxOffice iOS Widgets.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 190679F4268E0EDC004C6FEC /* BoxOffice iOS Widgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 14 | 19067A06268E0F81004C6FEC /* SampleWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190679EE268E0B4D004C6FEC /* SampleWidget.swift */; }; 15 | 19067A07268E0F81004C6FEC /* BoxOfficeWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190679EB268E0AF0004C6FEC /* BoxOfficeWidgetBundle.swift */; }; 16 | 1916394C2358589C002B9EC1 /* StarType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1916394B2358589C002B9EC1 /* StarType.swift */; }; 17 | 191639502358C68D002B9EC1 /* StarRatingBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1916394F2358C68D002B9EC1 /* StarRatingBar.swift */; }; 18 | 1916395223594BA3002B9EC1 /* DateFormatter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1916395123594BA3002B9EC1 /* DateFormatter+.swift */; }; 19 | 19163954235956A6002B9EC1 /* CommentPostingResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19163953235956A6002B9EC1 /* CommentPostingResponseModel.swift */; }; 20 | 191E19F32357371D00D8963A /* MovieMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E19F22357371D00D8963A /* MovieMainView.swift */; }; 21 | 191E19F52357371E00D8963A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 191E19F42357371E00D8963A /* Assets.xcassets */; }; 22 | 191E19F82357371E00D8963A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 191E19F72357371E00D8963A /* Preview Assets.xcassets */; }; 23 | 191E1A032357396000D8963A /* MovieMainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E1A022357396000D8963A /* MovieMainViewModel.swift */; }; 24 | 191E1A052357396900D8963A /* MovieDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E1A042357396900D8963A /* MovieDetailView.swift */; }; 25 | 191E1A072357396E00D8963A /* MovieDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E1A062357396E00D8963A /* MovieDetailViewModel.swift */; }; 26 | 191E1A092357397800D8963A /* CommentPostingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E1A082357397800D8963A /* CommentPostingView.swift */; }; 27 | 191E1A0B2357397E00D8963A /* CommentPostingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E1A0A2357397E00D8963A /* CommentPostingViewModel.swift */; }; 28 | 191E1A0D235739D700D8963A /* MoviesResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E1A0C235739D700D8963A /* MoviesResponseModel.swift */; }; 29 | 191E1A0F235739DE00D8963A /* MovieDetailResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E1A0E235739DE00D8963A /* MovieDetailResponseModel.swift */; }; 30 | 191E1A1123573BB600D8963A /* CommentsResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E1A1023573BB600D8963A /* CommentsResponseModel.swift */; }; 31 | 191E1A1323573C2100D8963A /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E1A1223573C2100D8963A /* Comment.swift */; }; 32 | 191E1A152357461800D8963A /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E1A142357461800D8963A /* APIService.swift */; }; 33 | 191E1A172357552000D8963A /* SortMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E1A162357552000D8963A /* SortMethod.swift */; }; 34 | 191E1A1F2357725200D8963A /* Grade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E1A1E2357725200D8963A /* Grade.swift */; }; 35 | 192103C7269ABE650063AA78 /* DIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192103C6269ABE650063AA78 /* DIContainer.swift */; }; 36 | 192103C9269AD0C00063AA78 /* MovieMainViewNavigationBarStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192103C8269AD0C00063AA78 /* MovieMainViewNavigationBarStyle.swift */; }; 37 | 192103CD269AE9330063AA78 /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192103CC269AE9330063AA78 /* CircularProgressView.swift */; }; 38 | 19340CB42680B42700E88186 /* APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19340CB32680B42700E88186 /* APIError.swift */; }; 39 | 19340CB62680BC6E00E88186 /* BoxOfficeApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19340CB52680BC6E00E88186 /* BoxOfficeApp.swift */; }; 40 | 1984CB52235F59E800E4B20E /* MovieRetryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1984CB51235F59E800E4B20E /* MovieRetryView.swift */; }; 41 | 1984CB54235F610900E4B20E /* MultipleSpacer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1984CB53235F610900E4B20E /* MultipleSpacer.swift */; }; 42 | 1984CB562360116300E4B20E /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1984CB552360116300E4B20E /* HTTPMethod.swift */; }; 43 | 1984CB58236018DF00E4B20E /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1984CB57236018DF00E4B20E /* NetworkManager.swift */; }; 44 | 1984CB5A23601B5B00E4B20E /* Target.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1984CB5923601B5B00E4B20E /* Target.swift */; }; 45 | 1984CB5E23601BD000E4B20E /* TargetVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1984CB5D23601BD000E4B20E /* TargetVersion.swift */; }; 46 | 1984CB612360227F00E4B20E /* MoviesTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1984CB602360227F00E4B20E /* MoviesTarget.swift */; }; 47 | 1984CB632360228500E4B20E /* MovieTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1984CB622360228500E4B20E /* MovieTarget.swift */; }; 48 | 1984CB652360228B00E4B20E /* CommentsTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1984CB642360228B00E4B20E /* CommentsTarget.swift */; }; 49 | 1984CB672360229000E4B20E /* CommentPostingTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1984CB662360229000E4B20E /* CommentPostingTarget.swift */; }; 50 | 1984CB69236023FE00E4B20E /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1984CB68236023FE00E4B20E /* Cache.swift */; }; 51 | 1998CA9D24AB2679003D592A /* Text+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1998CA9C24AB2679003D592A /* Text+.swift */; }; 52 | 1998CA9F24AB6557003D592A /* NumberFormatter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1998CA9E24AB6557003D592A /* NumberFormatter+.swift */; }; 53 | 19F9B92E24A8E98800CAAB28 /* MovieListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F9B92D24A8E98800CAAB28 /* MovieListView.swift */; }; 54 | 19F9B93024A8EA9B00CAAB28 /* MovieListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F9B92F24A8EA9B00CAAB28 /* MovieListItemView.swift */; }; 55 | 19F9B93224A8EC4000CAAB28 /* MovieListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F9B93124A8EC4000CAAB28 /* MovieListItemViewModel.swift */; }; 56 | 19F9B93424A8FCF500CAAB28 /* MovieGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F9B93324A8FCF500CAAB28 /* MovieGridView.swift */; }; 57 | 19F9B93624A8FD0000CAAB28 /* MovieGridItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F9B93524A8FD0000CAAB28 /* MovieGridItemView.swift */; }; 58 | 19F9B93824A8FD0A00CAAB28 /* MovieGridItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F9B93724A8FD0A00CAAB28 /* MovieGridItemViewModel.swift */; }; 59 | 19F9B93D24AA39D100CAAB28 /* PosterImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F9B93C24AA39D000CAAB28 /* PosterImage.swift */; }; 60 | 19FABE402680CFBA00810611 /* MockAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19FABE3F2680CFBA00810611 /* MockAPIService.swift */; }; 61 | 19FABE432680CFD800810611 /* APIServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19FABE422680CFD800810611 /* APIServiceProtocol.swift */; }; 62 | 19FABE462680D02B00810611 /* NetworkManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19FABE452680D02B00810611 /* NetworkManagerProtocol.swift */; }; 63 | 19FABE482680D04200810611 /* MockNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19FABE472680D04200810611 /* MockNetworkManager.swift */; }; 64 | /* End PBXBuildFile section */ 65 | 66 | /* Begin PBXContainerItemProxy section */ 67 | 19067A00268E0EDE004C6FEC /* PBXContainerItemProxy */ = { 68 | isa = PBXContainerItemProxy; 69 | containerPortal = 191E19E32357371D00D8963A /* Project object */; 70 | proxyType = 1; 71 | remoteGlobalIDString = 190679F3268E0EDC004C6FEC; 72 | remoteInfo = "ios-widgetsExtension"; 73 | }; 74 | /* End PBXContainerItemProxy section */ 75 | 76 | /* Begin PBXCopyFilesBuildPhase section */ 77 | 19340CCE2680BE3C00E88186 /* Embed App Clips */ = { 78 | isa = PBXCopyFilesBuildPhase; 79 | buildActionMask = 2147483647; 80 | dstPath = "$(CONTENTS_FOLDER_PATH)/AppClips"; 81 | dstSubfolderSpec = 16; 82 | files = ( 83 | ); 84 | name = "Embed App Clips"; 85 | runOnlyForDeploymentPostprocessing = 0; 86 | }; 87 | 19340CFC2680BE5F00E88186 /* Embed App Extensions */ = { 88 | isa = PBXCopyFilesBuildPhase; 89 | buildActionMask = 2147483647; 90 | dstPath = ""; 91 | dstSubfolderSpec = 13; 92 | files = ( 93 | 19067A02268E0EDE004C6FEC /* BoxOffice iOS Widgets.appex in Embed App Extensions */, 94 | ); 95 | name = "Embed App Extensions"; 96 | runOnlyForDeploymentPostprocessing = 0; 97 | }; 98 | /* End PBXCopyFilesBuildPhase section */ 99 | 100 | /* Begin PBXFileReference section */ 101 | 190679EB268E0AF0004C6FEC /* BoxOfficeWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeWidgetBundle.swift; sourceTree = ""; }; 102 | 190679EE268E0B4D004C6FEC /* SampleWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleWidget.swift; sourceTree = ""; }; 103 | 190679F4268E0EDC004C6FEC /* BoxOffice iOS Widgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "BoxOffice iOS Widgets.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 104 | 190679FB268E0EDE004C6FEC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 105 | 190679FD268E0EDE004C6FEC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 106 | 1916394B2358589C002B9EC1 /* StarType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarType.swift; sourceTree = ""; }; 107 | 1916394F2358C68D002B9EC1 /* StarRatingBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarRatingBar.swift; sourceTree = ""; }; 108 | 1916395123594BA3002B9EC1 /* DateFormatter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+.swift"; sourceTree = ""; }; 109 | 19163953235956A6002B9EC1 /* CommentPostingResponseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentPostingResponseModel.swift; sourceTree = ""; }; 110 | 191E19EB2357371D00D8963A /* BoxOffice.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BoxOffice.app; sourceTree = BUILT_PRODUCTS_DIR; }; 111 | 191E19F22357371D00D8963A /* MovieMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieMainView.swift; sourceTree = ""; }; 112 | 191E19F42357371E00D8963A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 113 | 191E19F72357371E00D8963A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 114 | 191E19FC2357371E00D8963A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 115 | 191E1A022357396000D8963A /* MovieMainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieMainViewModel.swift; sourceTree = ""; }; 116 | 191E1A042357396900D8963A /* MovieDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailView.swift; sourceTree = ""; }; 117 | 191E1A062357396E00D8963A /* MovieDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailViewModel.swift; sourceTree = ""; }; 118 | 191E1A082357397800D8963A /* CommentPostingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentPostingView.swift; sourceTree = ""; }; 119 | 191E1A0A2357397E00D8963A /* CommentPostingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentPostingViewModel.swift; sourceTree = ""; }; 120 | 191E1A0C235739D700D8963A /* MoviesResponseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesResponseModel.swift; sourceTree = ""; }; 121 | 191E1A0E235739DE00D8963A /* MovieDetailResponseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailResponseModel.swift; sourceTree = ""; }; 122 | 191E1A1023573BB600D8963A /* CommentsResponseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsResponseModel.swift; sourceTree = ""; }; 123 | 191E1A1223573C2100D8963A /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; 124 | 191E1A142357461800D8963A /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; 125 | 191E1A162357552000D8963A /* SortMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortMethod.swift; sourceTree = ""; }; 126 | 191E1A1E2357725200D8963A /* Grade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Grade.swift; sourceTree = ""; }; 127 | 192103C6269ABE650063AA78 /* DIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DIContainer.swift; sourceTree = ""; }; 128 | 192103C8269AD0C00063AA78 /* MovieMainViewNavigationBarStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieMainViewNavigationBarStyle.swift; sourceTree = ""; }; 129 | 192103CC269AE9330063AA78 /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; 130 | 19340CB32680B42700E88186 /* APIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIError.swift; sourceTree = ""; }; 131 | 19340CB52680BC6E00E88186 /* BoxOfficeApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeApp.swift; sourceTree = ""; }; 132 | 19340CE92680BE5E00E88186 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 133 | 19340CEB2680BE5E00E88186 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 134 | 1984CB51235F59E800E4B20E /* MovieRetryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieRetryView.swift; sourceTree = ""; }; 135 | 1984CB53235F610900E4B20E /* MultipleSpacer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleSpacer.swift; sourceTree = ""; }; 136 | 1984CB552360116300E4B20E /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; 137 | 1984CB57236018DF00E4B20E /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; 138 | 1984CB5923601B5B00E4B20E /* Target.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Target.swift; sourceTree = ""; }; 139 | 1984CB5D23601BD000E4B20E /* TargetVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetVersion.swift; sourceTree = ""; }; 140 | 1984CB602360227F00E4B20E /* MoviesTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesTarget.swift; sourceTree = ""; }; 141 | 1984CB622360228500E4B20E /* MovieTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieTarget.swift; sourceTree = ""; }; 142 | 1984CB642360228B00E4B20E /* CommentsTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsTarget.swift; sourceTree = ""; }; 143 | 1984CB662360229000E4B20E /* CommentPostingTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentPostingTarget.swift; sourceTree = ""; }; 144 | 1984CB68236023FE00E4B20E /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; 145 | 1998CA9C24AB2679003D592A /* Text+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+.swift"; sourceTree = ""; }; 146 | 1998CA9E24AB6557003D592A /* NumberFormatter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+.swift"; sourceTree = ""; }; 147 | 19F9B92D24A8E98800CAAB28 /* MovieListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieListView.swift; sourceTree = ""; }; 148 | 19F9B92F24A8EA9B00CAAB28 /* MovieListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieListItemView.swift; sourceTree = ""; }; 149 | 19F9B93124A8EC4000CAAB28 /* MovieListItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieListItemViewModel.swift; sourceTree = ""; }; 150 | 19F9B93324A8FCF500CAAB28 /* MovieGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieGridView.swift; sourceTree = ""; }; 151 | 19F9B93524A8FD0000CAAB28 /* MovieGridItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieGridItemView.swift; sourceTree = ""; }; 152 | 19F9B93724A8FD0A00CAAB28 /* MovieGridItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieGridItemViewModel.swift; sourceTree = ""; }; 153 | 19F9B93C24AA39D000CAAB28 /* PosterImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterImage.swift; sourceTree = ""; }; 154 | 19FABE3F2680CFBA00810611 /* MockAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAPIService.swift; sourceTree = ""; }; 155 | 19FABE422680CFD800810611 /* APIServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServiceProtocol.swift; sourceTree = ""; }; 156 | 19FABE452680D02B00810611 /* NetworkManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManagerProtocol.swift; sourceTree = ""; }; 157 | 19FABE472680D04200810611 /* MockNetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkManager.swift; sourceTree = ""; }; 158 | /* End PBXFileReference section */ 159 | 160 | /* Begin PBXFrameworksBuildPhase section */ 161 | 190679F1268E0EDC004C6FEC /* Frameworks */ = { 162 | isa = PBXFrameworksBuildPhase; 163 | buildActionMask = 2147483647; 164 | files = ( 165 | 190679F6268E0EDC004C6FEC /* SwiftUI.framework in Frameworks */, 166 | 190679F5268E0EDC004C6FEC /* WidgetKit.framework in Frameworks */, 167 | ); 168 | runOnlyForDeploymentPostprocessing = 0; 169 | }; 170 | 191E19E82357371D00D8963A /* Frameworks */ = { 171 | isa = PBXFrameworksBuildPhase; 172 | buildActionMask = 2147483647; 173 | files = ( 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | /* End PBXFrameworksBuildPhase section */ 178 | 179 | /* Begin PBXGroup section */ 180 | 190679F7268E0EDC004C6FEC /* iOS Widgets */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | 190679EB268E0AF0004C6FEC /* BoxOfficeWidgetBundle.swift */, 184 | 190679EE268E0B4D004C6FEC /* SampleWidget.swift */, 185 | 190679FB268E0EDE004C6FEC /* Assets.xcassets */, 186 | 190679FD268E0EDE004C6FEC /* Info.plist */, 187 | ); 188 | path = "iOS Widgets"; 189 | sourceTree = ""; 190 | }; 191 | 1916394123584AFC002B9EC1 /* Sources */ = { 192 | isa = PBXGroup; 193 | children = ( 194 | 1916394723585305002B9EC1 /* Extension */, 195 | 19163946235852F0002B9EC1 /* Network */, 196 | 19163945235852ED002B9EC1 /* Model */, 197 | 19163944235852EA002B9EC1 /* View */, 198 | 19163943235852E6002B9EC1 /* Common */, 199 | ); 200 | path = Sources; 201 | sourceTree = ""; 202 | }; 203 | 1916394223584B00002B9EC1 /* Resources */ = { 204 | isa = PBXGroup; 205 | children = ( 206 | 191E19F42357371E00D8963A /* Assets.xcassets */, 207 | 191E19F62357371E00D8963A /* Preview Content */, 208 | ); 209 | path = Resources; 210 | sourceTree = ""; 211 | }; 212 | 19163943235852E6002B9EC1 /* Common */ = { 213 | isa = PBXGroup; 214 | children = ( 215 | 191E1A1E2357725200D8963A /* Grade.swift */, 216 | 191E1A162357552000D8963A /* SortMethod.swift */, 217 | 1916394B2358589C002B9EC1 /* StarType.swift */, 218 | 1984CB68236023FE00E4B20E /* Cache.swift */, 219 | 192103C6269ABE650063AA78 /* DIContainer.swift */, 220 | ); 221 | path = Common; 222 | sourceTree = ""; 223 | }; 224 | 19163944235852EA002B9EC1 /* View */ = { 225 | isa = PBXGroup; 226 | children = ( 227 | 191E19F22357371D00D8963A /* MovieMainView.swift */, 228 | 191E1A022357396000D8963A /* MovieMainViewModel.swift */, 229 | 19F9B92D24A8E98800CAAB28 /* MovieListView.swift */, 230 | 19F9B92F24A8EA9B00CAAB28 /* MovieListItemView.swift */, 231 | 19F9B93124A8EC4000CAAB28 /* MovieListItemViewModel.swift */, 232 | 19F9B93324A8FCF500CAAB28 /* MovieGridView.swift */, 233 | 19F9B93524A8FD0000CAAB28 /* MovieGridItemView.swift */, 234 | 19F9B93724A8FD0A00CAAB28 /* MovieGridItemViewModel.swift */, 235 | 191E1A042357396900D8963A /* MovieDetailView.swift */, 236 | 191E1A062357396E00D8963A /* MovieDetailViewModel.swift */, 237 | 191E1A082357397800D8963A /* CommentPostingView.swift */, 238 | 191E1A0A2357397E00D8963A /* CommentPostingViewModel.swift */, 239 | 1984CB51235F59E800E4B20E /* MovieRetryView.swift */, 240 | 1998CA9A24AB24CF003D592A /* Parts */, 241 | 1998CA9B24AB266C003D592A /* Extension */, 242 | ); 243 | path = View; 244 | sourceTree = ""; 245 | }; 246 | 19163945235852ED002B9EC1 /* Model */ = { 247 | isa = PBXGroup; 248 | children = ( 249 | 191E1A0C235739D700D8963A /* MoviesResponseModel.swift */, 250 | 191E1A0E235739DE00D8963A /* MovieDetailResponseModel.swift */, 251 | 191E1A1023573BB600D8963A /* CommentsResponseModel.swift */, 252 | 19163953235956A6002B9EC1 /* CommentPostingResponseModel.swift */, 253 | 191E1A1223573C2100D8963A /* Comment.swift */, 254 | ); 255 | path = Model; 256 | sourceTree = ""; 257 | }; 258 | 19163946235852F0002B9EC1 /* Network */ = { 259 | isa = PBXGroup; 260 | children = ( 261 | 19FABE442680D01C00810611 /* Manager */, 262 | 19FABE412680CFCE00810611 /* Service */, 263 | 1984CB5F2360226F00E4B20E /* Target */, 264 | 1984CB552360116300E4B20E /* HTTPMethod.swift */, 265 | 1984CB5923601B5B00E4B20E /* Target.swift */, 266 | 1984CB5D23601BD000E4B20E /* TargetVersion.swift */, 267 | 19340CB32680B42700E88186 /* APIError.swift */, 268 | ); 269 | path = Network; 270 | sourceTree = ""; 271 | }; 272 | 1916394723585305002B9EC1 /* Extension */ = { 273 | isa = PBXGroup; 274 | children = ( 275 | 1916395123594BA3002B9EC1 /* DateFormatter+.swift */, 276 | 1998CA9E24AB6557003D592A /* NumberFormatter+.swift */, 277 | ); 278 | path = Extension; 279 | sourceTree = ""; 280 | }; 281 | 191E19E22357371D00D8963A = { 282 | isa = PBXGroup; 283 | children = ( 284 | 19340D152680BEEC00E88186 /* iOS */, 285 | 190679F7268E0EDC004C6FEC /* iOS Widgets */, 286 | 19340CE82680BE5E00E88186 /* Frameworks */, 287 | 191E19EC2357371D00D8963A /* Products */, 288 | ); 289 | sourceTree = ""; 290 | }; 291 | 191E19EC2357371D00D8963A /* Products */ = { 292 | isa = PBXGroup; 293 | children = ( 294 | 191E19EB2357371D00D8963A /* BoxOffice.app */, 295 | 190679F4268E0EDC004C6FEC /* BoxOffice iOS Widgets.appex */, 296 | ); 297 | name = Products; 298 | sourceTree = ""; 299 | }; 300 | 191E19F62357371E00D8963A /* Preview Content */ = { 301 | isa = PBXGroup; 302 | children = ( 303 | 191E19F72357371E00D8963A /* Preview Assets.xcassets */, 304 | ); 305 | path = "Preview Content"; 306 | sourceTree = ""; 307 | }; 308 | 19340CE82680BE5E00E88186 /* Frameworks */ = { 309 | isa = PBXGroup; 310 | children = ( 311 | 19340CE92680BE5E00E88186 /* WidgetKit.framework */, 312 | 19340CEB2680BE5E00E88186 /* SwiftUI.framework */, 313 | ); 314 | name = Frameworks; 315 | sourceTree = ""; 316 | }; 317 | 19340D152680BEEC00E88186 /* iOS */ = { 318 | isa = PBXGroup; 319 | children = ( 320 | 1916394123584AFC002B9EC1 /* Sources */, 321 | 1916394223584B00002B9EC1 /* Resources */, 322 | 19340CB52680BC6E00E88186 /* BoxOfficeApp.swift */, 323 | 191E19FC2357371E00D8963A /* Info.plist */, 324 | ); 325 | path = iOS; 326 | sourceTree = ""; 327 | }; 328 | 1984CB5F2360226F00E4B20E /* Target */ = { 329 | isa = PBXGroup; 330 | children = ( 331 | 1984CB602360227F00E4B20E /* MoviesTarget.swift */, 332 | 1984CB622360228500E4B20E /* MovieTarget.swift */, 333 | 1984CB642360228B00E4B20E /* CommentsTarget.swift */, 334 | 1984CB662360229000E4B20E /* CommentPostingTarget.swift */, 335 | ); 336 | path = Target; 337 | sourceTree = ""; 338 | }; 339 | 1998CA9A24AB24CF003D592A /* Parts */ = { 340 | isa = PBXGroup; 341 | children = ( 342 | 1984CB53235F610900E4B20E /* MultipleSpacer.swift */, 343 | 19F9B93C24AA39D000CAAB28 /* PosterImage.swift */, 344 | 1916394F2358C68D002B9EC1 /* StarRatingBar.swift */, 345 | 192103CC269AE9330063AA78 /* CircularProgressView.swift */, 346 | ); 347 | path = Parts; 348 | sourceTree = ""; 349 | }; 350 | 1998CA9B24AB266C003D592A /* Extension */ = { 351 | isa = PBXGroup; 352 | children = ( 353 | 1998CA9C24AB2679003D592A /* Text+.swift */, 354 | 192103C8269AD0C00063AA78 /* MovieMainViewNavigationBarStyle.swift */, 355 | ); 356 | path = Extension; 357 | sourceTree = ""; 358 | }; 359 | 19FABE412680CFCE00810611 /* Service */ = { 360 | isa = PBXGroup; 361 | children = ( 362 | 19FABE422680CFD800810611 /* APIServiceProtocol.swift */, 363 | 191E1A142357461800D8963A /* APIService.swift */, 364 | 19FABE3F2680CFBA00810611 /* MockAPIService.swift */, 365 | ); 366 | path = Service; 367 | sourceTree = ""; 368 | }; 369 | 19FABE442680D01C00810611 /* Manager */ = { 370 | isa = PBXGroup; 371 | children = ( 372 | 19FABE452680D02B00810611 /* NetworkManagerProtocol.swift */, 373 | 1984CB57236018DF00E4B20E /* NetworkManager.swift */, 374 | 19FABE472680D04200810611 /* MockNetworkManager.swift */, 375 | ); 376 | path = Manager; 377 | sourceTree = ""; 378 | }; 379 | /* End PBXGroup section */ 380 | 381 | /* Begin PBXNativeTarget section */ 382 | 190679F3268E0EDC004C6FEC /* BoxOffice iOS Widgets */ = { 383 | isa = PBXNativeTarget; 384 | buildConfigurationList = 19067A03268E0EDE004C6FEC /* Build configuration list for PBXNativeTarget "BoxOffice iOS Widgets" */; 385 | buildPhases = ( 386 | 190679F0268E0EDC004C6FEC /* Sources */, 387 | 190679F1268E0EDC004C6FEC /* Frameworks */, 388 | 190679F2268E0EDC004C6FEC /* Resources */, 389 | ); 390 | buildRules = ( 391 | ); 392 | dependencies = ( 393 | ); 394 | name = "BoxOffice iOS Widgets"; 395 | productName = "ios-widgetsExtension"; 396 | productReference = 190679F4268E0EDC004C6FEC /* BoxOffice iOS Widgets.appex */; 397 | productType = "com.apple.product-type.app-extension"; 398 | }; 399 | 191E19EA2357371D00D8963A /* BoxOffice */ = { 400 | isa = PBXNativeTarget; 401 | buildConfigurationList = 191E19FF2357371E00D8963A /* Build configuration list for PBXNativeTarget "BoxOffice" */; 402 | buildPhases = ( 403 | 191E19E72357371D00D8963A /* Sources */, 404 | 191E19E82357371D00D8963A /* Frameworks */, 405 | 191E19E92357371D00D8963A /* Resources */, 406 | 19340CCE2680BE3C00E88186 /* Embed App Clips */, 407 | 19340CFC2680BE5F00E88186 /* Embed App Extensions */, 408 | ); 409 | buildRules = ( 410 | ); 411 | dependencies = ( 412 | 19067A01268E0EDE004C6FEC /* PBXTargetDependency */, 413 | ); 414 | name = BoxOffice; 415 | productName = BoxOffice_SwiftUI; 416 | productReference = 191E19EB2357371D00D8963A /* BoxOffice.app */; 417 | productType = "com.apple.product-type.application"; 418 | }; 419 | /* End PBXNativeTarget section */ 420 | 421 | /* Begin PBXProject section */ 422 | 191E19E32357371D00D8963A /* Project object */ = { 423 | isa = PBXProject; 424 | attributes = { 425 | LastSwiftUpdateCheck = 1250; 426 | LastUpgradeCheck = 1200; 427 | ORGANIZATIONNAME = presto; 428 | TargetAttributes = { 429 | 190679F3268E0EDC004C6FEC = { 430 | CreatedOnToolsVersion = 12.5.1; 431 | }; 432 | 191E19EA2357371D00D8963A = { 433 | CreatedOnToolsVersion = 11.1; 434 | }; 435 | }; 436 | }; 437 | buildConfigurationList = 191E19E62357371D00D8963A /* Build configuration list for PBXProject "BoxOffice_SwiftUI" */; 438 | compatibilityVersion = "Xcode 9.3"; 439 | developmentRegion = en; 440 | hasScannedForEncodings = 0; 441 | knownRegions = ( 442 | en, 443 | Base, 444 | ); 445 | mainGroup = 191E19E22357371D00D8963A; 446 | productRefGroup = 191E19EC2357371D00D8963A /* Products */; 447 | projectDirPath = ""; 448 | projectRoot = ""; 449 | targets = ( 450 | 191E19EA2357371D00D8963A /* BoxOffice */, 451 | 190679F3268E0EDC004C6FEC /* BoxOffice iOS Widgets */, 452 | ); 453 | }; 454 | /* End PBXProject section */ 455 | 456 | /* Begin PBXResourcesBuildPhase section */ 457 | 190679F2268E0EDC004C6FEC /* Resources */ = { 458 | isa = PBXResourcesBuildPhase; 459 | buildActionMask = 2147483647; 460 | files = ( 461 | 190679FC268E0EDE004C6FEC /* Assets.xcassets in Resources */, 462 | ); 463 | runOnlyForDeploymentPostprocessing = 0; 464 | }; 465 | 191E19E92357371D00D8963A /* Resources */ = { 466 | isa = PBXResourcesBuildPhase; 467 | buildActionMask = 2147483647; 468 | files = ( 469 | 191E19F82357371E00D8963A /* Preview Assets.xcassets in Resources */, 470 | 191E19F52357371E00D8963A /* Assets.xcassets in Resources */, 471 | ); 472 | runOnlyForDeploymentPostprocessing = 0; 473 | }; 474 | /* End PBXResourcesBuildPhase section */ 475 | 476 | /* Begin PBXSourcesBuildPhase section */ 477 | 190679F0268E0EDC004C6FEC /* Sources */ = { 478 | isa = PBXSourcesBuildPhase; 479 | buildActionMask = 2147483647; 480 | files = ( 481 | 19067A06268E0F81004C6FEC /* SampleWidget.swift in Sources */, 482 | 19067A07268E0F81004C6FEC /* BoxOfficeWidgetBundle.swift in Sources */, 483 | ); 484 | runOnlyForDeploymentPostprocessing = 0; 485 | }; 486 | 191E19E72357371D00D8963A /* Sources */ = { 487 | isa = PBXSourcesBuildPhase; 488 | buildActionMask = 2147483647; 489 | files = ( 490 | 1984CB672360229000E4B20E /* CommentPostingTarget.swift in Sources */, 491 | 191E1A052357396900D8963A /* MovieDetailView.swift in Sources */, 492 | 1984CB5A23601B5B00E4B20E /* Target.swift in Sources */, 493 | 191639502358C68D002B9EC1 /* StarRatingBar.swift in Sources */, 494 | 19F9B93D24AA39D100CAAB28 /* PosterImage.swift in Sources */, 495 | 191E1A0F235739DE00D8963A /* MovieDetailResponseModel.swift in Sources */, 496 | 19F9B93024A8EA9B00CAAB28 /* MovieListItemView.swift in Sources */, 497 | 191E1A152357461800D8963A /* APIService.swift in Sources */, 498 | 19163954235956A6002B9EC1 /* CommentPostingResponseModel.swift in Sources */, 499 | 1984CB54235F610900E4B20E /* MultipleSpacer.swift in Sources */, 500 | 191E1A0D235739D700D8963A /* MoviesResponseModel.swift in Sources */, 501 | 191E1A072357396E00D8963A /* MovieDetailViewModel.swift in Sources */, 502 | 192103C7269ABE650063AA78 /* DIContainer.swift in Sources */, 503 | 191E1A1123573BB600D8963A /* CommentsResponseModel.swift in Sources */, 504 | 1916394C2358589C002B9EC1 /* StarType.swift in Sources */, 505 | 1998CA9F24AB6557003D592A /* NumberFormatter+.swift in Sources */, 506 | 1984CB52235F59E800E4B20E /* MovieRetryView.swift in Sources */, 507 | 191E1A0B2357397E00D8963A /* CommentPostingViewModel.swift in Sources */, 508 | 1984CB58236018DF00E4B20E /* NetworkManager.swift in Sources */, 509 | 1984CB562360116300E4B20E /* HTTPMethod.swift in Sources */, 510 | 19FABE402680CFBA00810611 /* MockAPIService.swift in Sources */, 511 | 1984CB5E23601BD000E4B20E /* TargetVersion.swift in Sources */, 512 | 191E1A1F2357725200D8963A /* Grade.swift in Sources */, 513 | 19F9B93224A8EC4000CAAB28 /* MovieListItemViewModel.swift in Sources */, 514 | 1984CB69236023FE00E4B20E /* Cache.swift in Sources */, 515 | 191E19F32357371D00D8963A /* MovieMainView.swift in Sources */, 516 | 1984CB652360228B00E4B20E /* CommentsTarget.swift in Sources */, 517 | 19FABE482680D04200810611 /* MockNetworkManager.swift in Sources */, 518 | 191E1A032357396000D8963A /* MovieMainViewModel.swift in Sources */, 519 | 191E1A092357397800D8963A /* CommentPostingView.swift in Sources */, 520 | 19FABE462680D02B00810611 /* NetworkManagerProtocol.swift in Sources */, 521 | 19F9B93824A8FD0A00CAAB28 /* MovieGridItemViewModel.swift in Sources */, 522 | 19F9B93424A8FCF500CAAB28 /* MovieGridView.swift in Sources */, 523 | 19340CB42680B42700E88186 /* APIError.swift in Sources */, 524 | 1998CA9D24AB2679003D592A /* Text+.swift in Sources */, 525 | 19FABE432680CFD800810611 /* APIServiceProtocol.swift in Sources */, 526 | 192103CD269AE9330063AA78 /* CircularProgressView.swift in Sources */, 527 | 1916395223594BA3002B9EC1 /* DateFormatter+.swift in Sources */, 528 | 192103C9269AD0C00063AA78 /* MovieMainViewNavigationBarStyle.swift in Sources */, 529 | 19F9B93624A8FD0000CAAB28 /* MovieGridItemView.swift in Sources */, 530 | 191E1A172357552000D8963A /* SortMethod.swift in Sources */, 531 | 1984CB632360228500E4B20E /* MovieTarget.swift in Sources */, 532 | 191E1A1323573C2100D8963A /* Comment.swift in Sources */, 533 | 19340CB62680BC6E00E88186 /* BoxOfficeApp.swift in Sources */, 534 | 1984CB612360227F00E4B20E /* MoviesTarget.swift in Sources */, 535 | 19F9B92E24A8E98800CAAB28 /* MovieListView.swift in Sources */, 536 | ); 537 | runOnlyForDeploymentPostprocessing = 0; 538 | }; 539 | /* End PBXSourcesBuildPhase section */ 540 | 541 | /* Begin PBXTargetDependency section */ 542 | 19067A01268E0EDE004C6FEC /* PBXTargetDependency */ = { 543 | isa = PBXTargetDependency; 544 | target = 190679F3268E0EDC004C6FEC /* BoxOffice iOS Widgets */; 545 | targetProxy = 19067A00268E0EDE004C6FEC /* PBXContainerItemProxy */; 546 | }; 547 | /* End PBXTargetDependency section */ 548 | 549 | /* Begin XCBuildConfiguration section */ 550 | 19067A04268E0EDE004C6FEC /* Debug */ = { 551 | isa = XCBuildConfiguration; 552 | buildSettings = { 553 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 554 | ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 555 | CODE_SIGN_STYLE = Automatic; 556 | DEVELOPMENT_TEAM = H79MF628K3; 557 | INFOPLIST_FILE = "iOS Widgets/Info.plist"; 558 | LD_RUNPATH_SEARCH_PATHS = ( 559 | "$(inherited)", 560 | "@executable_path/Frameworks", 561 | "@executable_path/../../Frameworks", 562 | ); 563 | PRODUCT_BUNDLE_IDENTIFIER = "com.presto.BoxOffice-SwiftUI.ios-widgets"; 564 | PRODUCT_NAME = "$(TARGET_NAME)"; 565 | SKIP_INSTALL = YES; 566 | SWIFT_VERSION = 5.0; 567 | TARGETED_DEVICE_FAMILY = "1,2"; 568 | }; 569 | name = Debug; 570 | }; 571 | 19067A05268E0EDE004C6FEC /* Release */ = { 572 | isa = XCBuildConfiguration; 573 | buildSettings = { 574 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 575 | ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 576 | CODE_SIGN_STYLE = Automatic; 577 | DEVELOPMENT_TEAM = H79MF628K3; 578 | INFOPLIST_FILE = "iOS Widgets/Info.plist"; 579 | LD_RUNPATH_SEARCH_PATHS = ( 580 | "$(inherited)", 581 | "@executable_path/Frameworks", 582 | "@executable_path/../../Frameworks", 583 | ); 584 | PRODUCT_BUNDLE_IDENTIFIER = "com.presto.BoxOffice-SwiftUI.ios-widgets"; 585 | PRODUCT_NAME = "$(TARGET_NAME)"; 586 | SKIP_INSTALL = YES; 587 | SWIFT_VERSION = 5.0; 588 | TARGETED_DEVICE_FAMILY = "1,2"; 589 | }; 590 | name = Release; 591 | }; 592 | 191E19FD2357371E00D8963A /* Debug */ = { 593 | isa = XCBuildConfiguration; 594 | buildSettings = { 595 | ALWAYS_SEARCH_USER_PATHS = NO; 596 | CLANG_ANALYZER_NONNULL = YES; 597 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 598 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 599 | CLANG_CXX_LIBRARY = "libc++"; 600 | CLANG_ENABLE_MODULES = YES; 601 | CLANG_ENABLE_OBJC_ARC = YES; 602 | CLANG_ENABLE_OBJC_WEAK = YES; 603 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 604 | CLANG_WARN_BOOL_CONVERSION = YES; 605 | CLANG_WARN_COMMA = YES; 606 | CLANG_WARN_CONSTANT_CONVERSION = YES; 607 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 608 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 609 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 610 | CLANG_WARN_EMPTY_BODY = YES; 611 | CLANG_WARN_ENUM_CONVERSION = YES; 612 | CLANG_WARN_INFINITE_RECURSION = YES; 613 | CLANG_WARN_INT_CONVERSION = YES; 614 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 615 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 616 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 617 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 618 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 619 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 620 | CLANG_WARN_STRICT_PROTOTYPES = YES; 621 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 622 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 623 | CLANG_WARN_UNREACHABLE_CODE = YES; 624 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 625 | COPY_PHASE_STRIP = NO; 626 | DEBUG_INFORMATION_FORMAT = dwarf; 627 | ENABLE_STRICT_OBJC_MSGSEND = YES; 628 | ENABLE_TESTABILITY = YES; 629 | GCC_C_LANGUAGE_STANDARD = gnu11; 630 | GCC_DYNAMIC_NO_PIC = NO; 631 | GCC_NO_COMMON_BLOCKS = YES; 632 | GCC_OPTIMIZATION_LEVEL = 0; 633 | GCC_PREPROCESSOR_DEFINITIONS = ( 634 | "DEBUG=1", 635 | "$(inherited)", 636 | ); 637 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 638 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 639 | GCC_WARN_UNDECLARED_SELECTOR = YES; 640 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 641 | GCC_WARN_UNUSED_FUNCTION = YES; 642 | GCC_WARN_UNUSED_VARIABLE = YES; 643 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 644 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 645 | MTL_FAST_MATH = YES; 646 | ONLY_ACTIVE_ARCH = YES; 647 | SDKROOT = iphoneos; 648 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 649 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 650 | }; 651 | name = Debug; 652 | }; 653 | 191E19FE2357371E00D8963A /* Release */ = { 654 | isa = XCBuildConfiguration; 655 | buildSettings = { 656 | ALWAYS_SEARCH_USER_PATHS = NO; 657 | CLANG_ANALYZER_NONNULL = YES; 658 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 659 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 660 | CLANG_CXX_LIBRARY = "libc++"; 661 | CLANG_ENABLE_MODULES = YES; 662 | CLANG_ENABLE_OBJC_ARC = YES; 663 | CLANG_ENABLE_OBJC_WEAK = YES; 664 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 665 | CLANG_WARN_BOOL_CONVERSION = YES; 666 | CLANG_WARN_COMMA = YES; 667 | CLANG_WARN_CONSTANT_CONVERSION = YES; 668 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 669 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 670 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 671 | CLANG_WARN_EMPTY_BODY = YES; 672 | CLANG_WARN_ENUM_CONVERSION = YES; 673 | CLANG_WARN_INFINITE_RECURSION = YES; 674 | CLANG_WARN_INT_CONVERSION = YES; 675 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 676 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 677 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 678 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 679 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 680 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 681 | CLANG_WARN_STRICT_PROTOTYPES = YES; 682 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 683 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 684 | CLANG_WARN_UNREACHABLE_CODE = YES; 685 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 686 | COPY_PHASE_STRIP = NO; 687 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 688 | ENABLE_NS_ASSERTIONS = NO; 689 | ENABLE_STRICT_OBJC_MSGSEND = YES; 690 | GCC_C_LANGUAGE_STANDARD = gnu11; 691 | GCC_NO_COMMON_BLOCKS = YES; 692 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 693 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 694 | GCC_WARN_UNDECLARED_SELECTOR = YES; 695 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 696 | GCC_WARN_UNUSED_FUNCTION = YES; 697 | GCC_WARN_UNUSED_VARIABLE = YES; 698 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 699 | MTL_ENABLE_DEBUG_INFO = NO; 700 | MTL_FAST_MATH = YES; 701 | SDKROOT = iphoneos; 702 | SWIFT_COMPILATION_MODE = wholemodule; 703 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 704 | VALIDATE_PRODUCT = YES; 705 | }; 706 | name = Release; 707 | }; 708 | 191E1A002357371E00D8963A /* Debug */ = { 709 | isa = XCBuildConfiguration; 710 | buildSettings = { 711 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 712 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 713 | CODE_SIGN_ENTITLEMENTS = "BoxOffice iOS.entitlements"; 714 | CODE_SIGN_STYLE = Automatic; 715 | DEVELOPMENT_ASSET_PATHS = "\"iOS/Resources/Preview Content\""; 716 | DEVELOPMENT_TEAM = H79MF628K3; 717 | ENABLE_PREVIEWS = YES; 718 | INFOPLIST_FILE = iOS/Info.plist; 719 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 720 | LD_RUNPATH_SEARCH_PATHS = ( 721 | "$(inherited)", 722 | "@executable_path/Frameworks", 723 | ); 724 | PRODUCT_BUNDLE_IDENTIFIER = "com.presto.BoxOffice-SwiftUI"; 725 | PRODUCT_NAME = "$(TARGET_NAME)"; 726 | SUPPORTS_MACCATALYST = NO; 727 | SWIFT_VERSION = 5.0; 728 | TARGETED_DEVICE_FAMILY = 1; 729 | }; 730 | name = Debug; 731 | }; 732 | 191E1A012357371E00D8963A /* Release */ = { 733 | isa = XCBuildConfiguration; 734 | buildSettings = { 735 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 736 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 737 | CODE_SIGN_ENTITLEMENTS = "BoxOffice iOS.entitlements"; 738 | CODE_SIGN_STYLE = Automatic; 739 | DEVELOPMENT_ASSET_PATHS = "\"iOS/Resources/Preview Content\""; 740 | DEVELOPMENT_TEAM = H79MF628K3; 741 | ENABLE_PREVIEWS = YES; 742 | INFOPLIST_FILE = iOS/Info.plist; 743 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 744 | LD_RUNPATH_SEARCH_PATHS = ( 745 | "$(inherited)", 746 | "@executable_path/Frameworks", 747 | ); 748 | PRODUCT_BUNDLE_IDENTIFIER = "com.presto.BoxOffice-SwiftUI"; 749 | PRODUCT_NAME = "$(TARGET_NAME)"; 750 | SUPPORTS_MACCATALYST = NO; 751 | SWIFT_VERSION = 5.0; 752 | TARGETED_DEVICE_FAMILY = 1; 753 | }; 754 | name = Release; 755 | }; 756 | /* End XCBuildConfiguration section */ 757 | 758 | /* Begin XCConfigurationList section */ 759 | 19067A03268E0EDE004C6FEC /* Build configuration list for PBXNativeTarget "BoxOffice iOS Widgets" */ = { 760 | isa = XCConfigurationList; 761 | buildConfigurations = ( 762 | 19067A04268E0EDE004C6FEC /* Debug */, 763 | 19067A05268E0EDE004C6FEC /* Release */, 764 | ); 765 | defaultConfigurationIsVisible = 0; 766 | defaultConfigurationName = Release; 767 | }; 768 | 191E19E62357371D00D8963A /* Build configuration list for PBXProject "BoxOffice_SwiftUI" */ = { 769 | isa = XCConfigurationList; 770 | buildConfigurations = ( 771 | 191E19FD2357371E00D8963A /* Debug */, 772 | 191E19FE2357371E00D8963A /* Release */, 773 | ); 774 | defaultConfigurationIsVisible = 0; 775 | defaultConfigurationName = Release; 776 | }; 777 | 191E19FF2357371E00D8963A /* Build configuration list for PBXNativeTarget "BoxOffice" */ = { 778 | isa = XCConfigurationList; 779 | buildConfigurations = ( 780 | 191E1A002357371E00D8963A /* Debug */, 781 | 191E1A012357371E00D8963A /* Release */, 782 | ); 783 | defaultConfigurationIsVisible = 0; 784 | defaultConfigurationName = Release; 785 | }; 786 | /* End XCConfigurationList section */ 787 | }; 788 | rootObject = 191E19E32357371D00D8963A /* Project object */; 789 | } 790 | -------------------------------------------------------------------------------- /BoxOffice_SwiftUI.xcodeproj/xcshareddata/xcschemes/BoxOffice iOS Widgets.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 60 | 62 | 68 | 69 | 70 | 71 | 75 | 76 | 80 | 81 | 85 | 86 | 87 | 88 | 96 | 98 | 104 | 105 | 106 | 107 | 109 | 110 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /BoxOffice_SwiftUI.xcodeproj/xcshareddata/xcschemes/BoxOffice.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BoxOffice_SwiftUI 2 | 3 | Boostcourse iOS 앱 프로그래밍 과정의 마지막 프로젝트 [BoxOffice](https://www.boostcourse.org/mo326/project/24/content/22)를 SwiftUI와 Combine을 사용하여 개발 4 | 5 | ![1](./images/1.png) ![2](./images/2.png) 6 | ![3](./images/3.png) ![4](./images/4.png) 7 | ![5](./images/5.png) 8 | -------------------------------------------------------------------------------- /iOS Widgets/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iOS Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /iOS Widgets/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iOS Widgets/BoxOfficeWidgetBundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoxOfficeWidgetBundle.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2021/07/01. 6 | // Copyright © 2021 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import WidgetKit 11 | 12 | @main 13 | struct BoxOfficeWidgetBundle: WidgetBundle { 14 | var body: some Widget { 15 | SampleWidget() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /iOS Widgets/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | BoxOffice iOS Widgets 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | NSExtension 24 | 25 | NSExtensionPointIdentifier 26 | com.apple.widgetkit-extension 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /iOS Widgets/SampleWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleWidget.swift 3 | // BoxOffice 4 | // 5 | // Created by Presto on 2021/07/01. 6 | // Copyright © 2021 presto. All rights reserved. 7 | // 8 | 9 | import WidgetKit 10 | import SwiftUI 11 | 12 | struct SampleWidget: Widget { 13 | var body: some WidgetConfiguration { 14 | StaticConfiguration(kind: "Sample", provider: Provider()) { entry in 15 | Text("Sample") 16 | } 17 | .configurationDisplayName("BoxOffice") 18 | .description("Sample BoxOffice") 19 | .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) 20 | } 21 | } 22 | 23 | extension SampleWidget { 24 | struct Provider: TimelineProvider { 25 | typealias Entry = SampleWidget.Entry 26 | 27 | func placeholder(in context: Context) -> SampleWidget.Entry { 28 | return Entry(date: Date()) 29 | } 30 | 31 | func getSnapshot(in context: Context, completion: @escaping (SampleWidget.Entry) -> Void) { 32 | completion(Entry(date: Date())) 33 | } 34 | 35 | func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { 36 | let entry = Entry(date: Date()) 37 | let timeline = Timeline(entries: [entry], policy: .never) 38 | completion(timeline) 39 | } 40 | } 41 | 42 | struct Entry: TimelineEntry { 43 | var date: Date 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /iOS/BoxOfficeApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoxOfficeApp.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2021/06/21. 6 | // Copyright © 2021 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @main 12 | struct BoxOfficeApp: App { 13 | init() { 14 | DIContainer.shared.register(NetworkManager()) 15 | DIContainer.shared.register(APIService()) 16 | } 17 | 18 | var body: some Scene { 19 | WindowGroup { 20 | MovieMainView(viewModel: MovieMainViewModel()) 21 | .accentColor(.purple) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /iOS/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 | LSApplicationCategoryType 22 | public.app-category.entertainment 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | UIApplicationSceneManifest 31 | 32 | UIApplicationSupportsMultipleScenes 33 | 34 | 35 | UILaunchScreen 36 | 37 | UINavigationBar 38 | 39 | UIImageName 40 | 41 | 42 | UITabBar 43 | 44 | UIImageName 45 | 46 | 47 | 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "NotificationIcon@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "NotificationIcon@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-Small.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-Small@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-Small@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "57x57", 47 | "idiom" : "iphone", 48 | "filename" : "Icon.png", 49 | "scale" : "1x" 50 | }, 51 | { 52 | "size" : "57x57", 53 | "idiom" : "iphone", 54 | "filename" : "Icon@2x.png", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "size" : "60x60", 59 | "idiom" : "iphone", 60 | "filename" : "Icon-60@2x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "60x60", 65 | "idiom" : "iphone", 66 | "filename" : "Icon-60@3x.png", 67 | "scale" : "3x" 68 | }, 69 | { 70 | "size" : "20x20", 71 | "idiom" : "ipad", 72 | "filename" : "NotificationIcon~ipad.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "20x20", 77 | "idiom" : "ipad", 78 | "filename" : "NotificationIcon~ipad@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "29x29", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-Small.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "29x29", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-Small@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "40x40", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-40.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "40x40", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-40@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "50x50", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-Small-50.png", 109 | "scale" : "1x" 110 | }, 111 | { 112 | "size" : "50x50", 113 | "idiom" : "ipad", 114 | "filename" : "Icon-Small-50@2x.png", 115 | "scale" : "2x" 116 | }, 117 | { 118 | "size" : "72x72", 119 | "idiom" : "ipad", 120 | "filename" : "Icon-72.png", 121 | "scale" : "1x" 122 | }, 123 | { 124 | "size" : "72x72", 125 | "idiom" : "ipad", 126 | "filename" : "Icon-72@2x.png", 127 | "scale" : "2x" 128 | }, 129 | { 130 | "size" : "76x76", 131 | "idiom" : "ipad", 132 | "filename" : "Icon-76.png", 133 | "scale" : "1x" 134 | }, 135 | { 136 | "size" : "76x76", 137 | "idiom" : "ipad", 138 | "filename" : "Icon-76@2x.png", 139 | "scale" : "2x" 140 | }, 141 | { 142 | "size" : "83.5x83.5", 143 | "idiom" : "ipad", 144 | "filename" : "Icon-83.5@2x.png", 145 | "scale" : "2x" 146 | }, 147 | { 148 | "size" : "1024x1024", 149 | "idiom" : "ios-marketing", 150 | "filename" : "ios-marketing.png", 151 | "scale" : "1x" 152 | } 153 | ], 154 | "info" : { 155 | "version" : 1, 156 | "author" : "xcode" 157 | } 158 | } -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-72.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/AppIcon.appiconset/ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/AppIcon.appiconset/ios-marketing.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/Connect_logo_w.imageset/Connect_logo_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/Connect_logo_w.imageset/Connect_logo_w.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/Connect_logo_w.imageset/Connect_logo_w@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/Connect_logo_w.imageset/Connect_logo_w@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/Connect_logo_w.imageset/Connect_logo_w@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/Connect_logo_w.imageset/Connect_logo_w@3x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/Connect_logo_w.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Connect_logo_w.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Connect_logo_w@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Connect_logo_w@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_12.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_12.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_12@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_12@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_12.imageset/ic_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_12.imageset/ic_12.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_12.imageset/ic_12@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_12.imageset/ic_12@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_12.imageset/ic_12@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_12.imageset/ic_12@3x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_15.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_15.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_15@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_15@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_15.imageset/ic_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_15.imageset/ic_15.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_15.imageset/ic_15@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_15.imageset/ic_15@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_15.imageset/ic_15@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_15.imageset/ic_15@3x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_19.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_19.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_19@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_19@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_19.imageset/ic_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_19.imageset/ic_19.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_19.imageset/ic_19@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_19.imageset/ic_19@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_19.imageset/ic_19@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_19.imageset/ic_19@3x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_allages.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_allages.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_allages@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_allages@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_allages.imageset/ic_allages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_allages.imageset/ic_allages.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_allages.imageset/ic_allages@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_allages.imageset/ic_allages@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_allages.imageset/ic_allages@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_allages.imageset/ic_allages@3x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_star_large.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ic_star_large.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ic_star_large@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "ic_star_large@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_star_large.imageset/ic_star_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_star_large.imageset/ic_star_large.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_star_large.imageset/ic_star_large@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_star_large.imageset/ic_star_large@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_star_large.imageset/ic_star_large@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_star_large.imageset/ic_star_large@3x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_star_large_full.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ic_star_large_full.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ic_star_large_full@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "ic_star_large_full@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_star_large_full.imageset/ic_star_large_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_star_large_full.imageset/ic_star_large_full.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_star_large_full.imageset/ic_star_large_full@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_star_large_full.imageset/ic_star_large_full@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_star_large_full.imageset/ic_star_large_full@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_star_large_full.imageset/ic_star_large_full@3x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_star_large_half.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ic_star_large_half.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ic_star_large_half@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "ic_star_large_half@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_star_large_half.imageset/ic_star_large_half.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_star_large_half.imageset/ic_star_large_half.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_star_large_half.imageset/ic_star_large_half@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_star_large_half.imageset/ic_star_large_half@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/ic_star_large_half.imageset/ic_star_large_half@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/ic_star_large_half.imageset/ic_star_large_half@3x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/img_splash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "img_splash.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "img_splash@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "img_splash@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/img_splash.imageset/img_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/img_splash.imageset/img_splash.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/img_splash.imageset/img_splash@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/img_splash.imageset/img_splash@2x.png -------------------------------------------------------------------------------- /iOS/Resources/Assets.xcassets/img_splash.imageset/img_splash@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/iOS/Resources/Assets.xcassets/img_splash.imageset/img_splash@3x.png -------------------------------------------------------------------------------- /iOS/Resources/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /iOS/Sources/Common/Cache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCache.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/23. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol Cache { 12 | associatedtype Key 13 | associatedtype Value 14 | 15 | static func add(_ value: Value, forKey key: Key) 16 | static func remove(forKey key: Key) 17 | static func value(forKey key: Key) -> Value? 18 | } 19 | 20 | final class ImageCache: Cache { 21 | private static let cache = NSCache() 22 | 23 | static func add(_ value: Data, forKey key: String) { 24 | cache.setObject(value as NSData, forKey: key as NSString) 25 | } 26 | 27 | static func remove(forKey key: String) { 28 | cache.removeObject(forKey: key as NSString) 29 | } 30 | 31 | static func value(forKey key: String) -> Data? { 32 | guard let data = cache.object(forKey: key as NSString) else { return nil } 33 | return Data(referencing: data) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /iOS/Sources/Common/DIContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DIContainer.swift 3 | // BoxOffice 4 | // 5 | // Created by Presto on 2021/07/11. 6 | // Copyright © 2021 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class DIContainer { 12 | private var dependencies: [String: Any] = [:] 13 | private init() {} 14 | 15 | static let shared = DIContainer() 16 | 17 | func register(_ dependency: Dependency) { 18 | let key = self.key(dependency) 19 | register(dependency, forKey: key) 20 | } 21 | 22 | func register(_ dependency: Dependency, forKey key: Key) { 23 | let key = self.key(key) 24 | register(dependency, forKey: key) 25 | } 26 | 27 | func resolve(_ dependency: Dependency.Type) -> Dependency? { 28 | let key = self.key(dependency) 29 | return resolve(dependency, forKey: key) 30 | } 31 | 32 | func resolve(_ dependency: Dependency.Type, forKey key: Key) -> Dependency? { 33 | let key = self.key(key) 34 | return resolve(dependency, forKey: key) 35 | } 36 | } 37 | 38 | private extension DIContainer { 39 | func key(_ value: Value) -> String { 40 | return String(describing: value).components(separatedBy: ".").last ?? "" 41 | } 42 | 43 | func register(_ dependency: Dependency, forKey key: String) { 44 | if dependencies[key] == nil { 45 | dependencies[key] = dependency 46 | } else { 47 | #if DEBUG 48 | print("Dependency for '\(key)' is already registered. It will be replaced.") 49 | #endif 50 | dependencies[key] = dependency 51 | } 52 | } 53 | 54 | func resolve(_ dependency: Dependency.Type, forKey key: String) -> Dependency? { 55 | if let dependency = dependencies[key] as? Dependency { 56 | return dependency 57 | } else { 58 | #if DEBUG 59 | print("Dependency for '\(key)' is not registered.") 60 | #endif 61 | return nil 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /iOS/Sources/Common/Grade.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Grade.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/17. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | enum Grade: Int { 10 | case allAges = 0 11 | case twelve = 12 12 | case fifteen = 15 13 | case nineteen = 19 14 | } 15 | 16 | extension Grade { 17 | var imageName: String { 18 | switch self { 19 | case .allAges: 20 | return "ic_allages" 21 | case .twelve: 22 | return "ic_12" 23 | case .fifteen: 24 | return "ic_15" 25 | case .nineteen: 26 | return "ic_19" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /iOS/Sources/Common/SortMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SortMethod.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/16. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | enum SortMethod: Int { 10 | case reservation 11 | case curation 12 | case date 13 | } 14 | 15 | extension SortMethod: CustomStringConvertible { 16 | var description: String { 17 | switch self { 18 | case .reservation: 19 | return "예매율순" 20 | case .curation: 21 | return "큐레이션" 22 | case .date: 23 | return "개봉일순" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /iOS/Sources/Common/StarType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StarType.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/17. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | enum StarType { 10 | case normal 11 | case half 12 | case full 13 | } 14 | 15 | extension StarType { 16 | var imageName: String { 17 | switch self { 18 | case .normal: 19 | return "ic_star_large" 20 | case .half: 21 | return "ic_star_large_half" 22 | case .full: 23 | return "ic_star_large_full" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /iOS/Sources/Extension/DateFormatter+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatter+.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/18. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension DateFormatter { 12 | static func custom(_ format: String) -> DateFormatter { 13 | let formatter = DateFormatter() 14 | formatter.dateFormat = format 15 | return formatter 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /iOS/Sources/Extension/NumberFormatter+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberFormatter+.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2020/06/30. 6 | // Copyright © 2020 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NumberFormatter { 12 | static let decimal: NumberFormatter = { 13 | let formatter = NumberFormatter() 14 | formatter.numberStyle = .decimal 15 | return formatter 16 | }() 17 | } 18 | -------------------------------------------------------------------------------- /iOS/Sources/Model/Comment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Comment.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/16. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | struct Comment: Encodable { 10 | let rating: Int 11 | let writer: String 12 | let movieID: String 13 | let contents: String 14 | 15 | private enum CodingKeys: String, CodingKey { 16 | case rating 17 | case writer 18 | case movieID = "movie_id" 19 | case contents 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/Sources/Model/CommentPostingResponseModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentResponse.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/18. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | struct CommentPostingResponseModel: Decodable { 10 | let rating: Int 11 | let timestamp: Double 12 | let writer: String 13 | let movieID: String 14 | let contents: String 15 | 16 | private enum CodingKeys: String, CodingKey { 17 | case rating 18 | case timestamp 19 | case writer 20 | case movieID = "movie_id" 21 | case contents 22 | } 23 | } 24 | 25 | extension CommentPostingResponseModel { 26 | static let dummy = CommentPostingResponseModel(rating: 0, 27 | timestamp: 0, 28 | writer: "Presto", 29 | movieID: "1", 30 | contents: "contents") 31 | } 32 | -------------------------------------------------------------------------------- /iOS/Sources/Model/CommentsResponseModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentsResponse.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/16. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | struct CommentsResponseModel: Decodable, Identifiable { 10 | struct Comment: Decodable, Identifiable { 11 | let movieID: String 12 | let contents: String 13 | let timestamp: Double 14 | let id: String 15 | let writer: String 16 | let rating: Double 17 | 18 | private enum CodingKeys: String, CodingKey { 19 | case movieID = "movie_id" 20 | case contents 21 | case timestamp 22 | case id 23 | case writer 24 | case rating 25 | } 26 | } 27 | 28 | var id: String { movieID } 29 | let comments: [Comment] 30 | let movieID: String 31 | 32 | private enum CodingKeys: String, CodingKey { 33 | case comments 34 | case movieID = "movie_id" 35 | } 36 | } 37 | 38 | extension CommentsResponseModel { 39 | static let dummy = CommentsResponseModel(comments: [.dummy], movieID: "1") 40 | } 41 | 42 | extension CommentsResponseModel.Comment { 43 | static let dummy = CommentsResponseModel.Comment(movieID: "1", 44 | contents: "컨텐츠", 45 | timestamp: 0, 46 | id: "1", 47 | writer: "Presto", 48 | rating: 8.4) 49 | } 50 | -------------------------------------------------------------------------------- /iOS/Sources/Model/MovieDetailResponseModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieResponse.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/16. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | struct MovieDetailResponseModel: Decodable, Identifiable { 10 | let userRating: Double 11 | let actor: String 12 | let director: String 13 | let date: String 14 | let grade: Int 15 | let reservationRate: Double 16 | let imageURLString: String 17 | let duration: Int 18 | let id: String 19 | let reservationGrade: Int 20 | let title: String 21 | let synopsis: String 22 | let audience: Int 23 | let genre: String 24 | 25 | private enum CodingKeys: String, CodingKey { 26 | case userRating = "user_rating" 27 | case actor 28 | case director 29 | case date 30 | case grade 31 | case reservationRate = "reservation_rate" 32 | case imageURLString = "image" 33 | case duration 34 | case id 35 | case reservationGrade = "reservation_grade" 36 | case title 37 | case synopsis 38 | case audience 39 | case genre 40 | } 41 | } 42 | 43 | extension MovieDetailResponseModel { 44 | static let dummy = MovieDetailResponseModel(userRating: 9.5, 45 | actor: "Presto", 46 | director: "Presto", 47 | date: "2020-02-02", 48 | grade: 7, 49 | reservationRate: 32.2, 50 | imageURLString: "", 51 | duration: 120, 52 | id: "1", 53 | reservationGrade: 1, 54 | title: "영화", 55 | synopsis: "줄거리", 56 | audience: 172948, 57 | genre: "장르") 58 | } 59 | -------------------------------------------------------------------------------- /iOS/Sources/Model/MoviesResponseModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoviesResponse.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/16. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct MoviesResponseModel: Decodable, Identifiable { 12 | struct Movie: Decodable, Identifiable, Equatable { 13 | let userRating: Double 14 | let grade: Int 15 | let date: String 16 | let reservationRate: Double 17 | let id: String 18 | let reservationGrade: Int 19 | let title: String 20 | let thumb: String 21 | 22 | private enum CodingKeys: String, CodingKey { 23 | case userRating = "user_rating" 24 | case grade 25 | case date 26 | case reservationRate = "reservation_rate" 27 | case id 28 | case reservationGrade = "reservation_grade" 29 | case title 30 | case thumb 31 | } 32 | } 33 | 34 | let id = UUID() 35 | let movies: [Movie] 36 | let sortMethod: Int 37 | 38 | private enum CodingKeys: String, CodingKey { 39 | case movies 40 | case sortMethod = "order_type" 41 | } 42 | } 43 | 44 | extension MoviesResponseModel.Movie { 45 | static let dummy = MoviesResponseModel.Movie(userRating: 9.7, 46 | grade: 12, 47 | date: "2020-06-29", 48 | reservationRate: 79.6, 49 | id: "1", 50 | reservationGrade: 1, 51 | title: "신과 함께-죄와벌", 52 | thumb: "") 53 | } 54 | 55 | extension MoviesResponseModel { 56 | static let dummy = MoviesResponseModel(movies: [.dummy], sortMethod: 0) 57 | } 58 | -------------------------------------------------------------------------------- /iOS/Sources/Network/APIError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIError.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2021/06/21. 6 | // Copyright © 2021 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum APIError: Error { 12 | case imageDataRequestFailed 13 | case moviesRequestFailed 14 | case movieDetailRequestFailed 15 | case commentsRequestFailed 16 | case commentPostingRequestFailed 17 | } 18 | 19 | extension APIError: LocalizedError { 20 | var localizedDescription: String { 21 | switch self { 22 | case .imageDataRequestFailed: 23 | return "이미지를 불러오지 못했습니다." 24 | case .moviesRequestFailed: 25 | return "영화 정보를 불러오지 못했습니다." 26 | case .commentsRequestFailed: 27 | return "한줄평 정보를 불러오지 못했습니다." 28 | case .movieDetailRequestFailed: 29 | return "영화 상세 정보를 불러오지 못했습니다." 30 | case .commentPostingRequestFailed: 31 | return "한줄평 등록에 실패했습니다." 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iOS/Sources/Network/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethod.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/23. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | enum HTTPMethod: String { 10 | case get = "GET" 11 | case post = "POST" 12 | } 13 | -------------------------------------------------------------------------------- /iOS/Sources/Network/Manager/MockNetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockNetworkManager.swift 3 | // BoxOffice 4 | // 5 | // Created by Presto on 2021/06/21. 6 | // Copyright © 2021 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | final class MockNetworkManager: NetworkManagerProtocol { 13 | func publisher(from target: Target) -> AnyPublisher { 14 | Just(Data()) 15 | .setFailureType(to: Error.self) 16 | .eraseToAnyPublisher() 17 | } 18 | 19 | func publisher(fromURLString urlString: String) -> AnyPublisher { 20 | Just(Data()) 21 | .setFailureType(to: Error.self) 22 | .eraseToAnyPublisher() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /iOS/Sources/Network/Manager/NetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkManager.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/23. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | final class NetworkManager: NetworkManagerProtocol { 13 | func publisher(from target: Target) -> AnyPublisher { 14 | var components = URLComponents() 15 | components.scheme = target.routerVersion.scheme 16 | components.host = target.routerVersion.host 17 | components.path = target.paths.map { "/\($0)" }.joined() 18 | components.queryItems = target.parameter?.map { URLQueryItem(name: $0.key, value: $0.value) } 19 | 20 | guard let url = components.url else { 21 | return Empty().eraseToAnyPublisher() 22 | } 23 | 24 | var request = URLRequest(url: url, timeoutInterval: 1) 25 | request.httpMethod = target.method.rawValue 26 | request.httpBody = target.body 27 | return URLSession.shared.dataTaskPublisher(for: request) 28 | .retry(1) 29 | .mapError { $0 as Error } 30 | .map(\.data) 31 | .eraseToAnyPublisher() 32 | } 33 | 34 | func publisher(fromURLString urlString: String) -> AnyPublisher { 35 | return Just(urlString) 36 | .compactMap(URL.init) 37 | .setFailureType(to: URLError.self) 38 | .receive(on: DispatchQueue.global(qos: .utility)) 39 | .flatMap(URLSession.shared.dataTaskPublisher(for:)) 40 | .retry(1) 41 | .receive(on: DispatchQueue.main) 42 | .map(\.data) 43 | .mapError { $0 as Error } 44 | .eraseToAnyPublisher() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /iOS/Sources/Network/Manager/NetworkManagerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkManagerProtocol.swift 3 | // BoxOffice 4 | // 5 | // Created by Presto on 2021/06/21. 6 | // Copyright © 2021 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | protocol NetworkManagerProtocol { 13 | func publisher(from target: Target) -> AnyPublisher 14 | func publisher(fromURLString urlString: String) -> AnyPublisher 15 | } 16 | -------------------------------------------------------------------------------- /iOS/Sources/Network/Service/APIService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIService.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/16. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | final class APIService: APIProtocol { 13 | private let networkManager: NetworkManagerProtocol? 14 | 15 | init() { 16 | self.networkManager = DIContainer.shared.resolve(NetworkManager.self) 17 | } 18 | 19 | func imageDataPublisher(fromURLString urlString: String) -> ImageDataPublisher { 20 | guard let networkManager = networkManager else { return Empty().eraseToAnyPublisher() } 21 | 22 | guard let imageData = ImageCache.value(forKey: urlString) else { 23 | return Just(urlString) 24 | .setFailureType(to: Error.self) 25 | .flatMap(networkManager.publisher(fromURLString:)) 26 | .mapError { _ in APIError.imageDataRequestFailed } 27 | .handleEvents(receiveOutput: { data in 28 | ImageCache.add(data, forKey: urlString) 29 | }) 30 | .eraseToAnyPublisher() 31 | } 32 | 33 | return Just(imageData) 34 | .setFailureType(to: APIError.self) 35 | .eraseToAnyPublisher() 36 | } 37 | 38 | func moviesPublisher(with sortMethod: SortMethod) -> MoviesPublisher { 39 | guard let networkManager = networkManager else { return Empty().eraseToAnyPublisher() } 40 | 41 | return Just(sortMethod) 42 | .map(\.rawValue) 43 | .map(String.init) 44 | .map { ["order_type": $0] } 45 | .map(MoviesTarget.init) 46 | .setFailureType(to: Error.self) 47 | .flatMap(networkManager.publisher(from:)) 48 | .decode(type: MoviesResponseModel.self, decoder: JSONDecoder()) 49 | .mapError { _ in APIError.moviesRequestFailed } 50 | .eraseToAnyPublisher() 51 | } 52 | 53 | func movieDetailPublisher(withMovieID movieID: String) -> MovieDetailPublisher { 54 | guard let networkManager = networkManager else { return Empty().eraseToAnyPublisher() } 55 | 56 | return Just(movieID) 57 | .map { ["id": $0] } 58 | .map(MovieTarget.init) 59 | .setFailureType(to: Error.self) 60 | .flatMap(networkManager.publisher(from:)) 61 | .decode(type: MovieDetailResponseModel.self, decoder: JSONDecoder()) 62 | .mapError { _ in APIError.movieDetailRequestFailed } 63 | .eraseToAnyPublisher() 64 | } 65 | 66 | func commentsPublisher(withMovieID movieID: String) -> CommentsPublisher { 67 | guard let networkManager = networkManager else { return Empty().eraseToAnyPublisher() } 68 | 69 | return Just(movieID) 70 | .map { ["movie_id": $0] } 71 | .map(CommentsTarget.init) 72 | .setFailureType(to: Error.self) 73 | .flatMap(networkManager.publisher(from:)) 74 | .decode(type: CommentsResponseModel.self, decoder: JSONDecoder()) 75 | .mapError { _ in APIError.commentsRequestFailed } 76 | .eraseToAnyPublisher() 77 | } 78 | 79 | func commentPostingPublisher(with comment: Comment) -> CommentPostingPublisher { 80 | guard let networkManager = networkManager else { return Empty().eraseToAnyPublisher() } 81 | 82 | return Just(comment) 83 | .encode(encoder: JSONEncoder()) 84 | .map(CommentPostingTarget.init) 85 | .flatMap(networkManager.publisher(from:)) 86 | .decode(type: CommentPostingResponseModel.self, decoder: JSONDecoder()) 87 | .mapError { _ in APIError.commentPostingRequestFailed } 88 | .eraseToAnyPublisher() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /iOS/Sources/Network/Service/APIServiceProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIProtocol.swift 3 | // BoxOffice 4 | // 5 | // Created by Presto on 2021/06/21. 6 | // Copyright © 2021 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | protocol APIProtocol { 13 | func imageDataPublisher(fromURLString urlString: String) -> ImageDataPublisher 14 | func moviesPublisher(with sortMethod: SortMethod) -> MoviesPublisher 15 | func movieDetailPublisher(withMovieID: String) -> MovieDetailPublisher 16 | func commentsPublisher(withMovieID: String) -> CommentsPublisher 17 | func commentPostingPublisher(with: Comment) -> CommentPostingPublisher 18 | } 19 | 20 | typealias ImageData = Data 21 | typealias ImageDataPublisher = AnyPublisher 22 | typealias MoviesPublisher = AnyPublisher 23 | typealias MovieDetailPublisher = AnyPublisher 24 | typealias CommentsPublisher = AnyPublisher 25 | typealias CommentPostingPublisher = AnyPublisher 26 | -------------------------------------------------------------------------------- /iOS/Sources/Network/Service/MockAPIService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockAPIService.swift 3 | // BoxOffice 4 | // 5 | // Created by Presto on 2021/06/21. 6 | // Copyright © 2021 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | final class MockAPIService: APIProtocol { 13 | private let networkManager: NetworkManagerProtocol = MockNetworkManager() 14 | 15 | func imageDataPublisher(fromURLString urlString: String) -> ImageDataPublisher { 16 | networkManager.publisher(fromURLString: urlString) 17 | .mapError { _ in APIError.imageDataRequestFailed } 18 | .eraseToAnyPublisher() 19 | } 20 | 21 | func moviesPublisher(with sortMethod: SortMethod) -> MoviesPublisher { 22 | Just(MoviesResponseModel.dummy) 23 | .setFailureType(to: APIError.self) 24 | .eraseToAnyPublisher() 25 | } 26 | 27 | func movieDetailPublisher(withMovieID: String) -> MovieDetailPublisher { 28 | Just(MovieDetailResponseModel.dummy) 29 | .setFailureType(to: APIError.self) 30 | .eraseToAnyPublisher() 31 | } 32 | 33 | func commentsPublisher(withMovieID: String) -> CommentsPublisher { 34 | Just(CommentsResponseModel.dummy) 35 | .setFailureType(to: APIError.self) 36 | .eraseToAnyPublisher() 37 | } 38 | 39 | func commentPostingPublisher(with: Comment) -> CommentPostingPublisher { 40 | Just(CommentPostingResponseModel.dummy) 41 | .setFailureType(to: APIError.self) 42 | .eraseToAnyPublisher() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /iOS/Sources/Network/Target.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Target.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/23. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol Target { 12 | var routerVersion: TargetVersion { get } 13 | var method: HTTPMethod { get } 14 | var paths: [String] { get } 15 | var parameter: [String: String]? { get } 16 | var body: Data? { get } 17 | } 18 | -------------------------------------------------------------------------------- /iOS/Sources/Network/Target/CommentPostingTarget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentPostingTarget.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/23. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct CommentPostingTarget: Target { 12 | var routerVersion: TargetVersion { .movieAPI } 13 | var method: HTTPMethod { .post } 14 | var paths: [String] { ["comment"] } 15 | var parameter: [String: String]? 16 | var body: Data? 17 | 18 | init(body: Data?) { 19 | self.body = body 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/Sources/Network/Target/CommentsTarget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentsTarget.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/23. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct CommentsTarget: Target { 12 | var routerVersion: TargetVersion { .movieAPI } 13 | var method: HTTPMethod { .get } 14 | var paths: [String] { ["comments"] } 15 | var parameter: [String: String]? 16 | var body: Data? 17 | 18 | init(parameter: [String: String]?) { 19 | self.parameter = parameter 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/Sources/Network/Target/MovieTarget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieTarget.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/23. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct MovieTarget: Target { 12 | var routerVersion: TargetVersion { .movieAPI } 13 | var method: HTTPMethod { .get } 14 | var paths: [String] { ["movie"] } 15 | var parameter: [String: String]? 16 | var body: Data? 17 | 18 | init(parameter: [String: String]?) { 19 | self.parameter = parameter 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/Sources/Network/Target/MoviesTarget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoviesTarget.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/23. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct MoviesTarget: Target { 12 | var routerVersion: TargetVersion { .movieAPI } 13 | var method: HTTPMethod { .get } 14 | var paths: [String] { ["movies"] } 15 | var parameter: [String: String]? 16 | var body: Data? 17 | 18 | init(parameter: [String: String]?) { 19 | self.parameter = parameter 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/Sources/Network/TargetVersion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TargetVersion.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/23. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | enum TargetVersion { 10 | case movieAPI 11 | } 12 | 13 | extension TargetVersion { 14 | var scheme: String { 15 | switch self { 16 | case .movieAPI: 17 | return "https" 18 | } 19 | } 20 | 21 | var host: String { 22 | switch self { 23 | case .movieAPI: 24 | return "connect-boxoffice.run.goorm.io" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /iOS/Sources/View/CommentPostingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentPostingView.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/16. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CommentPostingView: View { 12 | @ObservedObject private var viewModel: CommentPostingViewModel 13 | @Binding private var isPresented: Bool 14 | 15 | init(viewModel: CommentPostingViewModel, isPresented: Binding) { 16 | self.viewModel = viewModel 17 | _isPresented = isPresented 18 | } 19 | 20 | var body: some View { 21 | NavigationView { 22 | VStack { 23 | starRatingSection 24 | 25 | Divider() 26 | .padding(4) 27 | 28 | ratingFormSection 29 | } 30 | .navigationTitle("한줄평 작성") 31 | .navigationBarTitleDisplayMode(.inline) 32 | .toolbar { 33 | ToolbarItem(placement: .navigationBarLeading) { 34 | cancelButton 35 | } 36 | } 37 | .toolbar { 38 | ToolbarItem(placement: .navigationBarTrailing) { 39 | confirmButton 40 | } 41 | } 42 | .frame(maxWidth: .infinity, maxHeight: .infinity) 43 | .padding() 44 | .onReceive(viewModel.$isPostingFinished) { isPostingFinished in 45 | if isPostingFinished { 46 | isPresented.toggle() 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | // MARK: - View 54 | 55 | private extension CommentPostingView { 56 | var starRatingSection: some View { 57 | VStack { 58 | HStack { 59 | Text(viewModel.title) 60 | .font(.headline) 61 | 62 | Image(viewModel.gradeImageName) 63 | } 64 | 65 | VStack { 66 | StarRatingBar(score: viewModel.rating, length: 40) 67 | .gesture(starRatingBarDragGesture) 68 | 69 | Text(viewModel.ratingString) 70 | .font(.headline) 71 | } 72 | } 73 | } 74 | 75 | var ratingFormSection: some View { 76 | VStack { 77 | TextField("닉네임", text: $viewModel.nickname) 78 | .textFieldStyle(RoundedBorderTextFieldStyle()) 79 | 80 | TextField("한줄평을 작성해주세요", text: $viewModel.comments) 81 | .frame(maxHeight: .infinity, alignment: .topLeading) 82 | .lineLimit(nil) 83 | .textFieldStyle(RoundedBorderTextFieldStyle()) 84 | } 85 | .frame(maxHeight: .infinity) 86 | } 87 | 88 | var starRatingBarDragGesture: some Gesture { 89 | DragGesture() 90 | .onChanged { value in 91 | let x = max(0, min(value.location.x, 40 * 5)) 92 | viewModel.setRating(Double(x / 20)) 93 | } 94 | } 95 | 96 | var cancelButton: some View { 97 | Button("취소") { isPresented.toggle() } 98 | } 99 | 100 | var confirmButton: some View { 101 | Button("완료") { viewModel.requestCommentPosting() } 102 | } 103 | } 104 | 105 | // MARK: - Preview 106 | 107 | struct CommentsView_Previews: PreviewProvider { 108 | static var previews: some View { 109 | CommentPostingView(viewModel: CommentPostingViewModel(movie: .dummy), isPresented: .constant(false)) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /iOS/Sources/View/CommentPostingViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentPostingViewModel.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/16. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | final class CommentPostingViewModel: ObservableObject { 13 | @Published var nickname = "" 14 | @Published var comments = "" 15 | 16 | @Published private(set) var title: String = "" 17 | @Published private(set) var gradeImageName: String = "" 18 | @Published private(set) var rating: Double = 0 19 | @Published private(set) var ratingString: String = "" 20 | @Published private(set) var isPostingFinished: Bool = false 21 | 22 | private let movieSubject = CurrentValueSubject(nil) 23 | private let ratingSubject = CurrentValueSubject(nil) 24 | 25 | private var cancellables = Set() 26 | 27 | init(movie: MovieDetailResponseModel) { 28 | let movieSharedPublisher = movieSubject 29 | .compactMap { $0 } 30 | .share() 31 | 32 | movieSharedPublisher 33 | .map(\.title) 34 | .removeDuplicates() 35 | .assign(to: \.title, on: self) 36 | .store(in: &cancellables) 37 | 38 | movieSharedPublisher 39 | .map(\.grade) 40 | .removeDuplicates() 41 | .compactMap(Grade.init) 42 | .map(\.imageName) 43 | .assign(to: \.gradeImageName, on: self) 44 | .store(in: &cancellables) 45 | 46 | let ratingSharedPublisher = ratingSubject 47 | .compactMap { $0 } 48 | .share() 49 | 50 | ratingSharedPublisher 51 | .removeDuplicates() 52 | .map(Int.init) 53 | .map(String.init) 54 | .assign(to: \.ratingString, on: self) 55 | .store(in: &cancellables) 56 | 57 | ratingSharedPublisher 58 | .removeDuplicates() 59 | .assign(to: \.rating, on: self) 60 | .store(in: &cancellables) 61 | 62 | movieSubject.send(movie) 63 | ratingSubject.send(0) 64 | } 65 | 66 | func setRating(_ rating: Double) { 67 | ratingSubject.send(rating) 68 | } 69 | 70 | func requestCommentPosting() { 71 | let apiService = DIContainer.shared.resolve(APIService.self) 72 | let comment = Comment(rating: Int(rating), 73 | writer: nickname, 74 | movieID: movieSubject.value?.id ?? "", 75 | contents: comments) 76 | Just(comment) 77 | .flatMap { comment -> CommentPostingPublisher in 78 | guard let apiService = apiService else { 79 | return Empty().eraseToAnyPublisher() 80 | } 81 | return apiService.commentPostingPublisher(with: comment) 82 | .eraseToAnyPublisher() 83 | } 84 | .receive(on: DispatchQueue.main) 85 | .map { _ in true } 86 | .replaceError(with: true) 87 | .assign(to: \.isPostingFinished, on: self) 88 | .store(in: &cancellables) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /iOS/Sources/View/Extension/MovieMainViewNavigationBarStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieMainViewNavigationBarStyle.swift 3 | // BoxOffice 4 | // 5 | // Created by Presto on 2021/07/11. 6 | // Copyright © 2021 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MovieMainViewNavigationBarStyle: ViewModifier { 12 | func body(content: Content) -> some View { 13 | content 14 | .navigationTitle(Text("BoxOffice")) 15 | .navigationBarTitleDisplayMode(.automatic) 16 | } 17 | } 18 | 19 | extension View { 20 | func movieMainViewNavigationBarStyle() -> some View { 21 | return self 22 | .modifier(MovieMainViewNavigationBarStyle()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /iOS/Sources/View/Extension/Text+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Text+.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2020/06/30. 6 | // Copyright © 2020 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Text { 12 | func titleStyle() -> some View { 13 | return self 14 | .font(.title3) 15 | .fontWeight(.bold) 16 | .lineLimit(2) 17 | } 18 | 19 | func contentsStyle() -> some View { 20 | return self 21 | .font(.subheadline) 22 | .fontWeight(.medium) 23 | .foregroundColor(.secondary) 24 | .lineLimit(1) 25 | .minimumScaleFactor(0.5) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /iOS/Sources/View/MovieDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieDetailView.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/16. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MovieDetailView: View { 12 | @ObservedObject private var viewModel: MovieDetailViewModel 13 | 14 | init(viewModel: MovieDetailViewModel) { 15 | self.viewModel = viewModel 16 | } 17 | 18 | var body: some View { 19 | NavigationView { 20 | content 21 | .navigationTitle(viewModel.title) 22 | .navigationBarTitleDisplayMode(.inline) 23 | .toolbar { 24 | ToolbarItem(placement: .navigationBarTrailing) { 25 | Button(action: viewModel.requestData) { 26 | Image(systemName: "arrow.triangle.2.circlepath") 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | private extension MovieDetailView { 35 | @ViewBuilder var content: some View { 36 | if viewModel.data != nil { 37 | ScrollView { 38 | VStack(alignment: .leading) { 39 | summarySection 40 | 41 | Divider() 42 | .padding(.horizontal, 16) 43 | 44 | synopsisSection 45 | 46 | Divider() 47 | .padding(.horizontal, 16) 48 | 49 | actorSection 50 | 51 | Divider() 52 | .padding(.horizontal, 16) 53 | 54 | ratingSection 55 | } 56 | } 57 | } else if viewModel.movieErrors.isEmpty == false { 58 | MovieRetryView(errors: viewModel.movieErrors, onRetry: viewModel.requestData) 59 | } else { 60 | CircularProgressView() 61 | .onAppear { 62 | viewModel.requestData() 63 | } 64 | } 65 | } 66 | } 67 | 68 | // MARK: - Section: Summary 69 | 70 | private extension MovieDetailView { 71 | var summarySection: some View { 72 | VStack(alignment: .leading, spacing: 20) { 73 | summaryMainSection 74 | 75 | summarySubSection 76 | } 77 | .padding() 78 | } 79 | 80 | var summaryMainSection: some View { 81 | HStack { 82 | PosterImage(data: viewModel.posterImageData) 83 | .cornerRadius(5) 84 | .aspectRatio(61 / 91, contentMode: .fit) 85 | 86 | VStack(alignment: .leading) { 87 | MultipleSpacer(count: 2) 88 | 89 | HStack { 90 | Text(viewModel.title) 91 | .titleStyle() 92 | 93 | Image(viewModel.gradeImageName) 94 | } 95 | 96 | Spacer() 97 | 98 | VStack(alignment: .leading, spacing: 4) { 99 | Text(viewModel.date) 100 | .contentsStyle() 101 | 102 | Text(viewModel.genreAndDuration) 103 | .contentsStyle() 104 | } 105 | 106 | MultipleSpacer(count: 2) 107 | } 108 | } 109 | .frame(height: 170) 110 | } 111 | 112 | var summarySubSection: some View { 113 | HStack { 114 | Spacer() 115 | 116 | VStack { 117 | Spacer() 118 | 119 | Text("예매율") 120 | .font(.headline) 121 | 122 | Spacer() 123 | 124 | Text(viewModel.reservationMetric) 125 | .font(.footnote) 126 | .foregroundColor(.secondary) 127 | 128 | Spacer() 129 | } 130 | 131 | Group { 132 | Spacer() 133 | 134 | Divider() 135 | 136 | Spacer() 137 | } 138 | 139 | VStack { 140 | Spacer() 141 | 142 | Text("평점") 143 | .font(.headline) 144 | 145 | Spacer() 146 | 147 | Text(viewModel.userRatingDescription) 148 | .font(.footnote) 149 | .foregroundColor(.secondary) 150 | 151 | Spacer() 152 | 153 | StarRatingBar(score: viewModel.userRating) 154 | 155 | Spacer() 156 | } 157 | 158 | Group { 159 | Spacer() 160 | 161 | Divider() 162 | 163 | Spacer() 164 | } 165 | 166 | VStack { 167 | Spacer() 168 | 169 | Text("누적관객수") 170 | .font(.headline) 171 | 172 | Spacer() 173 | 174 | Text(viewModel.audience) 175 | .font(.footnote) 176 | .foregroundColor(.secondary) 177 | 178 | Spacer() 179 | } 180 | 181 | Spacer() 182 | } 183 | .frame(height: 70) 184 | } 185 | } 186 | 187 | // MARK: - Section: Info 188 | 189 | private extension MovieDetailView { 190 | var synopsisSection: some View { 191 | VStack(alignment: .leading, spacing: 8) { 192 | Text("줄거리") 193 | .font(.headline) 194 | 195 | Text(viewModel.synopsis) 196 | .font(.footnote) 197 | .fontWeight(.medium) 198 | .padding(.leading, 4) 199 | } 200 | .padding() 201 | } 202 | 203 | var actorSection: some View { 204 | VStack(alignment: .leading, spacing: 8) { 205 | Text("감독/출연") 206 | .font(.headline) 207 | 208 | HStack { 209 | Text("감독") 210 | .font(.subheadline) 211 | .fontWeight(.semibold) 212 | 213 | Text(viewModel.director) 214 | .font(.footnote) 215 | .fontWeight(.medium) 216 | } 217 | .padding(.leading, 4) 218 | 219 | HStack(alignment: .firstTextBaseline) { 220 | Text("출연") 221 | .font(.subheadline) 222 | .fontWeight(.semibold) 223 | 224 | Text(viewModel.actor) 225 | .font(.footnote) 226 | .fontWeight(.medium) 227 | } 228 | .padding(.leading, 4) 229 | } 230 | .padding() 231 | } 232 | } 233 | 234 | // MARK: - Section: Rating 235 | 236 | private extension MovieDetailView { 237 | var ratingSection: some View { 238 | VStack(alignment: .leading, spacing: 8) { 239 | HStack { 240 | Text("한줄평") 241 | .font(.headline) 242 | 243 | Spacer() 244 | 245 | Button(action: viewModel.setShowsCommentPosting) { 246 | Image(systemName: "square.and.pencil") 247 | .accentColor(.orange) 248 | } 249 | } 250 | 251 | ratingContentsSection 252 | } 253 | .padding() 254 | .sheet(isPresented: $viewModel.showsCommentPostingView) { 255 | if let movieDetail = viewModel.data?.movieDetail { 256 | let viewModel = CommentPostingViewModel(movie: movieDetail) 257 | 258 | CommentPostingView(viewModel: viewModel, isPresented: $viewModel.showsCommentPostingView) 259 | .accentColor(.purple) 260 | } 261 | } 262 | } 263 | 264 | @ViewBuilder var ratingContentsSection: some View { 265 | if let comments = viewModel.data?.comments { 266 | ForEach(comments) { comment in 267 | HStack(alignment: .top) { 268 | Image(systemName: "person.circle") 269 | .resizable() 270 | .aspectRatio(1, contentMode: .fit) 271 | .frame(height: 50) 272 | 273 | VStack(alignment: .leading, spacing: 4) { 274 | VStack(alignment: .leading) { 275 | HStack { 276 | Text(comment.writer) 277 | .font(.subheadline) 278 | .fontWeight(.semibold) 279 | 280 | StarRatingBar(score: comment.rating, length: 15) 281 | } 282 | 283 | Text(viewModel.commentDateString(timestamp: comment.timestamp)) 284 | .font(.footnote) 285 | .foregroundColor(.secondary) 286 | } 287 | 288 | Text(comment.contents) 289 | .font(.footnote) 290 | .fontWeight(.medium) 291 | } 292 | } 293 | .padding(.vertical, 8) 294 | } 295 | } else { 296 | EmptyView() 297 | } 298 | } 299 | } 300 | 301 | // MARK: - Preview 302 | 303 | struct MovieDetailView_Previews: PreviewProvider { 304 | static var previews: some View { 305 | MovieDetailView(viewModel: MovieDetailViewModel(movieID: "", movieTitle: "")) 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /iOS/Sources/View/MovieDetailViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieDetailViewModel.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/16. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | final class MovieDetailViewModel: ObservableObject { 13 | @Published var showsCommentPostingView = false 14 | 15 | @Published private(set) var data: (movieDetail: MovieDetailResponseModel, comments: [CommentsResponseModel.Comment])? 16 | @Published private(set) var posterImageData: Data? 17 | @Published private(set) var movieErrors: [APIError] = [] 18 | 19 | @Published private(set) var title: String = "-" 20 | @Published private(set) var gradeImageName: String = "" 21 | @Published private(set) var date: String = "-" 22 | @Published private(set) var genreAndDuration: String = "-" 23 | @Published private(set) var reservationMetric: String = "-" 24 | @Published private(set) var userRating: Double = 0 25 | @Published private(set) var userRatingDescription: String = "-" 26 | @Published private(set) var audience: String = "-" 27 | @Published private(set) var synopsis: String = "-" 28 | @Published private(set) var director: String = "-" 29 | @Published private(set) var actor: String = "-" 30 | 31 | private let showsCommentPostingSubject = CurrentValueSubject(nil) 32 | private let movieIDSubject = CurrentValueSubject(nil) 33 | private let dataSubject = CurrentValueSubject<(movieDetail: MovieDetailResponseModel, comments: CommentsResponseModel)?, Never>(nil) 34 | 35 | private var isLoading = false 36 | private var cancellables = Set() 37 | 38 | init(movieID: String, movieTitle: String) { 39 | self.title = movieTitle 40 | 41 | let apiService = DIContainer.shared.resolve(APIService.self) 42 | 43 | showsCommentPostingSubject 44 | .compactMap { $0 } 45 | .removeDuplicates() 46 | .assign(to: \.showsCommentPostingView, on: self) 47 | .store(in: &cancellables) 48 | 49 | let dataPublisher = dataSubject 50 | .compactMap { $0 } 51 | .share() 52 | 53 | let movieDetailPublisher = dataPublisher 54 | .map(\.movieDetail) 55 | 56 | dataPublisher 57 | .sink(receiveCompletion: { [weak self] completion in 58 | self?.movieErrors.append(.movieDetailRequestFailed) 59 | }, receiveValue: { [weak self] movieDetail, comments in 60 | self?.data = (movieDetail, comments.comments) 61 | }) 62 | .store(in: &cancellables) 63 | 64 | movieDetailPublisher 65 | .map(\.imageURLString) 66 | .flatMap { imageURLString -> ImageDataPublisher in 67 | guard let apiService = apiService else { 68 | return Empty().eraseToAnyPublisher() 69 | } 70 | return apiService.imageDataPublisher(fromURLString: imageURLString) 71 | } 72 | .replaceError(with: Data()) 73 | .compactMap { $0 } 74 | .assign(to: \.posterImageData, on: self) 75 | .store(in: &cancellables) 76 | 77 | movieDetailPublisher 78 | .map(\.title) 79 | .removeDuplicates() 80 | .assign(to: \.title, on: self) 81 | .store(in: &cancellables) 82 | 83 | movieDetailPublisher 84 | .map(\.grade) 85 | .removeDuplicates() 86 | .compactMap(Grade.init) 87 | .map(\.imageName) 88 | .assign(to: \.gradeImageName, on: self) 89 | .store(in: &cancellables) 90 | 91 | movieDetailPublisher 92 | .map(\.date) 93 | .removeDuplicates() 94 | .map { "\($0) 개봉" } 95 | .assign(to: \.date, on: self) 96 | .store(in: &cancellables) 97 | 98 | movieDetailPublisher 99 | .map { (genre: $0.genre, duration: $0.duration) } 100 | .removeDuplicates { $0.genre == $1.genre && $0.duration == $1.duration } 101 | .map { "\($0) / \($1)분" } 102 | .assign(to: \.genreAndDuration, on: self) 103 | .store(in: &cancellables) 104 | 105 | movieDetailPublisher 106 | .map { (grade: $0.reservationGrade, rate: $0.reservationRate) } 107 | .removeDuplicates { $0.grade == $1.grade && $0.rate == $1.rate } 108 | .map { "\($0)위 \(String(format: "%.1f%%", $1))" } 109 | .assign(to: \.reservationMetric, on: self) 110 | .store(in: &cancellables) 111 | 112 | movieDetailPublisher 113 | .map(\.userRating) 114 | .removeDuplicates() 115 | .assign(to: \.userRating, on: self) 116 | .store(in: &cancellables) 117 | 118 | movieDetailPublisher 119 | .map(\.userRating) 120 | .removeDuplicates() 121 | .map { String(format: "%.2f", $0) } 122 | .assign(to: \.userRatingDescription, on: self) 123 | .store(in: &cancellables) 124 | 125 | movieDetailPublisher 126 | .map(\.audience) 127 | .removeDuplicates() 128 | .compactMap { NumberFormatter.decimal.string(from: $0 as NSNumber) } 129 | .assign(to: \.audience, on: self) 130 | .store(in: &cancellables) 131 | 132 | movieDetailPublisher 133 | .map(\.synopsis) 134 | .removeDuplicates() 135 | .assign(to: \.synopsis, on: self) 136 | .store(in: &cancellables) 137 | 138 | movieDetailPublisher 139 | .map(\.director) 140 | .removeDuplicates() 141 | .assign(to: \.director, on: self) 142 | .store(in: &cancellables) 143 | 144 | movieDetailPublisher 145 | .map(\.actor) 146 | .removeDuplicates() 147 | .assign(to: \.actor, on: self) 148 | .store(in: &cancellables) 149 | 150 | movieIDSubject.send(movieID) 151 | } 152 | 153 | // MARK: - Inputs 154 | 155 | func setShowsCommentPosting() { 156 | showsCommentPostingSubject.send(true) 157 | } 158 | 159 | func requestData() { 160 | guard isLoading == false else { return } 161 | 162 | movieErrors.removeAll() 163 | isLoading = true 164 | 165 | if let apiService = DIContainer.shared.resolve(APIService.self), let movieID = movieIDSubject.value { 166 | Publishers.Zip(apiService.movieDetailPublisher(withMovieID: movieID), 167 | apiService.commentsPublisher(withMovieID: movieID)) 168 | .receive(on: DispatchQueue.main) 169 | .sink(receiveCompletion: { [weak self] completion in 170 | switch completion { 171 | case let .failure(error): 172 | self?.movieErrors.append(error) 173 | case .finished: 174 | break 175 | } 176 | 177 | self?.isLoading = false 178 | }, receiveValue: { [weak self] movieDetail, comments in 179 | self?.dataSubject.send((movieDetail, comments)) 180 | }) 181 | .store(in: &cancellables) 182 | } 183 | } 184 | } 185 | 186 | // MARK: - Private Method 187 | 188 | extension MovieDetailViewModel { 189 | func commentDateString(timestamp: Double) -> String { 190 | let formatter = DateFormatter.custom("yyyy-MM-dd HH:mm:ss") 191 | return formatter.string(from: Date(timeIntervalSince1970: timestamp)) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /iOS/Sources/View/MovieGridItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieGridItemView.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2020/06/29. 6 | // Copyright © 2020 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MovieGridItemView: View { 12 | @ObservedObject private var viewModel: MovieGridItemViewModel 13 | 14 | init(viewModel: MovieGridItemViewModel) { 15 | self.viewModel = viewModel 16 | } 17 | 18 | var body: some View { 19 | VStack { 20 | ZStack(alignment: .topTrailing) { 21 | PosterImage(data: viewModel.posterImageData) 22 | .aspectRatio(61 / 91, contentMode: .fit) 23 | .cornerRadius(5) 24 | 25 | Image(viewModel.gradeImageName) 26 | .padding(.top, 8) 27 | .padding(.trailing, 8) 28 | } 29 | .padding(.top, 8) 30 | .padding(.horizontal, 10) 31 | 32 | VStack(spacing: 4) { 33 | Text(viewModel.primaryText) 34 | .titleStyle() 35 | .multilineTextAlignment(.center) 36 | 37 | Text(viewModel.secondaryText) 38 | .contentsStyle() 39 | 40 | Text(viewModel.tertiaryText) 41 | .contentsStyle() 42 | } 43 | .padding(.bottom, 4) 44 | } 45 | } 46 | } 47 | 48 | // MARK: - Preview 49 | 50 | struct MovieGridCell_Previews: PreviewProvider { 51 | static var previews: some View { 52 | MovieGridItemView(viewModel: MovieGridItemViewModel(movie: .dummy)) 53 | .frame(maxWidth: UIScreen.main.bounds.width / 2) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /iOS/Sources/View/MovieGridItemViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieGridItemViewModel.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2020/06/29. 6 | // Copyright © 2020 presto. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | final class MovieGridItemViewModel: ObservableObject { 13 | @Published private(set) var posterImageData: Data? 14 | @Published private(set) var gradeImageName: String = "" 15 | @Published private(set) var primaryText: String = "" 16 | @Published private(set) var secondaryText: String = "" 17 | @Published private(set) var tertiaryText: String = "" 18 | 19 | private let movieSubject = CurrentValueSubject(nil) 20 | 21 | private var cancellables = Set() 22 | 23 | init(movie: MoviesResponseModel.Movie) { 24 | let apiService = DIContainer.shared.resolve(APIService.self) 25 | let movieSharedPublisher = movieSubject 26 | .compactMap { $0 } 27 | .share() 28 | 29 | movieSharedPublisher 30 | .map(\.grade) 31 | .removeDuplicates() 32 | .compactMap(Grade.init) 33 | .map(\.imageName) 34 | .assign(to: \.gradeImageName, on: self) 35 | .store(in: &cancellables) 36 | 37 | movieSharedPublisher 38 | .map(\.title) 39 | .removeDuplicates() 40 | .assign(to: \.primaryText, on: self) 41 | .store(in: &cancellables) 42 | 43 | movieSharedPublisher 44 | .map { "\($0.reservationGrade)위(\($0.userRating)) / \($0.reservationRate)%" } 45 | .removeDuplicates() 46 | .assign(to: \.secondaryText, on: self) 47 | .store(in: &cancellables) 48 | 49 | movieSharedPublisher 50 | .map(\.date) 51 | .removeDuplicates() 52 | .assign(to: \.tertiaryText, on: self) 53 | .store(in: &cancellables) 54 | 55 | movieSharedPublisher 56 | .map(\.thumb) 57 | .removeDuplicates() 58 | .flatMap { thumb -> ImageDataPublisher in 59 | guard let apiService = apiService else { 60 | return Empty().eraseToAnyPublisher() 61 | } 62 | return apiService.imageDataPublisher(fromURLString: thumb) 63 | .eraseToAnyPublisher() 64 | } 65 | .replaceError(with: Data()) 66 | .compactMap { $0 } 67 | .assign(to: \.posterImageData, on: self) 68 | .store(in: &cancellables) 69 | 70 | movieSubject.send(movie) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /iOS/Sources/View/MovieGridView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieGridView.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2020/06/29. 6 | // Copyright © 2020 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MovieGridView: View { 12 | @Binding private var movies: [MoviesResponseModel.Movie] 13 | @Binding private var sortMethod: SortMethod 14 | 15 | init(movies: Binding<[MoviesResponseModel.Movie]>, sortMethod: Binding) { 16 | _movies = movies 17 | _sortMethod = sortMethod 18 | } 19 | 20 | var body: some View { 21 | ScrollView { 22 | LazyVGrid(columns: Array(repeating: GridItem(.flexible(), alignment: .top), count: 2)) { 23 | ForEach(movies) { movie in 24 | let destinationViewModel = MovieDetailViewModel(movieID: movie.id, movieTitle: movie.title) 25 | let destination = MovieDetailView(viewModel: destinationViewModel) 26 | NavigationLink(destination: destination) { 27 | let viewModel = MovieGridItemViewModel(movie: movie) 28 | 29 | MovieGridItemView(viewModel: viewModel) 30 | } 31 | .buttonStyle(PlainButtonStyle()) 32 | } 33 | } 34 | .padding(.horizontal, 6) 35 | } 36 | } 37 | } 38 | 39 | // MARK: - Preview 40 | 41 | struct MovieGridView_Previews: PreviewProvider { 42 | static var previews: some View { 43 | MovieGridView(movies: .constant([.dummy]), sortMethod: .constant(.curation)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /iOS/Sources/View/MovieListItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieListItemView.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2020/06/29. 6 | // Copyright © 2020 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MovieListItemView: View { 12 | @ObservedObject private var viewModel: MovieListItemViewModel 13 | 14 | init(viewModel: MovieListItemViewModel) { 15 | self.viewModel = viewModel 16 | } 17 | 18 | var body: some View { 19 | HStack { 20 | PosterImage(data: viewModel.posterImageData) 21 | .aspectRatio(61 / 91, contentMode: .fit) 22 | .frame(height: UIScreen.main.bounds.height / 5) 23 | .cornerRadius(5) 24 | 25 | VStack(alignment: .leading) { 26 | Spacer() 27 | 28 | HStack(alignment: .center) { 29 | Text(viewModel.title) 30 | .titleStyle() 31 | 32 | Image(viewModel.gradeImageName) 33 | 34 | Spacer() 35 | } 36 | 37 | Spacer() 38 | 39 | HStack(spacing: 16) { 40 | VStack(alignment: .leading, spacing: 4) { 41 | Text("평점") 42 | .contentsStyle() 43 | .foregroundColor(.init(.tertiaryLabel)) 44 | 45 | Text("예매순위") 46 | .contentsStyle() 47 | .foregroundColor(.init(.tertiaryLabel)) 48 | 49 | Text("예매율") 50 | .contentsStyle() 51 | .foregroundColor(.init(.tertiaryLabel)) 52 | 53 | Text("개봉일") 54 | .contentsStyle() 55 | .foregroundColor(.init(.tertiaryLabel)) 56 | } 57 | 58 | VStack(alignment: .leading, spacing: 4) { 59 | Text(viewModel.rating) 60 | .contentsStyle() 61 | 62 | Text(viewModel.reservationGrade) 63 | .contentsStyle() 64 | 65 | Text(viewModel.reservationRate) 66 | .contentsStyle() 67 | 68 | Text(viewModel.date) 69 | .contentsStyle() 70 | } 71 | } 72 | 73 | Spacer() 74 | } 75 | } 76 | } 77 | } 78 | 79 | // MARK: - Preview 80 | 81 | struct MovieListCell_Previews: PreviewProvider { 82 | static var previews: some View { 83 | MovieListItemView(viewModel: MovieListItemViewModel(movie: .dummy)) 84 | .frame(maxHeight: UIScreen.main.bounds.height / 5) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /iOS/Sources/View/MovieListItemViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieListItemViewModel.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2020/06/29. 6 | // Copyright © 2020 presto. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | final class MovieListItemViewModel: ObservableObject { 13 | @Published private(set) var posterImageData: Data? 14 | @Published private(set) var gradeImageName: String = "" 15 | @Published private(set) var title: String = "" 16 | @Published private(set) var rating: String = "" 17 | @Published private(set) var reservationGrade: String = "" 18 | @Published private(set) var reservationRate: String = "" 19 | @Published private(set) var date: String = "" 20 | 21 | private let movieSubject = CurrentValueSubject(nil) 22 | 23 | private var cancellables = Set() 24 | 25 | init(movie: MoviesResponseModel.Movie) { 26 | let apiService = DIContainer.shared.resolve(APIService.self) 27 | let movieSharedPublisher = movieSubject 28 | .compactMap { $0 } 29 | .share() 30 | 31 | movieSharedPublisher 32 | .map(\.grade) 33 | .removeDuplicates() 34 | .compactMap(Grade.init) 35 | .map(\.imageName) 36 | .assign(to: \.gradeImageName, on: self) 37 | .store(in: &cancellables) 38 | 39 | movieSharedPublisher 40 | .map(\.title) 41 | .removeDuplicates() 42 | .assign(to: \.title, on: self) 43 | .store(in: &cancellables) 44 | 45 | movieSharedPublisher 46 | .map(\.userRating) 47 | .removeDuplicates() 48 | .map { String($0) } 49 | .assign(to: \.rating, on: self) 50 | .store(in: &cancellables) 51 | 52 | movieSharedPublisher 53 | .map(\.reservationGrade) 54 | .removeDuplicates() 55 | .map(String.init) 56 | .assign(to: \.reservationGrade, on: self) 57 | .store(in: &cancellables) 58 | 59 | movieSharedPublisher 60 | .map(\.reservationRate) 61 | .removeDuplicates() 62 | .map { "\($0)%" } 63 | .assign(to: \.reservationRate, on: self) 64 | .store(in: &cancellables) 65 | 66 | movieSharedPublisher 67 | .map(\.date) 68 | .removeDuplicates() 69 | .assign(to: \.date, on: self) 70 | .store(in: &cancellables) 71 | 72 | movieSharedPublisher 73 | .map(\.thumb) 74 | .removeDuplicates() 75 | .flatMap { thumb -> ImageDataPublisher in 76 | guard let apiService = apiService else { 77 | return Empty().eraseToAnyPublisher() 78 | } 79 | return apiService.imageDataPublisher(fromURLString: thumb) 80 | .eraseToAnyPublisher() 81 | } 82 | .replaceError(with: Data()) 83 | .compactMap { $0 } 84 | .assign(to: \.posterImageData, on: self) 85 | .store(in: &cancellables) 86 | 87 | movieSubject.send(movie) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /iOS/Sources/View/MovieListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieListView.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2020/06/29. 6 | // Copyright © 2020 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MovieListView: View { 12 | @Binding private var movies: [MoviesResponseModel.Movie] 13 | @Binding private var sortMethod: SortMethod 14 | 15 | init(movies: Binding<[MoviesResponseModel.Movie]>, sortMethod: Binding) { 16 | _movies = movies 17 | _sortMethod = sortMethod 18 | } 19 | 20 | var body: some View { 21 | List(movies) { movie in 22 | let destinationViewModel = MovieDetailViewModel(movieID: movie.id, movieTitle: movie.title) 23 | let destination = MovieDetailView(viewModel: destinationViewModel) 24 | 25 | NavigationLink(destination: destination) { 26 | let viewModel = MovieListItemViewModel(movie: movie) 27 | 28 | MovieListItemView(viewModel: viewModel) 29 | } 30 | } 31 | } 32 | } 33 | 34 | // MARK: - Preview 35 | 36 | struct MovieListView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | MovieListView(movies: .constant([.dummy]), sortMethod: .constant(.curation)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /iOS/Sources/View/MovieMainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieMainView.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/16. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MovieMainView: View { 12 | @ObservedObject private var viewModel: MovieMainViewModel 13 | 14 | init(viewModel: MovieMainViewModel) { 15 | self.viewModel = viewModel 16 | } 17 | 18 | var body: some View { 19 | TabView(selection: $viewModel.currentTab) { 20 | NavigationView { 21 | movieListView 22 | .movieMainViewNavigationBarStyle() 23 | .toolbar { 24 | ToolbarItem(placement: .navigationBarTrailing) { 25 | sortButton 26 | } 27 | } 28 | } 29 | .tabItem { 30 | Label("List", systemImage: "list.dash") 31 | } 32 | .tag(MovieMainView.Tab.list) 33 | 34 | NavigationView { 35 | movieGridView 36 | .movieMainViewNavigationBarStyle() 37 | .toolbar { 38 | ToolbarItem(placement: .navigationBarTrailing) { 39 | sortButton 40 | } 41 | } 42 | } 43 | .tabItem { 44 | Label("Grid", systemImage: "square.grid.2x2") 45 | } 46 | .tag(MovieMainView.Tab.grid) 47 | } 48 | .actionSheet(isPresented: $viewModel.showsSortActionSheet) { 49 | sortActionSheet 50 | } 51 | .onAppear { 52 | viewModel.requestData() 53 | } 54 | } 55 | } 56 | 57 | // MARK: - View 58 | 59 | private extension MovieMainView { 60 | @ViewBuilder var movieListView: some View { 61 | if viewModel.movies.isEmpty == false { 62 | MovieListView(movies: $viewModel.movies, sortMethod: $viewModel.sortMethod) 63 | } else if viewModel.movieErrors.isEmpty == false { 64 | MovieRetryView(errors: viewModel.movieErrors, onRetry: { 65 | viewModel.requestData() 66 | }) 67 | } else { 68 | CircularProgressView() 69 | } 70 | } 71 | 72 | @ViewBuilder var movieGridView: some View { 73 | if viewModel.movies.isEmpty == false { 74 | MovieGridView(movies: $viewModel.movies, sortMethod: $viewModel.sortMethod) 75 | } else if viewModel.movieErrors.isEmpty == false { 76 | MovieRetryView(errors: viewModel.movieErrors, onRetry: { 77 | viewModel.requestData() 78 | }) 79 | } else { 80 | CircularProgressView() 81 | } 82 | } 83 | 84 | var sortButton: some View { 85 | Button(action: viewModel.setShowsSortActionSheet) { 86 | Text(viewModel.sortMethodDescription) 87 | } 88 | } 89 | 90 | var sortActionSheet: ActionSheet { 91 | ActionSheet( 92 | title: Text("정렬방식 선택"), 93 | message: Text("영화를 어떤 순서로 정렬할까요?"), 94 | buttons: [ 95 | .default(Text("예매율")) { 96 | viewModel.setSortMethod(.reservation) 97 | }, 98 | .default(Text("큐레이션")) { 99 | viewModel.setSortMethod(.curation) 100 | }, 101 | .default(Text("개봉일")) { 102 | viewModel.setSortMethod(.date) 103 | }, 104 | .cancel(Text("취소")), 105 | ] 106 | ) 107 | } 108 | } 109 | 110 | extension MovieMainView { 111 | enum Tab { 112 | case list 113 | case grid 114 | } 115 | } 116 | 117 | // MARK: - Preview 118 | 119 | struct MovieView_Previews: PreviewProvider { 120 | static var previews: some View { 121 | MovieMainView(viewModel: MovieMainViewModel()) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /iOS/Sources/View/MovieMainViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieMainViewModel.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/16. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | final class MovieMainViewModel: ObservableObject { 13 | @Published var currentTab: MovieMainView.Tab = .list 14 | @Published var showsSortActionSheet: Bool = false 15 | @Published var sortMethod: SortMethod = .reservation 16 | @Published var movies: [MoviesResponseModel.Movie] = [] 17 | 18 | @Published private(set) var movieErrors: [APIError] = [] 19 | @Published private(set) var sortMethodDescription: String = "" 20 | 21 | private let showsSortActionSheetSubject = CurrentValueSubject(nil) 22 | private let sortMethodSubject = CurrentValueSubject(nil) 23 | 24 | private var isLoading = false 25 | private var cancellables = Set() 26 | 27 | init() { 28 | sortMethodSubject 29 | .compactMap { $0 } 30 | .removeDuplicates() 31 | .sink(receiveValue: { [weak self] sortMethod in 32 | self?.sortMethod = sortMethod 33 | self?.sortMethodDescription = sortMethod.description 34 | self?.requestData() 35 | }) 36 | .store(in: &cancellables) 37 | 38 | showsSortActionSheetSubject 39 | .compactMap { $0 } 40 | .removeDuplicates() 41 | .assign(to: \.showsSortActionSheet, on: self) 42 | .store(in: &cancellables) 43 | } 44 | 45 | func setSortMethod(_ sortMethod: SortMethod) { 46 | sortMethodSubject.send(sortMethod) 47 | } 48 | 49 | func setShowsSortActionSheet() { 50 | showsSortActionSheetSubject.send(true) 51 | } 52 | 53 | func requestData() { 54 | movieErrors.removeAll() 55 | isLoading = true 56 | 57 | let apiService = DIContainer.shared.resolve(APIService.self) 58 | apiService?.moviesPublisher(with: sortMethod) 59 | .receive(on: DispatchQueue.main) 60 | .sink(receiveCompletion: { [weak self] completion in 61 | switch completion { 62 | case let .failure(error): 63 | self?.movieErrors.append(error) 64 | case .finished: 65 | break 66 | } 67 | 68 | self?.isLoading = false 69 | }, receiveValue: { [weak self] moviesResponse in 70 | self?.movies = moviesResponse.movies 71 | }) 72 | .store(in: &cancellables) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /iOS/Sources/View/MovieRetryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieRetryView.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/23. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MovieRetryView: View { 12 | private let errors: [APIError] 13 | private let onRetry: () -> Void 14 | 15 | init(errors: [APIError], onRetry: @escaping () -> Void) { 16 | self.errors = errors 17 | self.onRetry = onRetry 18 | } 19 | 20 | var body: some View { 21 | VStack(spacing: 32) { 22 | VStack(spacing: 8) { 23 | Text("정보를 불러오지 못했습니다.") 24 | .font(.title3) 25 | .fontWeight(.bold) 26 | .lineLimit(1) 27 | 28 | VStack { 29 | ForEach(errors, id: \.self) { error in 30 | Text(error.localizedDescription) 31 | .font(.body) 32 | .foregroundColor(.secondary) 33 | } 34 | } 35 | } 36 | 37 | Button(action: onRetry) { 38 | Text("다시 시도하기") 39 | .font(.title3) 40 | .fontWeight(.semibold) 41 | } 42 | .padding() 43 | } 44 | .padding() 45 | } 46 | } 47 | 48 | // MARK: - Preview 49 | 50 | struct MovieRetryView_Previews: PreviewProvider { 51 | static var previews: some View { 52 | MovieRetryView(errors: [.commentPostingRequestFailed], onRetry: {}) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /iOS/Sources/View/Parts/CircularProgressView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircularProgressView.swift 3 | // BoxOffice 4 | // 5 | // Created by Presto on 2021/07/11. 6 | // Copyright © 2021 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CircularProgressView: View { 12 | var body: some View { 13 | ProgressView() 14 | .progressViewStyle(CircularProgressViewStyle()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /iOS/Sources/View/Parts/MultipleSpacer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultipleSpacer.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/23. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MultipleSpacer: View { 12 | private let count: Int 13 | private let minLength: CGFloat? 14 | 15 | init(count: Int = 1, minLength: CGFloat? = nil) { 16 | self.count = max(1, count) 17 | self.minLength = minLength 18 | } 19 | 20 | var body: some View { 21 | ForEach(0 ..< count) { _ in 22 | Spacer(minLength: self.minLength) 23 | } 24 | } 25 | } 26 | 27 | // MARK: - Preview 28 | 29 | struct MultipleSpacer_Previews: PreviewProvider { 30 | static var previews: some View { 31 | MultipleSpacer() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /iOS/Sources/View/Parts/PosterImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PosterImage.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2020/06/30. 6 | // Copyright © 2020 presto. All rights reserved. 7 | // 8 | 9 | #if os(iOS) 10 | import UIKit 11 | #else 12 | import AppKit 13 | #endif 14 | import SwiftUI 15 | 16 | struct PosterImage: View { 17 | private let data: Data? 18 | 19 | init(data: Data?) { 20 | self.data = data 21 | } 22 | 23 | var body: some View { 24 | #if os(iOS) 25 | if let data = data, let posterUIImage = UIImage(data: data) { 26 | return AnyView( 27 | Image(uiImage: posterUIImage) 28 | .resizable() 29 | ) 30 | } else { 31 | return AnyView(placeholder) 32 | } 33 | #else 34 | if let data = data, let posterUIImage = NSImage(data: data) { 35 | return AnyView( 36 | Image(nsImage: posterUIImage) 37 | .resizable() 38 | ) 39 | } else { 40 | return AnyView(placeholder) 41 | } 42 | #endif 43 | } 44 | } 45 | 46 | // MARK: - View 47 | 48 | private extension PosterImage { 49 | var placeholder: some View { 50 | Image(systemName: "photo") 51 | .resizable() 52 | .aspectRatio(contentMode: .fit) 53 | } 54 | } 55 | 56 | // MARK: - Preview 57 | 58 | struct PosterImage_Previews: PreviewProvider { 59 | static var previews: some View { 60 | PosterImage(data: nil) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /iOS/Sources/View/Parts/StarRatingBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StarRatingBar.swift 3 | // BoxOffice_SwiftUI 4 | // 5 | // Created by Presto on 2019/10/18. 6 | // Copyright © 2019 presto. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct StarRatingBar: View { 12 | private let score: Double 13 | private let length: CGFloat 14 | 15 | init(score: Double, length: CGFloat = 20) { 16 | self.score = score 17 | self.length = length 18 | } 19 | 20 | var body: some View { 21 | HStack(spacing: 0) { 22 | ForEach(1 ..< 6) { number in 23 | let reference = Double(number * 2) 24 | if score >= reference { 25 | starImage(.full) 26 | } else if score > reference - 1 { 27 | starImage(.half) 28 | } else { 29 | starImage(.normal) 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | // MARK: - Private Method 37 | 38 | private extension StarRatingBar { 39 | func starImage(_ type: StarType) -> some View { 40 | Image(type.imageName) 41 | .resizable() 42 | .aspectRatio(1, contentMode: .fit) 43 | .frame(height: length) 44 | } 45 | } 46 | 47 | // MARK: - Preview 48 | 49 | struct StarRatingBar_Previews: PreviewProvider { 50 | static var previews: some View { 51 | StarRatingBar(score: 7.8, length: 20) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/images/1.png -------------------------------------------------------------------------------- /images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/images/2.png -------------------------------------------------------------------------------- /images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/images/3.png -------------------------------------------------------------------------------- /images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/images/4.png -------------------------------------------------------------------------------- /images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/images/5.png -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/presto95/BoxOffice_SwiftUI/66917bf467ec80138a58d9591042cca7a2b86041/preview.gif --------------------------------------------------------------------------------