├── .gitignore ├── .travis.yml ├── LICENSE ├── MyCards ├── .swiftlint.yml ├── Icon.pxm ├── MyCards.sketch ├── MyCards.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── MyCards.xcscheme ├── MyCards │ ├── AVCaptureDevice+Extension.swift │ ├── AppDelegate.swift │ ├── Array+Extension.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-60@2x.png │ │ │ ├── Icon-60@3x.png │ │ │ ├── Icon-Small@2x.png │ │ │ ├── Icon-Small@3x.png │ │ │ ├── Icon-Spotlight-40@2x.png │ │ │ ├── Icon-Spotlight-40@3x.png │ │ │ ├── MyCards@2x.png │ │ │ ├── notification40.png │ │ │ └── notification80.png │ │ ├── Contents.json │ │ ├── MyCards.imageset │ │ │ ├── Cards@1x.png │ │ │ ├── Cards@2x.png │ │ │ ├── Cards@3x.png │ │ │ └── Contents.json │ │ ├── back.imageset │ │ │ ├── Contents.json │ │ │ └── back.png │ │ ├── background.imageset │ │ │ ├── Background@2x.png │ │ │ ├── Background@3x.png │ │ │ └── Contents.json │ │ └── front.imageset │ │ │ ├── Contents.json │ │ │ └── front.png │ ├── CGAffineTransform+Rotate.swift │ ├── CGFloat+Extension.swift │ ├── CGRect+Extension.swift │ ├── Card.swift │ ├── CardCell.swift │ ├── CardDetailsViewController.swift │ ├── CardMO+CoreDataClass.swift │ ├── CardMO+CoreDataProperties.swift │ ├── CardParser.swift │ ├── CardPhotoViewController.swift │ ├── CardView.swift │ ├── CardsViewController.swift │ ├── CloseButton.swift │ ├── CoreDataService.swift │ ├── CoreDataWorker.swift │ ├── CropViewController.swift │ ├── Data+Extension.swift │ ├── DataModel.xcdatamodeld │ │ ├── .xccurrentversion │ │ └── DataModel.xcdatamodel │ │ │ └── contents │ ├── ImagePickerController.swift │ ├── Info.plist │ ├── LaunchScreen.storyboard │ ├── LightStatusBarViewController.swift │ ├── ManagedObjectConvertible.swift │ ├── ManagedObjectProtocol.swift │ ├── NSLayoutConstraint+Extension.swift │ ├── NSLayoutFormatOptions+Extension.swift │ ├── NSObject+Builder.swift │ ├── NetworLoader+Shared.swift │ ├── NetworkLoader.swift │ ├── NotificationCenter+Extension.swift │ ├── PhotoCamera.swift │ ├── PhotoCameraButton.swift │ ├── PhotoCaptureViewController.swift │ ├── PreviewOutline.swift │ ├── PreviewView.swift │ ├── Result.swift │ ├── String+Localized.swift │ ├── TappableView.swift │ ├── UICollectionView+Extension.swift │ ├── UIColor+Extension.swift │ ├── UIImage+Extension.swift │ ├── UIImagePickerController+Extension.swift │ ├── UIImagePickerControllerSourceType+Extension.swift │ ├── UINavigationController+Extension.swift │ ├── UITextField+Extension.swift │ ├── UIView+Constrained.swift │ ├── UIViewController+Extension.swift │ ├── cards.json │ ├── iTunesArtwork │ └── iTunesArtwork@2x ├── MyCardsTests │ ├── CardParserConstants.swift │ ├── CardParserTests.swift │ └── Info.plist ├── iTunesArtwork └── iTunesArtwork@2x └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode9.2 3 | script: 4 | - xcodebuild clean build -sdk iphonesimulator11.2 -project MyCards/MyCards.xcodeproj -scheme MyCards CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO 5 | - xcodebuild test -sdk iphonesimulator11.2 -project MyCards/MyCards.xcodeproj -scheme MyCards CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO -destination 'platform=iOS Simulator,name=iPhone X,OS=11.2' 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 swifting.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MyCards/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - valid_docs 3 | - missing_docs 4 | - nesting 5 | - trailing_comma 6 | - vertical_parameter_alignment 7 | opt_in_rules: # some rules are only opt-in 8 | - empty_count 9 | - cyclomatic_complexity 10 | - closure_spacing 11 | - explicit_init 12 | - overridden_super_call 13 | - redundant_nil_coalesing 14 | - private_outlet 15 | - file_header 16 | excluded: # paths to ignore during linting. Takes precedence over `included`. 17 | - DerivedData 18 | # binary rules can set their severity level 19 | force_cast: warning 20 | force_try: warning 21 | line_length: 140 22 | type_body_length: 23 | - 300 # warning 24 | - 400 # error 25 | file_length: 26 | warning: 500 27 | error: 1000 28 | function_parameter_count: 29 | warning: 5 30 | error: 6 31 | cyclomatic_complexity: 32 | warning: 4 33 | error: 6 34 | function_body_length: 35 | warning: 50 36 | error: 200 37 | variable_name: 38 | min_length: 2 39 | max_length: 30 40 | -------------------------------------------------------------------------------- /MyCards/Icon.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftingio/architecture-wars-mvc/8281f0f6ac244c4ed2a89bc19c7c7fb4f38bdb4a/MyCards/Icon.pxm -------------------------------------------------------------------------------- /MyCards/MyCards.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftingio/architecture-wars-mvc/8281f0f6ac244c4ed2a89bc19c7c7fb4f38bdb4a/MyCards/MyCards.sketch -------------------------------------------------------------------------------- /MyCards/MyCards.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 51C5D33D1FDCEDBD002A0944 /* CardParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C5D33C1FDCEDBD002A0944 /* CardParserTests.swift */; }; 11 | 51C5D3451FDCF4CB002A0944 /* CardParserConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C5D3441FDCF4CB002A0944 /* CardParserConstants.swift */; }; 12 | 69183D411DE653420026C1DF /* PhotoCameraButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69183D401DE653420026C1DF /* PhotoCameraButton.swift */; }; 13 | 69183D431DE653710026C1DF /* CloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69183D421DE653710026C1DF /* CloseButton.swift */; }; 14 | 691D7D0D1E436E5F00E53AF1 /* iTunesArtwork@2x in Resources */ = {isa = PBXBuildFile; fileRef = 691D7D0C1E436E5600E53AF1 /* iTunesArtwork@2x */; }; 15 | 691D7D0E1E436E6100E53AF1 /* iTunesArtwork in Resources */ = {isa = PBXBuildFile; fileRef = 691D7D0B1E436E4D00E53AF1 /* iTunesArtwork */; }; 16 | 691D7D101E4404ED00E53AF1 /* ImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691D7D0F1E4404ED00E53AF1 /* ImagePickerController.swift */; }; 17 | 691D7D121E44050F00E53AF1 /* UITextField+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691D7D111E44050F00E53AF1 /* UITextField+Extension.swift */; }; 18 | 693A2F601DE4D19F00975FA1 /* PhotoCaptureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A2F5F1DE4D19F00975FA1 /* PhotoCaptureViewController.swift */; }; 19 | 693A44C71DD7422700EDCABA /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A44C61DD7422700EDCABA /* String+Localized.swift */; }; 20 | 693A44C91DD7944B00EDCABA /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A44C81DD7944B00EDCABA /* CardView.swift */; }; 21 | 693A44CB1DD7D9CE00EDCABA /* TappableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A44CA1DD7D9CE00EDCABA /* TappableView.swift */; }; 22 | 694A73F21DF496FE00424107 /* AVCaptureDevice+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 694A73F11DF496FE00424107 /* AVCaptureDevice+Extension.swift */; }; 23 | 694A73F41DF4972700424107 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 694A73F31DF4972700424107 /* UIViewController+Extension.swift */; }; 24 | 6965BB621DF7E0E8009F9CBC /* CardMO+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6965BB601DF7E0E8009F9CBC /* CardMO+CoreDataClass.swift */; }; 25 | 6965BB631DF7E0E8009F9CBC /* CardMO+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6965BB611DF7E0E8009F9CBC /* CardMO+CoreDataProperties.swift */; }; 26 | 6965BB671DF7E7BC009F9CBC /* ManagedObjectConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6965BB651DF7E7BC009F9CBC /* ManagedObjectConvertible.swift */; }; 27 | 6965BB681DF7E7BC009F9CBC /* ManagedObjectProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6965BB661DF7E7BC009F9CBC /* ManagedObjectProtocol.swift */; }; 28 | 6967D4271DFAC163001A073B /* CropViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6967D4261DFAC163001A073B /* CropViewController.swift */; }; 29 | 696C84481E463B1A00F73EFD /* UICollectionView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 696C84471E463B1A00F73EFD /* UICollectionView+Extension.swift */; }; 30 | 69769AEC1DE2203700DF2747 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 69769AEB1DE2203700DF2747 /* LaunchScreen.storyboard */; }; 31 | 69769AEE1DE2324D00DF2747 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69769AED1DE2324D00DF2747 /* UIColor+Extension.swift */; }; 32 | 69769AF01DE241A300DF2747 /* UIImagePickerControllerSourceType+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69769AEF1DE241A300DF2747 /* UIImagePickerControllerSourceType+Extension.swift */; }; 33 | 69769AF21DE241D100DF2747 /* UIImagePickerController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69769AF11DE241D100DF2747 /* UIImagePickerController+Extension.swift */; }; 34 | 6976F6E01E22F75300EA7B43 /* NotificationCenter+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6976F6DF1E22F75300EA7B43 /* NotificationCenter+Extension.swift */; }; 35 | 6981E6361E9A412400AA1D5A /* Data+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6981E6351E9A412400AA1D5A /* Data+Extension.swift */; }; 36 | 6981E6381E9A43B100AA1D5A /* cards.json in Resources */ = {isa = PBXBuildFile; fileRef = 6981E6371E9A43B100AA1D5A /* cards.json */; }; 37 | 6981E6411E9AB9B000AA1D5A /* CardParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6981E6401E9AB9B000AA1D5A /* CardParser.swift */; }; 38 | 6981E6431E9AB9F600AA1D5A /* NetworLoader+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6981E6421E9AB9F600AA1D5A /* NetworLoader+Shared.swift */; }; 39 | 6981E6441E9ABF0500AA1D5A /* CardDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69ABD0E61DB3A1A300CB925C /* CardDetailsViewController.swift */; }; 40 | 698319891DD9134F0091ED64 /* PhotoCamera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698319881DD9134F0091ED64 /* PhotoCamera.swift */; }; 41 | 698C57EC1E4759EC0035A8B7 /* UIView+Constrained.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698C57EB1E4759EC0035A8B7 /* UIView+Constrained.swift */; }; 42 | 698C57EE1E475D700035A8B7 /* PreviewOutline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698C57ED1E475D700035A8B7 /* PreviewOutline.swift */; }; 43 | 6991C0751DE6488E00442B8E /* PreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6991C0741DE6488E00442B8E /* PreviewView.swift */; }; 44 | 699795491DEB631500CB408F /* CGRect+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699795481DEB631500CB408F /* CGRect+Extension.swift */; }; 45 | 69AB4B2A1DD652270014B996 /* NSLayoutConstraint+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69AB4B291DD652270014B996 /* NSLayoutConstraint+Extension.swift */; }; 46 | 69AB4B2C1DD66F190014B996 /* NSObject+Builder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69AB4B2B1DD66F190014B996 /* NSObject+Builder.swift */; }; 47 | 69AB4B2E1DD674C80014B996 /* NetworkLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69AB4B2D1DD674C80014B996 /* NetworkLoader.swift */; }; 48 | 69ABD0C61DB391E100CB925C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69ABD0C51DB391E100CB925C /* AppDelegate.swift */; }; 49 | 69ABD0CE1DB391E100CB925C /* DataModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 69ABD0CC1DB391E100CB925C /* DataModel.xcdatamodeld */; }; 50 | 69ABD0D01DB391E100CB925C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69ABD0CF1DB391E100CB925C /* Assets.xcassets */; }; 51 | 69ABD0DB1DB3932700CB925C /* CoreDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69ABD0DA1DB3932700CB925C /* CoreDataService.swift */; }; 52 | 69ABD0DF1DB3953600CB925C /* CardsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69ABD0DE1DB3953600CB925C /* CardsViewController.swift */; }; 53 | 69ABD0E11DB3955000CB925C /* CoreDataWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69ABD0E01DB3955000CB925C /* CoreDataWorker.swift */; }; 54 | 69ABD0E31DB395F800CB925C /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69ABD0E21DB395F800CB925C /* Result.swift */; }; 55 | 69D170761DF4CBDA009E5710 /* CardPhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D170751DF4CBDA009E5710 /* CardPhotoViewController.swift */; }; 56 | 69D170781DF5775D009E5710 /* CGFloat+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D170771DF5775D009E5710 /* CGFloat+Extension.swift */; }; 57 | 69D1707A1DF579C5009E5710 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D170791DF579C5009E5710 /* UINavigationController+Extension.swift */; }; 58 | 69D1707C1DF57AAA009E5710 /* LightStatusBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D1707B1DF57AAA009E5710 /* LightStatusBarViewController.swift */; }; 59 | 69DDFA731DB3E6AD00D856C5 /* CardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DDFA721DB3E6AD00D856C5 /* CardCell.swift */; }; 60 | 69DDFA751DB3E6D500D856C5 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DDFA741DB3E6D500D856C5 /* Card.swift */; }; 61 | 69DDFA771DB3E70900D856C5 /* NSLayoutFormatOptions+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DDFA761DB3E70900D856C5 /* NSLayoutFormatOptions+Extension.swift */; }; 62 | 69DDFA791DB3E7B200D856C5 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DDFA781DB3E7B200D856C5 /* Array+Extension.swift */; }; 63 | 69FD28F81EB608B700B3B664 /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69FD28F71EB608B700B3B664 /* UIImage+Extension.swift */; }; 64 | 69FD28FA1EB64F6E00B3B664 /* CGAffineTransform+Rotate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69FD28F91EB64F6E00B3B664 /* CGAffineTransform+Rotate.swift */; }; 65 | /* End PBXBuildFile section */ 66 | 67 | /* Begin PBXContainerItemProxy section */ 68 | 51C5D33F1FDCEDBD002A0944 /* PBXContainerItemProxy */ = { 69 | isa = PBXContainerItemProxy; 70 | containerPortal = 69ABD0BA1DB391E100CB925C /* Project object */; 71 | proxyType = 1; 72 | remoteGlobalIDString = 69ABD0C11DB391E100CB925C; 73 | remoteInfo = MyCards; 74 | }; 75 | /* End PBXContainerItemProxy section */ 76 | 77 | /* Begin PBXFileReference section */ 78 | 51C5D33A1FDCEDBD002A0944 /* MyCardsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyCardsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 79 | 51C5D33C1FDCEDBD002A0944 /* CardParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardParserTests.swift; sourceTree = ""; }; 80 | 51C5D33E1FDCEDBD002A0944 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 81 | 51C5D3441FDCF4CB002A0944 /* CardParserConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardParserConstants.swift; sourceTree = ""; }; 82 | 69183D401DE653420026C1DF /* PhotoCameraButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCameraButton.swift; sourceTree = ""; }; 83 | 69183D421DE653710026C1DF /* CloseButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloseButton.swift; sourceTree = ""; }; 84 | 691D7D0B1E436E4D00E53AF1 /* iTunesArtwork */ = {isa = PBXFileReference; lastKnownFileType = file; path = iTunesArtwork; sourceTree = SOURCE_ROOT; }; 85 | 691D7D0C1E436E5600E53AF1 /* iTunesArtwork@2x */ = {isa = PBXFileReference; lastKnownFileType = file; path = "iTunesArtwork@2x"; sourceTree = SOURCE_ROOT; }; 86 | 691D7D0F1E4404ED00E53AF1 /* ImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerController.swift; sourceTree = ""; }; 87 | 691D7D111E44050F00E53AF1 /* UITextField+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextField+Extension.swift"; sourceTree = ""; }; 88 | 693A2F5F1DE4D19F00975FA1 /* PhotoCaptureViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureViewController.swift; sourceTree = ""; }; 89 | 693A44C61DD7422700EDCABA /* String+Localized.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Localized.swift"; sourceTree = ""; }; 90 | 693A44C81DD7944B00EDCABA /* CardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; 91 | 693A44CA1DD7D9CE00EDCABA /* TappableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TappableView.swift; sourceTree = ""; }; 92 | 694A73F11DF496FE00424107 /* AVCaptureDevice+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice+Extension.swift"; sourceTree = ""; }; 93 | 694A73F31DF4972700424107 /* UIViewController+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; 94 | 6965BB601DF7E0E8009F9CBC /* CardMO+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CardMO+CoreDataClass.swift"; sourceTree = ""; }; 95 | 6965BB611DF7E0E8009F9CBC /* CardMO+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CardMO+CoreDataProperties.swift"; sourceTree = ""; }; 96 | 6965BB651DF7E7BC009F9CBC /* ManagedObjectConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedObjectConvertible.swift; sourceTree = ""; }; 97 | 6965BB661DF7E7BC009F9CBC /* ManagedObjectProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedObjectProtocol.swift; sourceTree = ""; }; 98 | 6967D4261DFAC163001A073B /* CropViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropViewController.swift; sourceTree = ""; }; 99 | 696C84471E463B1A00F73EFD /* UICollectionView+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extension.swift"; sourceTree = ""; }; 100 | 69769AEB1DE2203700DF2747 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 101 | 69769AED1DE2324D00DF2747 /* UIColor+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; 102 | 69769AEF1DE241A300DF2747 /* UIImagePickerControllerSourceType+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImagePickerControllerSourceType+Extension.swift"; sourceTree = ""; }; 103 | 69769AF11DE241D100DF2747 /* UIImagePickerController+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImagePickerController+Extension.swift"; sourceTree = ""; }; 104 | 6976F6DF1E22F75300EA7B43 /* NotificationCenter+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NotificationCenter+Extension.swift"; sourceTree = ""; }; 105 | 6981E6351E9A412400AA1D5A /* Data+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Extension.swift"; sourceTree = ""; }; 106 | 6981E6371E9A43B100AA1D5A /* cards.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = cards.json; sourceTree = ""; }; 107 | 6981E6401E9AB9B000AA1D5A /* CardParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardParser.swift; sourceTree = ""; }; 108 | 6981E6421E9AB9F600AA1D5A /* NetworLoader+Shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NetworLoader+Shared.swift"; sourceTree = ""; }; 109 | 698319881DD9134F0091ED64 /* PhotoCamera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCamera.swift; sourceTree = ""; }; 110 | 698C57EB1E4759EC0035A8B7 /* UIView+Constrained.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Constrained.swift"; sourceTree = ""; }; 111 | 698C57ED1E475D700035A8B7 /* PreviewOutline.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewOutline.swift; sourceTree = ""; }; 112 | 6991C0741DE6488E00442B8E /* PreviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewView.swift; sourceTree = ""; }; 113 | 699795481DEB631500CB408F /* CGRect+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+Extension.swift"; sourceTree = ""; }; 114 | 69AB4B291DD652270014B996 /* NSLayoutConstraint+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Extension.swift"; sourceTree = ""; }; 115 | 69AB4B2B1DD66F190014B996 /* NSObject+Builder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSObject+Builder.swift"; sourceTree = ""; }; 116 | 69AB4B2D1DD674C80014B996 /* NetworkLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkLoader.swift; sourceTree = ""; }; 117 | 69AB4B311DD676890014B996 /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = .swiftlint.yml; path = ../.swiftlint.yml; sourceTree = ""; }; 118 | 69ABD0C21DB391E100CB925C /* MyCards.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyCards.app; sourceTree = BUILT_PRODUCTS_DIR; }; 119 | 69ABD0C51DB391E100CB925C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 120 | 69ABD0CD1DB391E100CB925C /* DataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = DataModel.xcdatamodel; sourceTree = ""; }; 121 | 69ABD0CF1DB391E100CB925C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 122 | 69ABD0D41DB391E100CB925C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 123 | 69ABD0DA1DB3932700CB925C /* CoreDataService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataService.swift; sourceTree = ""; }; 124 | 69ABD0DE1DB3953600CB925C /* CardsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardsViewController.swift; sourceTree = ""; }; 125 | 69ABD0E01DB3955000CB925C /* CoreDataWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataWorker.swift; sourceTree = ""; }; 126 | 69ABD0E21DB395F800CB925C /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; 127 | 69ABD0E61DB3A1A300CB925C /* CardDetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardDetailsViewController.swift; sourceTree = ""; }; 128 | 69D170751DF4CBDA009E5710 /* CardPhotoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardPhotoViewController.swift; sourceTree = ""; }; 129 | 69D170771DF5775D009E5710 /* CGFloat+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGFloat+Extension.swift"; sourceTree = ""; }; 130 | 69D170791DF579C5009E5710 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = ""; }; 131 | 69D1707B1DF57AAA009E5710 /* LightStatusBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LightStatusBarViewController.swift; sourceTree = ""; }; 132 | 69DDFA721DB3E6AD00D856C5 /* CardCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardCell.swift; sourceTree = ""; }; 133 | 69DDFA741DB3E6D500D856C5 /* Card.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; 134 | 69DDFA761DB3E70900D856C5 /* NSLayoutFormatOptions+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSLayoutFormatOptions+Extension.swift"; sourceTree = ""; }; 135 | 69DDFA781DB3E7B200D856C5 /* Array+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; 136 | 69FD28F71EB608B700B3B664 /* UIImage+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = ""; }; 137 | 69FD28F91EB64F6E00B3B664 /* CGAffineTransform+Rotate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGAffineTransform+Rotate.swift"; sourceTree = ""; }; 138 | /* End PBXFileReference section */ 139 | 140 | /* Begin PBXFrameworksBuildPhase section */ 141 | 51C5D3371FDCEDBD002A0944 /* Frameworks */ = { 142 | isa = PBXFrameworksBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | ); 146 | runOnlyForDeploymentPostprocessing = 0; 147 | }; 148 | 69ABD0BF1DB391E100CB925C /* Frameworks */ = { 149 | isa = PBXFrameworksBuildPhase; 150 | buildActionMask = 2147483647; 151 | files = ( 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | /* End PBXFrameworksBuildPhase section */ 156 | 157 | /* Begin PBXGroup section */ 158 | 51C5D33B1FDCEDBD002A0944 /* MyCardsTests */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 51C5D33C1FDCEDBD002A0944 /* CardParserTests.swift */, 162 | 51C5D3441FDCF4CB002A0944 /* CardParserConstants.swift */, 163 | 51C5D33E1FDCEDBD002A0944 /* Info.plist */, 164 | ); 165 | path = MyCardsTests; 166 | sourceTree = ""; 167 | }; 168 | 6965BB641DF7E64B009F9CBC /* View */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | 698C57ED1E475D700035A8B7 /* PreviewOutline.swift */, 172 | 69183D421DE653710026C1DF /* CloseButton.swift */, 173 | 69183D401DE653420026C1DF /* PhotoCameraButton.swift */, 174 | 69DDFA721DB3E6AD00D856C5 /* CardCell.swift */, 175 | 693A44CA1DD7D9CE00EDCABA /* TappableView.swift */, 176 | 698319881DD9134F0091ED64 /* PhotoCamera.swift */, 177 | 693A44C81DD7944B00EDCABA /* CardView.swift */, 178 | 6991C0741DE6488E00442B8E /* PreviewView.swift */, 179 | ); 180 | name = View; 181 | sourceTree = ""; 182 | }; 183 | 69AB4B2F1DD674D10014B996 /* Network */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | 69AB4B2D1DD674C80014B996 /* NetworkLoader.swift */, 187 | 6981E6421E9AB9F600AA1D5A /* NetworLoader+Shared.swift */, 188 | 6981E6401E9AB9B000AA1D5A /* CardParser.swift */, 189 | ); 190 | name = Network; 191 | sourceTree = ""; 192 | }; 193 | 69ABD0B91DB391E100CB925C = { 194 | isa = PBXGroup; 195 | children = ( 196 | 69ABD0C41DB391E100CB925C /* MyCards */, 197 | 51C5D33B1FDCEDBD002A0944 /* MyCardsTests */, 198 | 69ABD0C31DB391E100CB925C /* Products */, 199 | ); 200 | sourceTree = ""; 201 | }; 202 | 69ABD0C31DB391E100CB925C /* Products */ = { 203 | isa = PBXGroup; 204 | children = ( 205 | 69ABD0C21DB391E100CB925C /* MyCards.app */, 206 | 51C5D33A1FDCEDBD002A0944 /* MyCardsTests.xctest */, 207 | ); 208 | name = Products; 209 | sourceTree = ""; 210 | }; 211 | 69ABD0C41DB391E100CB925C /* MyCards */ = { 212 | isa = PBXGroup; 213 | children = ( 214 | 69ABD0DD1DB3938F00CB925C /* Source code */, 215 | ); 216 | path = MyCards; 217 | sourceTree = ""; 218 | }; 219 | 69ABD0DC1DB3938500CB925C /* Resources */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | 691D7D0C1E436E5600E53AF1 /* iTunesArtwork@2x */, 223 | 691D7D0B1E436E4D00E53AF1 /* iTunesArtwork */, 224 | 69769AEB1DE2203700DF2747 /* LaunchScreen.storyboard */, 225 | 69AB4B311DD676890014B996 /* .swiftlint.yml */, 226 | 69ABD0CF1DB391E100CB925C /* Assets.xcassets */, 227 | 69ABD0D41DB391E100CB925C /* Info.plist */, 228 | 69ABD0CC1DB391E100CB925C /* DataModel.xcdatamodeld */, 229 | 6981E6371E9A43B100AA1D5A /* cards.json */, 230 | ); 231 | name = Resources; 232 | sourceTree = ""; 233 | }; 234 | 69ABD0DD1DB3938F00CB925C /* Source code */ = { 235 | isa = PBXGroup; 236 | children = ( 237 | 69ABD0C51DB391E100CB925C /* AppDelegate.swift */, 238 | 69DDFA7A1DB3E7DE00D856C5 /* Model */, 239 | 6965BB641DF7E64B009F9CBC /* View */, 240 | 69ABD0E81DB3A2F900CB925C /* Controller */, 241 | 69ABD0E41DB39B7100CB925C /* Persistence */, 242 | 69AB4B2F1DD674D10014B996 /* Network */, 243 | 69ABD0E51DB39B8400CB925C /* Extensions */, 244 | 69ABD0DC1DB3938500CB925C /* Resources */, 245 | ); 246 | name = "Source code"; 247 | sourceTree = ""; 248 | }; 249 | 69ABD0E41DB39B7100CB925C /* Persistence */ = { 250 | isa = PBXGroup; 251 | children = ( 252 | 6965BB651DF7E7BC009F9CBC /* ManagedObjectConvertible.swift */, 253 | 6965BB661DF7E7BC009F9CBC /* ManagedObjectProtocol.swift */, 254 | 69ABD0DA1DB3932700CB925C /* CoreDataService.swift */, 255 | 69ABD0E01DB3955000CB925C /* CoreDataWorker.swift */, 256 | ); 257 | name = Persistence; 258 | sourceTree = ""; 259 | }; 260 | 69ABD0E51DB39B8400CB925C /* Extensions */ = { 261 | isa = PBXGroup; 262 | children = ( 263 | 69DDFA781DB3E7B200D856C5 /* Array+Extension.swift */, 264 | 694A73F11DF496FE00424107 /* AVCaptureDevice+Extension.swift */, 265 | 69D170771DF5775D009E5710 /* CGFloat+Extension.swift */, 266 | 699795481DEB631500CB408F /* CGRect+Extension.swift */, 267 | 6981E6351E9A412400AA1D5A /* Data+Extension.swift */, 268 | 6976F6DF1E22F75300EA7B43 /* NotificationCenter+Extension.swift */, 269 | 69AB4B291DD652270014B996 /* NSLayoutConstraint+Extension.swift */, 270 | 69DDFA761DB3E70900D856C5 /* NSLayoutFormatOptions+Extension.swift */, 271 | 69AB4B2B1DD66F190014B996 /* NSObject+Builder.swift */, 272 | 693A44C61DD7422700EDCABA /* String+Localized.swift */, 273 | 696C84471E463B1A00F73EFD /* UICollectionView+Extension.swift */, 274 | 69769AED1DE2324D00DF2747 /* UIColor+Extension.swift */, 275 | 69FD28F71EB608B700B3B664 /* UIImage+Extension.swift */, 276 | 69769AF11DE241D100DF2747 /* UIImagePickerController+Extension.swift */, 277 | 69769AEF1DE241A300DF2747 /* UIImagePickerControllerSourceType+Extension.swift */, 278 | 69D170791DF579C5009E5710 /* UINavigationController+Extension.swift */, 279 | 691D7D111E44050F00E53AF1 /* UITextField+Extension.swift */, 280 | 698C57EB1E4759EC0035A8B7 /* UIView+Constrained.swift */, 281 | 694A73F31DF4972700424107 /* UIViewController+Extension.swift */, 282 | 69FD28F91EB64F6E00B3B664 /* CGAffineTransform+Rotate.swift */, 283 | ); 284 | name = Extensions; 285 | sourceTree = ""; 286 | }; 287 | 69ABD0E81DB3A2F900CB925C /* Controller */ = { 288 | isa = PBXGroup; 289 | children = ( 290 | 69ABD0DE1DB3953600CB925C /* CardsViewController.swift */, 291 | 69ABD0E61DB3A1A300CB925C /* CardDetailsViewController.swift */, 292 | 693A2F5F1DE4D19F00975FA1 /* PhotoCaptureViewController.swift */, 293 | 69D170751DF4CBDA009E5710 /* CardPhotoViewController.swift */, 294 | 69D1707B1DF57AAA009E5710 /* LightStatusBarViewController.swift */, 295 | 6967D4261DFAC163001A073B /* CropViewController.swift */, 296 | 691D7D0F1E4404ED00E53AF1 /* ImagePickerController.swift */, 297 | ); 298 | name = Controller; 299 | sourceTree = ""; 300 | }; 301 | 69DDFA7A1DB3E7DE00D856C5 /* Model */ = { 302 | isa = PBXGroup; 303 | children = ( 304 | 6965BB601DF7E0E8009F9CBC /* CardMO+CoreDataClass.swift */, 305 | 6965BB611DF7E0E8009F9CBC /* CardMO+CoreDataProperties.swift */, 306 | 69ABD0E21DB395F800CB925C /* Result.swift */, 307 | 69DDFA741DB3E6D500D856C5 /* Card.swift */, 308 | ); 309 | name = Model; 310 | sourceTree = ""; 311 | }; 312 | /* End PBXGroup section */ 313 | 314 | /* Begin PBXNativeTarget section */ 315 | 51C5D3391FDCEDBD002A0944 /* MyCardsTests */ = { 316 | isa = PBXNativeTarget; 317 | buildConfigurationList = 51C5D3431FDCEDBD002A0944 /* Build configuration list for PBXNativeTarget "MyCardsTests" */; 318 | buildPhases = ( 319 | 51C5D3361FDCEDBD002A0944 /* Sources */, 320 | 51C5D3371FDCEDBD002A0944 /* Frameworks */, 321 | 51C5D3381FDCEDBD002A0944 /* Resources */, 322 | ); 323 | buildRules = ( 324 | ); 325 | dependencies = ( 326 | 51C5D3401FDCEDBD002A0944 /* PBXTargetDependency */, 327 | ); 328 | name = MyCardsTests; 329 | productName = MyCardsTests; 330 | productReference = 51C5D33A1FDCEDBD002A0944 /* MyCardsTests.xctest */; 331 | productType = "com.apple.product-type.bundle.unit-test"; 332 | }; 333 | 69ABD0C11DB391E100CB925C /* MyCards */ = { 334 | isa = PBXNativeTarget; 335 | buildConfigurationList = 69ABD0D71DB391E100CB925C /* Build configuration list for PBXNativeTarget "MyCards" */; 336 | buildPhases = ( 337 | 69AB4B301DD6753A0014B996 /* SwiftLint */, 338 | 69ABD0BE1DB391E100CB925C /* Sources */, 339 | 69ABD0BF1DB391E100CB925C /* Frameworks */, 340 | 69ABD0C01DB391E100CB925C /* Resources */, 341 | ); 342 | buildRules = ( 343 | ); 344 | dependencies = ( 345 | ); 346 | name = MyCards; 347 | productName = MyCards; 348 | productReference = 69ABD0C21DB391E100CB925C /* MyCards.app */; 349 | productType = "com.apple.product-type.application"; 350 | }; 351 | /* End PBXNativeTarget section */ 352 | 353 | /* Begin PBXProject section */ 354 | 69ABD0BA1DB391E100CB925C /* Project object */ = { 355 | isa = PBXProject; 356 | attributes = { 357 | LastSwiftUpdateCheck = 0900; 358 | LastUpgradeCheck = 0900; 359 | ORGANIZATIONNAME = "Maciej Piotrowski"; 360 | TargetAttributes = { 361 | 51C5D3391FDCEDBD002A0944 = { 362 | CreatedOnToolsVersion = 9.0; 363 | ProvisioningStyle = Manual; 364 | TestTargetID = 69ABD0C11DB391E100CB925C; 365 | }; 366 | 69ABD0C11DB391E100CB925C = { 367 | CreatedOnToolsVersion = 8.0; 368 | DevelopmentTeam = 63UY7K2YNY; 369 | LastSwiftMigration = 0900; 370 | ProvisioningStyle = Automatic; 371 | }; 372 | }; 373 | }; 374 | buildConfigurationList = 69ABD0BD1DB391E100CB925C /* Build configuration list for PBXProject "MyCards" */; 375 | compatibilityVersion = "Xcode 3.2"; 376 | developmentRegion = English; 377 | hasScannedForEncodings = 0; 378 | knownRegions = ( 379 | en, 380 | Base, 381 | ); 382 | mainGroup = 69ABD0B91DB391E100CB925C; 383 | productRefGroup = 69ABD0C31DB391E100CB925C /* Products */; 384 | projectDirPath = ""; 385 | projectRoot = ""; 386 | targets = ( 387 | 69ABD0C11DB391E100CB925C /* MyCards */, 388 | 51C5D3391FDCEDBD002A0944 /* MyCardsTests */, 389 | ); 390 | }; 391 | /* End PBXProject section */ 392 | 393 | /* Begin PBXResourcesBuildPhase section */ 394 | 51C5D3381FDCEDBD002A0944 /* Resources */ = { 395 | isa = PBXResourcesBuildPhase; 396 | buildActionMask = 2147483647; 397 | files = ( 398 | ); 399 | runOnlyForDeploymentPostprocessing = 0; 400 | }; 401 | 69ABD0C01DB391E100CB925C /* Resources */ = { 402 | isa = PBXResourcesBuildPhase; 403 | buildActionMask = 2147483647; 404 | files = ( 405 | 691D7D0D1E436E5F00E53AF1 /* iTunesArtwork@2x in Resources */, 406 | 691D7D0E1E436E6100E53AF1 /* iTunesArtwork in Resources */, 407 | 69ABD0D01DB391E100CB925C /* Assets.xcassets in Resources */, 408 | 69769AEC1DE2203700DF2747 /* LaunchScreen.storyboard in Resources */, 409 | 6981E6381E9A43B100AA1D5A /* cards.json in Resources */, 410 | ); 411 | runOnlyForDeploymentPostprocessing = 0; 412 | }; 413 | /* End PBXResourcesBuildPhase section */ 414 | 415 | /* Begin PBXShellScriptBuildPhase section */ 416 | 69AB4B301DD6753A0014B996 /* SwiftLint */ = { 417 | isa = PBXShellScriptBuildPhase; 418 | buildActionMask = 2147483647; 419 | files = ( 420 | ); 421 | inputPaths = ( 422 | ); 423 | name = SwiftLint; 424 | outputPaths = ( 425 | ); 426 | runOnlyForDeploymentPostprocessing = 0; 427 | shellPath = /bin/sh; 428 | shellScript = "\nif which swiftlint >/dev/null; then\nswiftlint autocorrect\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; 429 | }; 430 | /* End PBXShellScriptBuildPhase section */ 431 | 432 | /* Begin PBXSourcesBuildPhase section */ 433 | 51C5D3361FDCEDBD002A0944 /* Sources */ = { 434 | isa = PBXSourcesBuildPhase; 435 | buildActionMask = 2147483647; 436 | files = ( 437 | 51C5D3451FDCF4CB002A0944 /* CardParserConstants.swift in Sources */, 438 | 51C5D33D1FDCEDBD002A0944 /* CardParserTests.swift in Sources */, 439 | ); 440 | runOnlyForDeploymentPostprocessing = 0; 441 | }; 442 | 69ABD0BE1DB391E100CB925C /* Sources */ = { 443 | isa = PBXSourcesBuildPhase; 444 | buildActionMask = 2147483647; 445 | files = ( 446 | 69D1707A1DF579C5009E5710 /* UINavigationController+Extension.swift in Sources */, 447 | 69D170761DF4CBDA009E5710 /* CardPhotoViewController.swift in Sources */, 448 | 6991C0751DE6488E00442B8E /* PreviewView.swift in Sources */, 449 | 69769AEE1DE2324D00DF2747 /* UIColor+Extension.swift in Sources */, 450 | 691D7D121E44050F00E53AF1 /* UITextField+Extension.swift in Sources */, 451 | 693A2F601DE4D19F00975FA1 /* PhotoCaptureViewController.swift in Sources */, 452 | 694A73F41DF4972700424107 /* UIViewController+Extension.swift in Sources */, 453 | 698C57EC1E4759EC0035A8B7 /* UIView+Constrained.swift in Sources */, 454 | 69DDFA751DB3E6D500D856C5 /* Card.swift in Sources */, 455 | 694A73F21DF496FE00424107 /* AVCaptureDevice+Extension.swift in Sources */, 456 | 69ABD0E31DB395F800CB925C /* Result.swift in Sources */, 457 | 69DDFA791DB3E7B200D856C5 /* Array+Extension.swift in Sources */, 458 | 69AB4B2E1DD674C80014B996 /* NetworkLoader.swift in Sources */, 459 | 69ABD0E11DB3955000CB925C /* CoreDataWorker.swift in Sources */, 460 | 69DDFA731DB3E6AD00D856C5 /* CardCell.swift in Sources */, 461 | 69ABD0DF1DB3953600CB925C /* CardsViewController.swift in Sources */, 462 | 6981E6361E9A412400AA1D5A /* Data+Extension.swift in Sources */, 463 | 6965BB631DF7E0E8009F9CBC /* CardMO+CoreDataProperties.swift in Sources */, 464 | 6965BB681DF7E7BC009F9CBC /* ManagedObjectProtocol.swift in Sources */, 465 | 69FD28F81EB608B700B3B664 /* UIImage+Extension.swift in Sources */, 466 | 69D170781DF5775D009E5710 /* CGFloat+Extension.swift in Sources */, 467 | 69ABD0C61DB391E100CB925C /* AppDelegate.swift in Sources */, 468 | 69769AF01DE241A300DF2747 /* UIImagePickerControllerSourceType+Extension.swift in Sources */, 469 | 69D1707C1DF57AAA009E5710 /* LightStatusBarViewController.swift in Sources */, 470 | 69183D411DE653420026C1DF /* PhotoCameraButton.swift in Sources */, 471 | 693A44CB1DD7D9CE00EDCABA /* TappableView.swift in Sources */, 472 | 69183D431DE653710026C1DF /* CloseButton.swift in Sources */, 473 | 6981E6441E9ABF0500AA1D5A /* CardDetailsViewController.swift in Sources */, 474 | 698319891DD9134F0091ED64 /* PhotoCamera.swift in Sources */, 475 | 699795491DEB631500CB408F /* CGRect+Extension.swift in Sources */, 476 | 698C57EE1E475D700035A8B7 /* PreviewOutline.swift in Sources */, 477 | 693A44C71DD7422700EDCABA /* String+Localized.swift in Sources */, 478 | 69ABD0CE1DB391E100CB925C /* DataModel.xcdatamodeld in Sources */, 479 | 69AB4B2C1DD66F190014B996 /* NSObject+Builder.swift in Sources */, 480 | 69FD28FA1EB64F6E00B3B664 /* CGAffineTransform+Rotate.swift in Sources */, 481 | 6976F6E01E22F75300EA7B43 /* NotificationCenter+Extension.swift in Sources */, 482 | 6965BB671DF7E7BC009F9CBC /* ManagedObjectConvertible.swift in Sources */, 483 | 691D7D101E4404ED00E53AF1 /* ImagePickerController.swift in Sources */, 484 | 69769AF21DE241D100DF2747 /* UIImagePickerController+Extension.swift in Sources */, 485 | 6967D4271DFAC163001A073B /* CropViewController.swift in Sources */, 486 | 6981E6431E9AB9F600AA1D5A /* NetworLoader+Shared.swift in Sources */, 487 | 696C84481E463B1A00F73EFD /* UICollectionView+Extension.swift in Sources */, 488 | 6981E6411E9AB9B000AA1D5A /* CardParser.swift in Sources */, 489 | 69AB4B2A1DD652270014B996 /* NSLayoutConstraint+Extension.swift in Sources */, 490 | 69DDFA771DB3E70900D856C5 /* NSLayoutFormatOptions+Extension.swift in Sources */, 491 | 6965BB621DF7E0E8009F9CBC /* CardMO+CoreDataClass.swift in Sources */, 492 | 693A44C91DD7944B00EDCABA /* CardView.swift in Sources */, 493 | 69ABD0DB1DB3932700CB925C /* CoreDataService.swift in Sources */, 494 | ); 495 | runOnlyForDeploymentPostprocessing = 0; 496 | }; 497 | /* End PBXSourcesBuildPhase section */ 498 | 499 | /* Begin PBXTargetDependency section */ 500 | 51C5D3401FDCEDBD002A0944 /* PBXTargetDependency */ = { 501 | isa = PBXTargetDependency; 502 | target = 69ABD0C11DB391E100CB925C /* MyCards */; 503 | targetProxy = 51C5D33F1FDCEDBD002A0944 /* PBXContainerItemProxy */; 504 | }; 505 | /* End PBXTargetDependency section */ 506 | 507 | /* Begin XCBuildConfiguration section */ 508 | 51C5D3411FDCEDBD002A0944 /* Debug */ = { 509 | isa = XCBuildConfiguration; 510 | buildSettings = { 511 | BUNDLE_LOADER = "$(TEST_HOST)"; 512 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 513 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 514 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 515 | CODE_SIGN_IDENTITY = "iPhone Developer"; 516 | CODE_SIGN_STYLE = Manual; 517 | DEVELOPMENT_TEAM = ""; 518 | GCC_C_LANGUAGE_STANDARD = gnu11; 519 | INFOPLIST_FILE = MyCardsTests/Info.plist; 520 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 521 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 522 | PRODUCT_BUNDLE_IDENTIFIER = io.swifting.MyCardsTests; 523 | PRODUCT_NAME = "$(TARGET_NAME)"; 524 | PROVISIONING_PROFILE_SPECIFIER = ""; 525 | SWIFT_VERSION = 4.0; 526 | TARGETED_DEVICE_FAMILY = "1,2"; 527 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MyCards.app/MyCards"; 528 | }; 529 | name = Debug; 530 | }; 531 | 51C5D3421FDCEDBD002A0944 /* Release */ = { 532 | isa = XCBuildConfiguration; 533 | buildSettings = { 534 | BUNDLE_LOADER = "$(TEST_HOST)"; 535 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 536 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 537 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 538 | CODE_SIGN_IDENTITY = "iPhone Developer"; 539 | CODE_SIGN_STYLE = Manual; 540 | DEVELOPMENT_TEAM = ""; 541 | GCC_C_LANGUAGE_STANDARD = gnu11; 542 | INFOPLIST_FILE = MyCardsTests/Info.plist; 543 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 544 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 545 | PRODUCT_BUNDLE_IDENTIFIER = io.swifting.MyCardsTests; 546 | PRODUCT_NAME = "$(TARGET_NAME)"; 547 | PROVISIONING_PROFILE_SPECIFIER = ""; 548 | SWIFT_VERSION = 4.0; 549 | TARGETED_DEVICE_FAMILY = "1,2"; 550 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MyCards.app/MyCards"; 551 | }; 552 | name = Release; 553 | }; 554 | 69ABD0D51DB391E100CB925C /* Debug */ = { 555 | isa = XCBuildConfiguration; 556 | buildSettings = { 557 | ALWAYS_SEARCH_USER_PATHS = NO; 558 | CLANG_ANALYZER_NONNULL = YES; 559 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 560 | CLANG_CXX_LIBRARY = "libc++"; 561 | CLANG_ENABLE_MODULES = YES; 562 | CLANG_ENABLE_OBJC_ARC = YES; 563 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 564 | CLANG_WARN_BOOL_CONVERSION = YES; 565 | CLANG_WARN_COMMA = YES; 566 | CLANG_WARN_CONSTANT_CONVERSION = YES; 567 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 568 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 569 | CLANG_WARN_EMPTY_BODY = YES; 570 | CLANG_WARN_ENUM_CONVERSION = YES; 571 | CLANG_WARN_INFINITE_RECURSION = YES; 572 | CLANG_WARN_INT_CONVERSION = YES; 573 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 574 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 575 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 576 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 577 | CLANG_WARN_STRICT_PROTOTYPES = YES; 578 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 579 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 580 | CLANG_WARN_UNREACHABLE_CODE = YES; 581 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 582 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 583 | COPY_PHASE_STRIP = NO; 584 | DEBUG_INFORMATION_FORMAT = dwarf; 585 | ENABLE_STRICT_OBJC_MSGSEND = YES; 586 | ENABLE_TESTABILITY = YES; 587 | GCC_C_LANGUAGE_STANDARD = gnu99; 588 | GCC_DYNAMIC_NO_PIC = NO; 589 | GCC_NO_COMMON_BLOCKS = YES; 590 | GCC_OPTIMIZATION_LEVEL = 0; 591 | GCC_PREPROCESSOR_DEFINITIONS = ( 592 | "DEBUG=1", 593 | "$(inherited)", 594 | ); 595 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 596 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 597 | GCC_WARN_UNDECLARED_SELECTOR = YES; 598 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 599 | GCC_WARN_UNUSED_FUNCTION = YES; 600 | GCC_WARN_UNUSED_VARIABLE = YES; 601 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 602 | MTL_ENABLE_DEBUG_INFO = YES; 603 | ONLY_ACTIVE_ARCH = YES; 604 | SDKROOT = iphoneos; 605 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 606 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 607 | }; 608 | name = Debug; 609 | }; 610 | 69ABD0D61DB391E100CB925C /* Release */ = { 611 | isa = XCBuildConfiguration; 612 | buildSettings = { 613 | ALWAYS_SEARCH_USER_PATHS = NO; 614 | CLANG_ANALYZER_NONNULL = YES; 615 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 616 | CLANG_CXX_LIBRARY = "libc++"; 617 | CLANG_ENABLE_MODULES = YES; 618 | CLANG_ENABLE_OBJC_ARC = YES; 619 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 620 | CLANG_WARN_BOOL_CONVERSION = YES; 621 | CLANG_WARN_COMMA = YES; 622 | CLANG_WARN_CONSTANT_CONVERSION = YES; 623 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 624 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 625 | CLANG_WARN_EMPTY_BODY = YES; 626 | CLANG_WARN_ENUM_CONVERSION = YES; 627 | CLANG_WARN_INFINITE_RECURSION = YES; 628 | CLANG_WARN_INT_CONVERSION = YES; 629 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 630 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 631 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 632 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 633 | CLANG_WARN_STRICT_PROTOTYPES = YES; 634 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 635 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 636 | CLANG_WARN_UNREACHABLE_CODE = YES; 637 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 638 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 639 | COPY_PHASE_STRIP = NO; 640 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 641 | ENABLE_NS_ASSERTIONS = NO; 642 | ENABLE_STRICT_OBJC_MSGSEND = YES; 643 | GCC_C_LANGUAGE_STANDARD = gnu99; 644 | GCC_NO_COMMON_BLOCKS = YES; 645 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 646 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 647 | GCC_WARN_UNDECLARED_SELECTOR = YES; 648 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 649 | GCC_WARN_UNUSED_FUNCTION = YES; 650 | GCC_WARN_UNUSED_VARIABLE = YES; 651 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 652 | MTL_ENABLE_DEBUG_INFO = NO; 653 | SDKROOT = iphoneos; 654 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 655 | VALIDATE_PRODUCT = YES; 656 | }; 657 | name = Release; 658 | }; 659 | 69ABD0D81DB391E100CB925C /* Debug */ = { 660 | isa = XCBuildConfiguration; 661 | buildSettings = { 662 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 663 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 664 | DEVELOPMENT_TEAM = ""; 665 | INFOPLIST_FILE = MyCards/Info.plist; 666 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 667 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 668 | PRODUCT_BUNDLE_IDENTIFIER = io.swifting.mycards; 669 | PRODUCT_NAME = "$(TARGET_NAME)"; 670 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 671 | SWIFT_VERSION = 4.0; 672 | }; 673 | name = Debug; 674 | }; 675 | 69ABD0D91DB391E100CB925C /* Release */ = { 676 | isa = XCBuildConfiguration; 677 | buildSettings = { 678 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 679 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 680 | DEVELOPMENT_TEAM = ""; 681 | INFOPLIST_FILE = MyCards/Info.plist; 682 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 683 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 684 | PRODUCT_BUNDLE_IDENTIFIER = io.swifting.mycards; 685 | PRODUCT_NAME = "$(TARGET_NAME)"; 686 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 687 | SWIFT_VERSION = 4.0; 688 | }; 689 | name = Release; 690 | }; 691 | /* End XCBuildConfiguration section */ 692 | 693 | /* Begin XCConfigurationList section */ 694 | 51C5D3431FDCEDBD002A0944 /* Build configuration list for PBXNativeTarget "MyCardsTests" */ = { 695 | isa = XCConfigurationList; 696 | buildConfigurations = ( 697 | 51C5D3411FDCEDBD002A0944 /* Debug */, 698 | 51C5D3421FDCEDBD002A0944 /* Release */, 699 | ); 700 | defaultConfigurationIsVisible = 0; 701 | defaultConfigurationName = Release; 702 | }; 703 | 69ABD0BD1DB391E100CB925C /* Build configuration list for PBXProject "MyCards" */ = { 704 | isa = XCConfigurationList; 705 | buildConfigurations = ( 706 | 69ABD0D51DB391E100CB925C /* Debug */, 707 | 69ABD0D61DB391E100CB925C /* Release */, 708 | ); 709 | defaultConfigurationIsVisible = 0; 710 | defaultConfigurationName = Release; 711 | }; 712 | 69ABD0D71DB391E100CB925C /* Build configuration list for PBXNativeTarget "MyCards" */ = { 713 | isa = XCConfigurationList; 714 | buildConfigurations = ( 715 | 69ABD0D81DB391E100CB925C /* Debug */, 716 | 69ABD0D91DB391E100CB925C /* Release */, 717 | ); 718 | defaultConfigurationIsVisible = 0; 719 | defaultConfigurationName = Release; 720 | }; 721 | /* End XCConfigurationList section */ 722 | 723 | /* Begin XCVersionGroup section */ 724 | 69ABD0CC1DB391E100CB925C /* DataModel.xcdatamodeld */ = { 725 | isa = XCVersionGroup; 726 | children = ( 727 | 69ABD0CD1DB391E100CB925C /* DataModel.xcdatamodel */, 728 | ); 729 | currentVersion = 69ABD0CD1DB391E100CB925C /* DataModel.xcdatamodel */; 730 | path = DataModel.xcdatamodeld; 731 | sourceTree = ""; 732 | versionGroupType = wrapper.xcdatamodel; 733 | }; 734 | /* End XCVersionGroup section */ 735 | }; 736 | rootObject = 69ABD0BA1DB391E100CB925C /* Project object */; 737 | } 738 | -------------------------------------------------------------------------------- /MyCards/MyCards.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MyCards/MyCards.xcodeproj/xcshareddata/xcschemes/MyCards.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 66 | 68 | 74 | 75 | 76 | 77 | 78 | 79 | 85 | 87 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /MyCards/MyCards/AVCaptureDevice+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVCaptureDevice+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 04/12/16. 6 | // 7 | 8 | import AVFoundation 9 | 10 | extension AVCaptureDevice { 11 | class var dualBackVideoCamera: AVCaptureDevice? { 12 | return AVCaptureDevice.default(AVCaptureDevice.DeviceType.builtInDualCamera, for: AVMediaType.video, position: .back) 13 | } 14 | 15 | class var dualFrontVideoCamera: AVCaptureDevice? { 16 | return AVCaptureDevice.default(AVCaptureDevice.DeviceType.builtInDualCamera, for: AVMediaType.video, position: .front) 17 | } 18 | 19 | class var wideAngleBackVideoCamera: AVCaptureDevice? { 20 | return AVCaptureDevice.default(AVCaptureDevice.DeviceType.builtInWideAngleCamera, for: AVMediaType.video, position: .back) 21 | } 22 | 23 | class var wideAngleFrontVideoCamera: AVCaptureDevice? { 24 | return AVCaptureDevice.default(AVCaptureDevice.DeviceType.builtInWideAngleCamera, for: AVMediaType.video, position: .front) 25 | } 26 | 27 | class var backVideoCamera: AVCaptureDevice? { 28 | return dualBackVideoCamera ?? wideAngleBackVideoCamera 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MyCards/MyCards/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 16/10/16. 6 | // 7 | 8 | import UIKit 9 | import CoreData 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions 17 | launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | configureWindow() 19 | configureNavigationBar() 20 | configureTextField() 21 | return true 22 | } 23 | 24 | private func configureNavigationBar() { 25 | let appearance = UINavigationBar.appearance() 26 | appearance.tintColor = .orange 27 | appearance.titleTextAttributes = [ 28 | NSAttributedStringKey.foregroundColor: UIColor.titleBlue, 29 | NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .callout), 30 | ] 31 | } 32 | 33 | func configureTextField() { 34 | let appearance = UITextField.appearance() 35 | appearance.textColor = .orange 36 | } 37 | 38 | private func configureWindow() { 39 | let window = UIWindow(frame: UIScreen.main.bounds) 40 | let cardsViewController = CardsViewController() 41 | window.backgroundColor = .white 42 | window.rootViewController = UINavigationController(rootViewController: cardsViewController) 43 | window.makeKeyAndVisible() 44 | self.window = window 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /MyCards/MyCards/Array+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 16/10/16. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Array { 11 | subscript(safe position: Index) -> Element? { 12 | // Thanks Mike Ash 13 | return (0.. Bool { 86 | return lhs.name == rhs.name && 87 | lhs.identifier == rhs.identifier && 88 | lhs.front?.base64EncodedString == rhs.front?.base64EncodedString && 89 | lhs.back?.base64EncodedString == rhs.back?.base64EncodedString 90 | } 91 | 92 | } 93 | 94 | extension UIImage { 95 | var data: Data? { 96 | return UIImagePNGRepresentation(self) 97 | } 98 | 99 | var base64EncodedString: String? { 100 | return self.data.flatMap { $0.base64EncodedString() } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /MyCards/MyCards/CardCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardCell.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 16/10/16. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol IndexedCell { 11 | var indexPath: IndexPath? { get set } 12 | } 13 | protocol IndexedCellDelegate: class { 14 | func cellWasTapped(_ cell: IndexedCell) 15 | } 16 | 17 | class CardCell: UICollectionViewCell, IndexedCell { 18 | 19 | fileprivate let nameLabel: UILabel 20 | fileprivate let imageView: UIImageView 21 | fileprivate let tappableView: TappableView 22 | 23 | weak var delegate: IndexedCellDelegate? 24 | var indexPath: IndexPath? 25 | var name: String? { 26 | set { 27 | nameLabel.text = newValue 28 | nameLabel.sizeToFit() 29 | } 30 | get { 31 | return nameLabel.text 32 | } 33 | } 34 | 35 | var image: UIImage? { 36 | set { 37 | imageView.image = newValue 38 | } 39 | get { 40 | return imageView.image 41 | } 42 | } 43 | 44 | override init(frame: CGRect) { 45 | nameLabel = UILabel(frame: .zero) 46 | imageView = UIImageView(frame: .zero) 47 | tappableView = TappableView(frame: .zero) 48 | super.init(frame: frame) 49 | configureViews() 50 | configureConstraints() 51 | } 52 | 53 | required init?(coder aDecoder: NSCoder) { 54 | fatalError("init(coder:) has not been implemented") 55 | } 56 | 57 | override func prepareForReuse() { 58 | super.prepareForReuse() 59 | name = nil 60 | image = nil 61 | indexPath = nil 62 | } 63 | } 64 | 65 | extension CardCell { 66 | fileprivate func configureViews() { 67 | tappableView.contentView.addSubview(imageView) 68 | tappableView.contentView.addSubview(nameLabel) 69 | contentView.addSubview(tappableView) 70 | 71 | imageView.contentMode = .scaleAspectFill 72 | nameLabel.textColor = .white 73 | nameLabel.font = UIFont.preferredFont(forTextStyle: .title1) 74 | tappableView.layer.cornerRadius = 10 75 | tappableView.tapped = { [unowned self] in self.tappableViewWasTapped() } 76 | } 77 | 78 | fileprivate func configureConstraints() { 79 | contentView.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } 80 | tappableView.contentView.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } 81 | var constraints: [NSLayoutConstraint] = [] 82 | constraints += NSLayoutConstraint.centeredInSuperview(nameLabel) 83 | constraints += NSLayoutConstraint.filledInSuperview(imageView) 84 | constraints += NSLayoutConstraint.filledInSuperview(tappableView) 85 | NSLayoutConstraint.activate(constraints) 86 | } 87 | 88 | func tappableViewWasTapped() { 89 | delegate?.cellWasTapped(self) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /MyCards/MyCards/CardDetailsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardDetailsViewController.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 16/10/16. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CardDetailsViewController: UIViewController { 11 | 12 | fileprivate var history: [Card] = [] 13 | fileprivate var card: Card { 14 | didSet { 15 | front.image = card.front 16 | back.image = card.back 17 | name.text = card.name 18 | doneButton.isEnabled = card.isValid 19 | } 20 | } 21 | fileprivate var mode: Mode { 22 | didSet { 23 | configureNavigationItem() 24 | configureModeForViews() 25 | } 26 | } 27 | fileprivate let worker: CoreDataWorkerProtocol 28 | fileprivate let loader: ResourceLoading 29 | 30 | // MARK: Views 31 | // codebeat:disable[TOO_MANY_IVARS] 32 | fileprivate lazy var editButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: 33 | .edit, target: self, action: #selector(editTapped)) 34 | fileprivate lazy var cancelButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: 35 | .cancel, target: self, action: #selector(cancelTapped)) 36 | fileprivate lazy var doneButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: 37 | .done, target: self, action: #selector(doneTapped)) 38 | fileprivate lazy var front: CardView = CardView(image: self.card.front).with { 39 | $0.tapped = { [unowned self] in self.cardTapped(.front) } 40 | } 41 | fileprivate lazy var back: CardView = CardView(image: self.card.back) .with { 42 | $0.tapped = { [unowned self] in self.cardTapped(.back) } 43 | } 44 | fileprivate lazy var name: UITextField = UITextField.makeNameField().with { 45 | $0.text = self.card.name 46 | $0.autocapitalizationType = .sentences 47 | $0.delegate = self 48 | $0.addTarget(self, action: #selector(nameChanged(sender:)), for: .editingChanged) 49 | } 50 | fileprivate lazy var toolbar: UIToolbar = UIToolbar.constrained().with { 51 | let delete = UIBarButtonItem(barButtonSystemItem: 52 | .trash, target: self, action: #selector(removeTapped)) 53 | let flexible = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) 54 | $0.items = [flexible, delete, flexible] 55 | } 56 | // codebeat:enable[TOO_MANY_IVARS] 57 | 58 | init(card: Card, 59 | mode: Mode = .normal, 60 | worker: CoreDataWorkerProtocol = CoreDataWorker(), 61 | loader: ResourceLoading = NetworkLoader.shared) { 62 | self.card = card 63 | self.mode = mode 64 | self.worker = worker 65 | self.loader = loader 66 | self.history.append(self.card) 67 | super.init(nibName: nil, bundle: nil) 68 | } 69 | 70 | required init?(coder aDecoder: NSCoder) { 71 | fatalError("NSCoding not supported") 72 | } 73 | 74 | override func viewDidLoad() { 75 | super.viewDidLoad() 76 | configureViews() 77 | configureNavigationItem() 78 | configureModeForViews() 79 | configureConstraints() 80 | } 81 | } 82 | 83 | extension CardDetailsViewController { 84 | fileprivate func configureViews() { 85 | view.backgroundColor = .white 86 | view.addSubview(name) 87 | view.addSubview(front) 88 | view.addSubview(back) 89 | view.addSubview(toolbar) 90 | } 91 | 92 | fileprivate func configureConstraints() { 93 | view.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } 94 | var constraints: [NSLayoutConstraint] = [] 95 | constraints.append(name.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40)) 96 | constraints.append(name.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20)) 97 | constraints.append(name.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20)) 98 | constraints.append(front.leadingAnchor.constraint(equalTo: name.leadingAnchor, constant: 0)) 99 | constraints.append(front.trailingAnchor.constraint(equalTo: name.trailingAnchor, constant: 0)) 100 | constraints.append(front.topAnchor.constraint(equalTo: name.bottomAnchor, constant: 20)) 101 | constraints.append(front.heightAnchor.constraint(equalTo: front.widthAnchor, multiplier: 1 / .cardRatio)) 102 | constraints.append(back.topAnchor.constraint(equalTo: front.bottomAnchor, constant: 20)) 103 | constraints.append(back.leadingAnchor.constraint(equalTo: front.leadingAnchor, constant: 0)) 104 | constraints.append(back.trailingAnchor.constraint(equalTo: front.trailingAnchor, constant: 0)) 105 | constraints.append(back.heightAnchor.constraint(equalTo: front.heightAnchor, constant: 0)) 106 | constraints.append(toolbar.heightAnchor.constraint(equalToConstant: 40)) 107 | constraints.append(toolbar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor)) 108 | constraints.append(toolbar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)) 109 | constraints.append(toolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)) 110 | NSLayoutConstraint.activate(constraints) 111 | } 112 | 113 | fileprivate func configureNavigationItem() { 114 | title = mode == .create ? .AddNewCard : .CardDetails 115 | doneButton.isEnabled = card.isValid 116 | switch mode { 117 | case .normal: 118 | navigationItem.rightBarButtonItems = [editButton] 119 | case .edit, .create: 120 | navigationItem.rightBarButtonItems = [doneButton] 121 | } 122 | navigationItem.leftBarButtonItem = cancelButton 123 | } 124 | 125 | fileprivate func configureModeForViews() { 126 | let editMode = mode != .normal 127 | name.resignFirstResponder() 128 | name.isUserInteractionEnabled = editMode 129 | name.placeholder = editMode ? .EnterCardName : .NoName 130 | front.photoCamera.isHidden = !editMode 131 | back.photoCamera.isHidden = !editMode 132 | toolbar.isHidden = !(mode == .edit) 133 | } 134 | 135 | @objc fileprivate func doneTapped() { 136 | guard card.isValid else { return } 137 | persist(card) 138 | switch mode { 139 | case .create: dismiss() 140 | default: mode = .normal 141 | } 142 | } 143 | 144 | @objc fileprivate func removeTapped() { 145 | worker.remove(entities: [card]) { [weak self] error in 146 | error.flatMap { print("\($0)") } 147 | DispatchQueue.main.async { 148 | self?.dismiss() 149 | } 150 | } 151 | } 152 | 153 | @objc fileprivate func cancelTapped() { 154 | switch mode { 155 | case .create, .normal: dismiss() 156 | case .edit: 157 | history.last.flatMap { card = $0 } 158 | mode = .normal 159 | } 160 | } 161 | 162 | @objc fileprivate func editTapped() { 163 | mode = .edit 164 | } 165 | 166 | @objc fileprivate func nameChanged(sender textField: UITextField) { 167 | guard let name = textField.text else { return } 168 | card = Card(identifier: card.identifier, 169 | name: name, 170 | front: card.front, 171 | back: card.back) 172 | } 173 | 174 | fileprivate func persist(_ card: Card) { 175 | guard card.isValid else { return } 176 | worker.upsert(entities: [card]) { [weak self, loader] error in 177 | loader.upload(object: [card], to: "/cards", parser: CardParser()) { _ in } 178 | 179 | guard let strongSelf = self, error == nil else { return } 180 | strongSelf.history.append(card) 181 | } 182 | } 183 | 184 | fileprivate func cardTapped(_ side: Card.Side) { 185 | switch mode { 186 | case .edit, .create: showImagePickerSources(for: side) 187 | case .normal: showImage(for: side) 188 | } 189 | } 190 | 191 | fileprivate func showImagePickerSources(for side: Card.Side) { 192 | view.endEditing(true) 193 | let title: String = .Set + " " + side.description 194 | let actionSheet = UIAlertController(title: title, message: 195 | nil, preferredStyle: .actionSheet) 196 | 197 | let actions: [UIImagePickerControllerSourceType] = UIImagePickerController.availableImagePickerSources() 198 | 199 | actions.forEach { source in 200 | let action = UIAlertAction(title: source.description, style: 201 | .default) { [unowned self] _ in 202 | self.showImagePicker(sourceType: source, for: side) 203 | } 204 | actionSheet.addAction(action) 205 | } 206 | 207 | let cancel = UIAlertAction(title: .Cancel, style: .cancel, handler: nil) 208 | actionSheet.addAction(cancel) 209 | present(actionSheet, animated: true, completion: nil) 210 | } 211 | 212 | fileprivate func showImage(for side: Card.Side) { 213 | view.endEditing(true) 214 | var image: UIImage? 215 | switch side { 216 | case .front: image = front.image 217 | case .back: image = back.image 218 | } 219 | guard let i = image else { return } 220 | let vc = CardPhotoViewController(image: i) 221 | present(vc, animated: true, completion: nil) 222 | } 223 | 224 | private func showImagePicker(sourceType: UIImagePickerControllerSourceType, for side: Card.Side) { 225 | let imagePicker: UIViewController 226 | if sourceType == .camera { 227 | imagePicker = PhotoCaptureViewController(side: side).with { 228 | $0.delegate = self 229 | } 230 | } else { 231 | imagePicker = ImagePickerController().with { 232 | $0.delegate = self 233 | $0.view.backgroundColor = .white 234 | $0.side = side 235 | } 236 | } 237 | present(imagePicker, animated: true, completion: nil) 238 | } 239 | 240 | fileprivate func set(_ image: UIImage, for side: Card.Side) { 241 | var front: UIImage? 242 | var back: UIImage? 243 | switch side { 244 | case .front: 245 | front = image 246 | back = card.back 247 | case .back: 248 | front = card.front 249 | back = image 250 | } 251 | card = Card(identifier: card.identifier, 252 | name: card.name, 253 | front: front, 254 | back: back) 255 | } 256 | } 257 | 258 | extension CardDetailsViewController: UITextFieldDelegate { 259 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 260 | textField.resignFirstResponder() 261 | return true 262 | } 263 | } 264 | 265 | extension CardDetailsViewController { 266 | enum Mode { 267 | case normal 268 | case edit 269 | case create 270 | } 271 | } 272 | 273 | extension CardDetailsViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { 274 | func imagePickerController(_ picker: UIImagePickerController, 275 | didFinishPickingMediaWithInfo info: [String: Any]) { 276 | guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage, 277 | let side = (picker as? ImagePickerController)?.side else { return } 278 | showImageCropping(for: image, side: side) 279 | } 280 | 281 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { 282 | dismiss() 283 | } 284 | 285 | func navigationController(_ navigationController: UINavigationController, willShow 286 | viewController: UIViewController, animated: Bool) { 287 | viewController.title = .SelectCardPhoto 288 | } 289 | 290 | fileprivate func showImageCropping(for image: UIImage = #imageLiteral(resourceName: "background"), 291 | side: Card.Side) { 292 | let vc = CropViewController(image: image, side: side) 293 | vc.delegate = self 294 | dismiss(animated: true) { 295 | self.present(vc, animated: true, completion: nil) 296 | } 297 | } 298 | 299 | fileprivate func process(_ image: UIImage, for side: Card.Side) { 300 | defer { dismiss() } 301 | let width: CGFloat = 600 302 | let height: CGFloat = width / .cardRatio 303 | let size = CGSize(width: width, height: height) 304 | guard let resized = image.resized(to: size) else { return } 305 | set(resized, for: side) 306 | } 307 | } 308 | 309 | extension CardDetailsViewController: PhotoCaptureViewControllerDelegate { 310 | 311 | func photoCaptureViewController(_ viewController: PhotoCaptureViewController, didTakePhoto 312 | photo: UIImage, for side: Card.Side) { 313 | process(photo, for: side) 314 | } 315 | } 316 | 317 | extension CardDetailsViewController: CropViewControllerDelegate { 318 | 319 | func cropViewController(_ viewController: CropViewController, didCropPhoto photo: UIImage, for side: Card.Side) { 320 | process(photo, for: side) 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /MyCards/MyCards/CardMO+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardMO+CoreDataClass.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 07/12/16. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | import UIKit 11 | 12 | public class CardMO: NSManagedObject {} 13 | 14 | extension CardMO: ManagedObjectProtocol { 15 | static var defaultSortDescriptors: [NSSortDescriptor] { 16 | return [NSSortDescriptor(key: "name", ascending: true)] 17 | } 18 | 19 | func toEntity() -> Card? { 20 | var front: UIImage? 21 | var back: UIImage? 22 | self.front.map { front = UIImage(data: $0 as Data) } 23 | self.back.map { back = UIImage(data: $0 as Data) } 24 | return Card(identifier: identifier, name: name, front: front, back: back) 25 | } 26 | } 27 | 28 | extension Card: ManagedObjectConvertible { 29 | func toManagedObject(in context: NSManagedObjectContext) -> CardMO? { 30 | let card = CardMO.getOrCreateSingle(with: identifier, from: context) 31 | card.name = name 32 | card.identifier = identifier 33 | front.flatMap(UIImagePNGRepresentation).flatMap { 34 | card.front = NSData(data: $0) 35 | } 36 | back.flatMap(UIImagePNGRepresentation).flatMap { 37 | card.back = NSData(data: $0) 38 | } 39 | return card 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MyCards/MyCards/CardMO+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardMO+CoreDataProperties.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 07/12/16. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | extension CardMO { 12 | 13 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 14 | return NSFetchRequest(entityName: "CardMO") 15 | } 16 | 17 | @NSManaged public var front: NSData? 18 | @NSManaged public var back: NSData? 19 | @NSManaged public var name: String 20 | @NSManaged public var identifier: String 21 | 22 | } 23 | -------------------------------------------------------------------------------- /MyCards/MyCards/CardParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardParser.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 09/04/17. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CardParser: Parser, JSONDataConverting { 11 | 12 | func parse(_ json: Data, with decoder: JSONDecoder = JSONDecoder()) throws -> Any { 13 | let cards: [Card] = try decoder.decode([Card].self, from: json) 14 | return cards 15 | } 16 | 17 | func json(from object: Any) -> Data? { 18 | guard let cards = object as? [Card] else { return nil } 19 | do { 20 | let encoder = JSONEncoder() 21 | let data = try encoder.encode(cards) 22 | return data 23 | } catch { 24 | print(error.localizedDescription) 25 | return nil 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MyCards/MyCards/CardPhotoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardPhotoViewController.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 04/12/16. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CardPhotoViewController: LightStatusBarViewController { 11 | 12 | fileprivate lazy var imageView: UIImageView = UIImageView(frame: .zero).with { 13 | $0.contentMode = .scaleAspectFill 14 | $0.layer.cornerRadius = 20 15 | $0.clipsToBounds = true 16 | $0.alpha = 0 17 | } 18 | fileprivate lazy var backgroundImageView: UIImageView = UIImageView(frame: .zero).with { 19 | $0.contentMode = .scaleAspectFill 20 | } 21 | fileprivate lazy var backgroundEffectView: UIVisualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) 22 | fileprivate lazy var closeButton: CloseButton = CloseButton(frame: .zero).with { 23 | $0.alpha = 0 24 | $0.tapped = { [unowned self] in self.dismiss() } 25 | } 26 | 27 | init(image: UIImage) { 28 | super.init(nibName: nil, bundle: nil) 29 | imageView.image = UIImage(cgImage: image.cgImage!, scale: 1, orientation: .right) 30 | backgroundImageView.image = imageView.image 31 | modalTransitionStyle = .crossDissolve 32 | } 33 | 34 | required init?(coder aDecoder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | configureViews() 41 | configureConstraints() 42 | } 43 | 44 | override func viewDidAppear(_ animated: Bool) { 45 | super.viewDidAppear(animated) 46 | UIView.animate(withDuration: 0.25) { 47 | self.imageView.alpha = 1 48 | self.closeButton.alpha = 1 49 | } 50 | } 51 | 52 | private func configureViews() { 53 | view.backgroundColor = .black 54 | view.addSubview(backgroundImageView) 55 | view.addSubview(backgroundEffectView) 56 | view.addSubview(imageView) 57 | view.addSubview(closeButton) 58 | } 59 | 60 | private func configureConstraints() { 61 | view.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } 62 | var constraints: [NSLayoutConstraint] = NSLayoutConstraint.filledInSuperview(backgroundImageView) 63 | constraints.append(contentsOf: NSLayoutConstraint.filledInSuperview(backgroundEffectView)) 64 | constraints.append(contentsOf: NSLayoutConstraint.centeredInSuperview(imageView)) 65 | constraints.append(NSLayoutConstraint.height2WidthCardRatio(for: imageView)) 66 | constraints.append(closeButton.heightAnchor.constraint(equalToConstant: 40)) 67 | constraints.append(closeButton.widthAnchor.constraint(equalToConstant: 40)) 68 | constraints.append(closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20)) 69 | constraints.append(closeButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20)) 70 | constraints.append(imageView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: .cardOffsetY)) 71 | constraints.append(imageView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -.cardOffsetY)) 72 | NSLayoutConstraint.activate(constraints) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /MyCards/MyCards/CardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardView.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 12/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | class CardView: TappableView { 11 | 12 | fileprivate let imageView: UIImageView 13 | let photoCamera: PhotoCamera 14 | 15 | init(image: UIImage?) { 16 | imageView = UIImageView(frame: .zero) 17 | photoCamera = PhotoCamera(frame: .zero) 18 | super.init(frame: .zero) 19 | self.image = image 20 | configureViews() 21 | configureConstraints() 22 | } 23 | 24 | required init?(coder aDecoder: NSCoder) { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | 28 | var image: UIImage? { 29 | set { 30 | imageView.image = newValue 31 | if newValue == nil { 32 | imageView.image = #imageLiteral(resourceName: "background") 33 | } 34 | } 35 | get { 36 | return imageView.image 37 | } 38 | } 39 | } 40 | 41 | extension CardView { 42 | fileprivate func configureViews() { 43 | contentView.addSubview(imageView) 44 | contentView.addSubview(photoCamera) 45 | 46 | imageView.contentMode = .scaleAspectFill 47 | let radius: CGFloat = 10 48 | layer.cornerRadius = radius 49 | photoCamera.isHidden = false 50 | } 51 | 52 | fileprivate func configureConstraints() { 53 | contentView.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } 54 | var constraints: [NSLayoutConstraint] = [ 55 | NSLayoutConstraint(item: photoCamera, attribute: .height, relatedBy: 56 | .equal, toItem: photoCamera.superview!, attribute: .height, multiplier: 0.2, constant: 0), 57 | NSLayoutConstraint(item: photoCamera, attribute: .width, relatedBy: 58 | .equal, toItem: photoCamera.superview!, attribute: .width, multiplier: 0.2, constant: 0), 59 | NSLayoutConstraint(item: photoCamera, attribute: .top, relatedBy: 60 | .equal, toItem: photoCamera.superview!, attribute: .top, multiplier: 1, constant: 10), 61 | NSLayoutConstraint(item: photoCamera, attribute: .trailing, relatedBy: 62 | .equal, toItem: photoCamera.superview!, attribute: .trailing, multiplier: 1, constant: -10), 63 | ] 64 | constraints.append(contentsOf: NSLayoutConstraint.filledInSuperview(imageView)) 65 | NSLayoutConstraint.activate(constraints) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /MyCards/MyCards/CardsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardsViewController.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 16/10/16. 6 | // 7 | 8 | import UIKit 9 | 10 | // MARK: - Lifecycle 11 | final class CardsViewController: UIViewController { 12 | 13 | fileprivate let worker: CoreDataWorkerProtocol 14 | fileprivate let loader: ResourceLoading 15 | fileprivate let notificationCenter: NotificationCenterProtocol 16 | fileprivate lazy var cards: [Card] = [] 17 | 18 | fileprivate lazy var emptyScreen: UIImageView = UIImageView(image: #imageLiteral(resourceName: "MyCards")).with { 19 | $0.contentMode = .scaleAspectFit 20 | $0.clipsToBounds = true 21 | $0.alpha = self.cards.isEmpty ? 1.0 : 0.0 22 | } 23 | fileprivate lazy var collectionView: UICollectionView = UICollectionView(frame: 24 | .zero, collectionViewLayout: self.layout).with { 25 | $0.dataSource = self 26 | $0.delegate = self 27 | $0.backgroundColor = .clear 28 | $0.register(CardCell.self) 29 | $0.alpha = 0.0 30 | $0.alwaysBounceVertical = true 31 | } 32 | fileprivate lazy var layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout().with { 33 | $0.scrollDirection = .vertical 34 | let offset: CGFloat = 20 35 | $0.sectionInset = UIEdgeInsets(top: 4.25*offset, left: offset, bottom: offset, right: offset) 36 | $0.minimumInteritemSpacing = offset 37 | $0.minimumLineSpacing = offset 38 | } 39 | 40 | fileprivate var observer: NSObjectProtocol? 41 | 42 | init(worker: CoreDataWorkerProtocol = CoreDataWorker(), 43 | loader: ResourceLoading = NetworkLoader.shared, 44 | notificationCenter: NotificationCenterProtocol = NotificationCenter.default) { 45 | self.worker = worker 46 | self.loader = loader 47 | self.notificationCenter = notificationCenter 48 | super.init(nibName: nil, bundle: nil) 49 | self.title = .MyCards 50 | observer = notificationCenter.observeChanges(for: CardMO.self) { [weak self] in 51 | self?.getCards() 52 | } 53 | } 54 | 55 | required init?(coder aDecoder: NSCoder) { 56 | fatalError("NSCoding not supported") 57 | } 58 | 59 | deinit { 60 | observer.flatMap { notificationCenter.removeObserver($0) } 61 | } 62 | 63 | override func viewDidLoad() { 64 | super.viewDidLoad() 65 | configureViews() 66 | configureConstraints() 67 | loadCards() 68 | getCards() 69 | } 70 | 71 | override func viewDidLayoutSubviews() { 72 | super.viewDidLayoutSubviews() 73 | let width = view.bounds.size.width - 2 * layout.minimumInteritemSpacing 74 | layout.itemSize = CGSize(width: width, height: width / .cardRatio) 75 | } 76 | 77 | func loadCards() { 78 | //NOTE: python -m SimpleHTTPServer 8000 in the directory of `cards.json` 79 | loader.download(from: "/cards.json", parser: CardParser()) { [unowned self] (cards) in 80 | guard let cards = cards as? [Card] else { self.getCards(); return } 81 | self.worker.upsert(entities: cards) { [unowned self] (_) in 82 | self.getCards() 83 | } 84 | } 85 | } 86 | 87 | func getCards() { 88 | worker.get { [weak self] (result: Result<[Card]>) in 89 | guard let sself = self else { return } 90 | switch result { 91 | case .failure: break 92 | case .success(let cards): 93 | sself.cards = cards 94 | sself.reloadData() 95 | } 96 | } 97 | } 98 | 99 | func reloadData() { 100 | if !cards.isEmpty { 101 | hideEmptyScreen() 102 | } else { 103 | showEmptyScreen() 104 | } 105 | collectionView.reloadData() 106 | } 107 | } 108 | 109 | // MARK: - Configuration 110 | extension CardsViewController { 111 | 112 | fileprivate func configureViews() { 113 | view.backgroundColor = . white 114 | view.clipsToBounds = true 115 | 116 | navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: 117 | .add, target: self, action: #selector(addTapped)) 118 | 119 | view.addSubview(emptyScreen) 120 | view.addSubview(collectionView) 121 | } 122 | 123 | fileprivate func configureConstraints() { 124 | view.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } 125 | var constraints: [NSLayoutConstraint] = [] 126 | constraints.append(contentsOf: NSLayoutConstraint.safelyFilledInSuperview(emptyScreen, padding: 44)) 127 | constraints.append(contentsOf: NSLayoutConstraint.safelyFilledInSuperview(collectionView)) 128 | NSLayoutConstraint.activate(constraints) 129 | } 130 | } 131 | 132 | // MARK: - Helpers 133 | extension CardsViewController { 134 | 135 | fileprivate func hideEmptyScreen() { 136 | UIView.animate(withDuration: 0.2) { 137 | self.emptyScreen.alpha = 0.0 138 | self.collectionView.alpha = 1.0 139 | } 140 | } 141 | 142 | fileprivate func showEmptyScreen() { 143 | UIView.animate(withDuration: 0.2) { 144 | self.emptyScreen.alpha = 1.0 145 | self.collectionView.alpha = 0.0 146 | } 147 | } 148 | 149 | @objc fileprivate func addTapped(sender: UIBarButtonItem) { 150 | showDetails(of: Card(name: ""), mode: .create) 151 | } 152 | 153 | fileprivate func showDetails(of card: Card, mode: CardDetailsViewController.Mode = .normal) { 154 | let viewController = CardDetailsViewController(card: card, mode: mode) 155 | let navigationController = UINavigationController(rootViewController: viewController) 156 | present(navigationController, animated: true, completion: nil) 157 | } 158 | 159 | } 160 | 161 | // MARK: - UICollectionViewDataSource 162 | extension CardsViewController: UICollectionViewDataSource { 163 | 164 | func numberOfSections(in collectionView: UICollectionView) -> Int { 165 | return 1 166 | } 167 | 168 | func collectionView(_ collectionView: UICollectionView, 169 | numberOfItemsInSection section: Int) -> Int { 170 | return cards.count 171 | } 172 | 173 | func collectionView(_ collectionView: UICollectionView, 174 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 175 | return collectionView.dequeueReusableCell(of: CardCell.self, for: indexPath) { cell in 176 | guard let card = cards[safe: indexPath.row] else { return } 177 | cell.image = card.front ?? #imageLiteral(resourceName: "background") 178 | cell.indexPath = indexPath 179 | cell.delegate = self 180 | } 181 | } 182 | } 183 | 184 | // MARK: - UICollectionViewDelegate 185 | extension CardsViewController: UICollectionViewDelegate {} 186 | 187 | // MARK: - IndexedCellDelegate 188 | extension CardsViewController: IndexedCellDelegate { 189 | func cellWasTapped(_ cell: IndexedCell) { 190 | guard let indexPath = cell.indexPath, 191 | let card = cards[safe: indexPath.row] else { return } 192 | showDetails(of: card) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /MyCards/MyCards/CloseButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CloseButton.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 23/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | class CloseButton: TappableView { 11 | 12 | let cross: UIView = UIView(frame: .zero).with { 13 | $0.translatesAutoresizingMaskIntoConstraints = false 14 | $0.backgroundColor = .white 15 | } 16 | 17 | override var bounds: CGRect { 18 | didSet { 19 | setCrossShape() 20 | } 21 | } 22 | 23 | override var frame: CGRect { 24 | didSet { 25 | setCrossShape() 26 | } 27 | } 28 | 29 | override init(frame: CGRect) { 30 | super.init(frame: frame) 31 | contentView.addSubview(cross) 32 | NSLayoutConstraint.activate(NSLayoutConstraint.filledInSuperview(cross)) 33 | } 34 | 35 | required init?(coder aDecoder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | override func layoutSubviews() { 40 | super.layoutSubviews() 41 | layer.cornerRadius = frame.height/2 42 | } 43 | 44 | func setCrossShape() { 45 | let rect = bounds 46 | let crossShape = CAShapeLayer() 47 | crossShape.path = rect.crossPath 48 | crossShape.strokeColor = UIColor.white.cgColor 49 | crossShape.lineWidth = 2.0 50 | crossShape.cornerRadius = rect.height/2.0 51 | cross.layer.mask = crossShape 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MyCards/MyCards/CoreDataService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataStack.swift 3 | // Created by swifting.io Team 4 | // 5 | 6 | import CoreData 7 | 8 | protocol CoreDataServiceProtocol: class { 9 | var errorHandler: (Error) -> Void { get set } 10 | var persistentContainer: NSPersistentContainer { get } 11 | var viewContext: NSManagedObjectContext { get } 12 | var backgroundContext: NSManagedObjectContext { get } 13 | func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) 14 | func performForegroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) 15 | } 16 | 17 | final class CoreDataService: CoreDataServiceProtocol { 18 | 19 | static let shared = CoreDataService() 20 | var errorHandler: (Error) -> Void = { _ in } 21 | private let notificationCenter: NotificationCenterProtocol = NotificationCenter.default 22 | lazy var persistentContainer: NSPersistentContainer = { 23 | let container = NSPersistentContainer(name: "DataModel") 24 | container.loadPersistentStores(completionHandler: { [weak self](_, error) in 25 | if let error = error { 26 | NSLog("CoreData error \(error), \(String(describing: error._userInfo))") 27 | self?.errorHandler(error) 28 | } 29 | }) 30 | return container 31 | }() 32 | 33 | lazy var viewContext: NSManagedObjectContext = { 34 | let context: NSManagedObjectContext = self.persistentContainer.viewContext 35 | context.automaticallyMergesChangesFromParent = true 36 | return context 37 | }() 38 | 39 | lazy var backgroundContext: NSManagedObjectContext = { 40 | return self.persistentContainer.newBackgroundContext() 41 | }() 42 | 43 | func performForegroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) { 44 | self.viewContext.perform { 45 | block(self.viewContext) 46 | } 47 | } 48 | 49 | private func startObservingChanges(in context: NSManagedObjectContext) { 50 | notificationCenter.addObserver(self, 51 | selector: #selector(handleSaveNotification(_:)), 52 | name: .NSManagedObjectContextDidSave, 53 | object: context) 54 | } 55 | 56 | private func stopObservingChanges(in context: NSManagedObjectContext) { 57 | notificationCenter.removeObserver(self, 58 | name: .NSManagedObjectContextDidSave, 59 | object: context) 60 | } 61 | 62 | private func objects(from notification: Notification) -> Set { 63 | guard let userInfo = notification.userInfo else { return Set() } 64 | 65 | var allObjects: Set = Set() 66 | 67 | if let updated = userInfo[NSUpdatedObjectsKey] as? Set, !updated.isEmpty { 68 | allObjects.formUnion(updated) 69 | } 70 | 71 | if let deleted = userInfo[NSDeletedObjectsKey] as? Set, !deleted.isEmpty { 72 | allObjects.formUnion(deleted) 73 | } 74 | 75 | if let inserted = userInfo[NSInsertedObjectsKey] as? Set, !inserted.isEmpty { 76 | allObjects.formUnion(inserted) 77 | } 78 | 79 | return allObjects 80 | } 81 | 82 | @objc private func handleSaveNotification(_ notification: Notification) { 83 | 84 | let allObjects: Set = objects(from: notification) 85 | 86 | var notificationNames: Set = Set() 87 | 88 | for object in allObjects { 89 | let name: Notification.Name = Notification.Name.entitiesChanged(type(of: object)) 90 | notificationNames.insert(name) 91 | } 92 | 93 | DispatchQueue.main.async { 94 | for name in notificationNames { 95 | self.notificationCenter.post(name: name, object: self) 96 | } 97 | } 98 | } 99 | func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) { 100 | persistentContainer.performBackgroundTask { [weak self] context in 101 | self?.startObservingChanges(in: context) 102 | block(context) 103 | self?.stopObservingChanges(in: context) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /MyCards/MyCards/CoreDataWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataWorker.swift 3 | // Created by swifting.io Team 4 | // 5 | 6 | import Foundation 7 | import CoreData 8 | 9 | protocol CoreDataWorkerProtocol { 10 | func get 11 | (with predicate: NSPredicate?, 12 | sortDescriptors: [NSSortDescriptor]?, 13 | fetchLimit: Int?, 14 | completion: @escaping (Result<[Entity]>) -> Void) 15 | func upsert 16 | (entities: [Entity], 17 | completion: @escaping (Error?) -> Void) 18 | func remove 19 | (entities: [Entity], 20 | completion: @escaping (Error?) -> Void) 21 | } 22 | 23 | extension CoreDataWorkerProtocol { 24 | func get 25 | (with predicate: NSPredicate? = nil, 26 | sortDescriptors: [NSSortDescriptor]? = nil, 27 | fetchLimit: Int? = nil, 28 | completion: @escaping (Result<[Entity]>) -> Void) { 29 | get(with: predicate, 30 | sortDescriptors: sortDescriptors, 31 | fetchLimit: fetchLimit, 32 | completion: completion) 33 | } 34 | } 35 | 36 | enum CoreDataWorkerError: Error { 37 | case cannotFetch(String) 38 | case cannotSave(Error) 39 | } 40 | 41 | class CoreDataWorker: CoreDataWorkerProtocol { 42 | let coreData: CoreDataServiceProtocol 43 | 44 | init(coreData: CoreDataServiceProtocol = CoreDataService.shared) { 45 | self.coreData = coreData 46 | } 47 | 48 | func get 49 | (with predicate: NSPredicate?, 50 | sortDescriptors: [NSSortDescriptor]?, 51 | fetchLimit: Int?, 52 | completion: @escaping (Result<[Entity]>) -> Void) { 53 | coreData.performForegroundTask { context in 54 | do { 55 | let fetchRequest = Entity.ManagedObject.fetchRequest() 56 | fetchRequest.predicate = predicate 57 | fetchRequest.sortDescriptors = sortDescriptors 58 | if let fetchLimit = fetchLimit { 59 | fetchRequest.fetchLimit = fetchLimit 60 | } 61 | let results = try context.fetch(fetchRequest) as? [Entity.ManagedObject] 62 | let items: [Entity] = results?.flatMap { $0.toEntity() as? Entity } ?? [] 63 | completion(.success(items)) 64 | } catch { 65 | let fetchError = CoreDataWorkerError.cannotFetch("Cannot fetch error: \(error))") 66 | completion(.failure(fetchError)) 67 | } 68 | } 69 | } 70 | 71 | func upsert 72 | (entities: [Entity], 73 | completion: @escaping (Error?) -> Void) { 74 | 75 | coreData.performBackgroundTask { context in 76 | _ = entities.flatMap({ (entity) -> Entity.ManagedObject? in 77 | return entity.toManagedObject(in: context) 78 | }) 79 | do { 80 | try context.save() 81 | completion(nil) 82 | } catch { 83 | completion(CoreDataWorkerError.cannotSave(error)) 84 | } 85 | } 86 | } 87 | 88 | func remove 89 | (entities: [Entity], completion: @escaping (Error?) -> Void) { 90 | coreData.performBackgroundTask { (context) in 91 | for entity in entities { 92 | if let managedEntity = entity.toManagedObject(in: context) { 93 | context.delete(managedEntity) 94 | } 95 | } 96 | do { 97 | try context.save() 98 | completion(nil) 99 | } catch { 100 | completion(CoreDataWorkerError.cannotSave(error)) 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /MyCards/MyCards/CropViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CropViewConttroller.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 09/12/16. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol CropViewControllerDelegate: class { 11 | func cropViewController(_ viewController: CropViewController, didCropPhoto photo: UIImage, for side: Card.Side) 12 | } 13 | 14 | class CropViewController: LightStatusBarViewController { 15 | 16 | weak var delegate: CropViewControllerDelegate? 17 | fileprivate let side: Card.Side 18 | 19 | // MARK: Views 20 | fileprivate lazy var previewView: PreviewOutline = PreviewOutline.constrained().with { 21 | $0.outline.clipsToBounds = true 22 | $0.captureButton.transform = .rotateRight 23 | $0.closeButton.transform = .rotateRight 24 | $0.closeButton.tapped = { [unowned self] in 25 | self.dismiss() 26 | } 27 | $0.captureButton.tapped = { [unowned self] in 28 | self.process().flatMap { 29 | self.delegate?.cropViewController(self, didCropPhoto: $0, for: self.side) 30 | } 31 | self.dismiss() 32 | } 33 | } 34 | fileprivate lazy var backgroundView: UIImageView = UIImageView.constrained().with { 35 | $0.contentMode = .scaleAspectFill 36 | } 37 | fileprivate let backgroundEffectView: UIVisualEffectView = UIVisualEffectView(effect: 38 | UIBlurEffect(style: .dark)).with { $0.translatesAutoresizingMaskIntoConstraints = false } 39 | fileprivate lazy var imageView: UIImageView = UIImageView(frame: .zero).with { 40 | $0.contentMode = .center 41 | } 42 | fileprivate lazy var scrollView: UIScrollView = UIScrollView.constrained().with { 43 | $0.delegate = self 44 | $0.maximumZoomScale = 2 45 | $0.showsVerticalScrollIndicator = false 46 | $0.showsHorizontalScrollIndicator = false 47 | $0.isScrollEnabled = true 48 | } 49 | 50 | init(image: UIImage, side: Card.Side) { 51 | self.side = side 52 | super.init(nibName: nil, bundle: nil) 53 | imageView.image = UIImage(cgImage: image.cgImage!, scale: 1, orientation: .right) 54 | backgroundView.image = imageView.image 55 | modalTransitionStyle = .crossDissolve 56 | } 57 | 58 | required init?(coder aDecoder: NSCoder) { 59 | fatalError("init(coder:) has not been implemented") 60 | } 61 | 62 | // MARK: Lifecycle 63 | override func viewDidLoad() { 64 | super.viewDidLoad() 65 | configureViews() 66 | configureConstraints() 67 | } 68 | 69 | private func configureViews() { 70 | view.clipsToBounds = true 71 | view.addSubview(backgroundView) 72 | view.addSubview(backgroundEffectView) 73 | view.addSubview(previewView) 74 | previewView.outline.addSubview(scrollView) 75 | scrollView.addSubview(imageView) 76 | } 77 | 78 | fileprivate func configureConstraints() { 79 | var constraints: [NSLayoutConstraint] = [] 80 | constraints.append(contentsOf: NSLayoutConstraint.filledInSuperview(previewView)) 81 | constraints.append(contentsOf: NSLayoutConstraint.filledInSuperview(scrollView)) 82 | constraints.append(contentsOf: NSLayoutConstraint.filledInSuperview(backgroundView)) 83 | constraints.append(contentsOf: NSLayoutConstraint.filledInSuperview(backgroundEffectView)) 84 | NSLayoutConstraint.activate(constraints) 85 | previewView.layoutIfNeeded() 86 | } 87 | 88 | override func viewDidLayoutSubviews() { 89 | super.viewDidLayoutSubviews() 90 | let w: CGFloat = previewView.outline.frame.width / CGFloat(imageView.image!.cgImage!.width) 91 | let h: CGFloat = previewView.outline.frame.height / CGFloat(imageView.image!.cgImage!.height) 92 | let scale = max(w, h) 93 | imageView.sizeToFit() 94 | scrollView.contentSize = imageView.bounds.size 95 | scrollView.contentOffset = CGPoint(x: imageView.bounds.width/2, y: imageView.bounds.height/2) 96 | scrollView.minimumZoomScale = scale 97 | scrollView.zoomScale = scrollView.minimumZoomScale 98 | } 99 | 100 | fileprivate func process() -> UIImage? { 101 | guard let image = imageView.image?.cgImage else { return nil } 102 | 103 | let scale = scrollView.zoomScale 104 | let x = scrollView.bounds.origin.y 105 | let y = -scrollView.bounds.origin.x - scrollView.bounds.width + scrollView.contentSize.width 106 | let height = scrollView.bounds.height 107 | let width = scrollView.bounds.width 108 | let rect = CGRect(x: x / scale, 109 | y: y / scale, 110 | width: height / scale, 111 | height: width / scale) 112 | 113 | guard let cropped = image.cropping(to: rect) else { return nil } 114 | 115 | let photo = UIImage(cgImage: cropped, scale: 1, orientation: .up) 116 | return photo 117 | } 118 | } 119 | 120 | extension CropViewController: UIScrollViewDelegate { 121 | func viewForZooming(in scrollView: UIScrollView) -> UIView? { 122 | return imageView 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /MyCards/MyCards/Data+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 09/04/17. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Data { 11 | var JSONObject: Any? { 12 | return try? JSONSerialization.jsonObject(with: self) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MyCards/MyCards/DataModel.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | DataModel.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /MyCards/MyCards/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /MyCards/MyCards/ImagePickerController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerController.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 03/02/17. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ImagePickerController: UIImagePickerController { 11 | var side: Card.Side? 12 | } 13 | -------------------------------------------------------------------------------- /MyCards/MyCards/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSAppTransportSecurity 24 | 25 | NSAllowsArbitraryLoads 26 | 27 | 28 | NSCameraUsageDescription 29 | $(PRODUCT_NAME) uses your Camera will be used to capture a photo of a loyalty card. 30 | NSPhotoLibraryUsageDescription 31 | $(PRODUCT_NAME) uses your Photo Library will be used to select a photo of a loyalty card. 32 | UILaunchStoryboardName 33 | LaunchScreen 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /MyCards/MyCards/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /MyCards/MyCards/LightStatusBarViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LightStatusBarViewController.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 05/12/16. 6 | // 7 | 8 | import UIKit 9 | 10 | class LightStatusBarViewController: UIViewController { 11 | 12 | override var shouldAutorotate: Bool { return false } 13 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask { 14 | return .landscapeRight 15 | } 16 | override var preferredStatusBarStyle: UIStatusBarStyle { 17 | return .lightContent 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /MyCards/MyCards/ManagedObjectConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManagedObjectConvertible.swift 3 | // Created by swifting.io Team 4 | // 5 | 6 | import CoreData 7 | 8 | protocol ManagedObjectConvertible { 9 | associatedtype ManagedObject: NSManagedObject, ManagedObjectProtocol 10 | func toManagedObject(in context: NSManagedObjectContext) -> ManagedObject? 11 | } 12 | -------------------------------------------------------------------------------- /MyCards/MyCards/ManagedObjectProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManagedObjectProtocol.swift 3 | // Created by swifting.io Team 4 | // 5 | 6 | import CoreData 7 | 8 | protocol ManagedObjectProtocol { 9 | associatedtype Entity 10 | func toEntity() -> Entity? 11 | } 12 | 13 | extension ManagedObjectProtocol where Self: NSManagedObject { 14 | 15 | static func getOrCreateSingle(with id: String, from context: NSManagedObjectContext) -> Self { 16 | let result = single(with: id, from: context) ?? insertNew(in: context) 17 | result.setValue(id, forKey: "identifier") 18 | return result 19 | } 20 | 21 | static func single(from context: NSManagedObjectContext, with predicate: NSPredicate?, 22 | sortDescriptors: [NSSortDescriptor]?) -> Self? { 23 | return fetch(from: context, with: predicate, 24 | sortDescriptors: sortDescriptors, fetchLimit: 1)?.first 25 | } 26 | 27 | static func single(with id: String, from context: NSManagedObjectContext) -> Self? { 28 | let predicate = NSPredicate(format: "identifier == %@", id) 29 | return single(from: context, with: predicate, sortDescriptors: nil) 30 | } 31 | 32 | static func insertNew(in context: NSManagedObjectContext) -> Self { 33 | return Self(context: context) 34 | } 35 | 36 | static func fetch(from context: NSManagedObjectContext, with predicate: NSPredicate?, 37 | sortDescriptors: [NSSortDescriptor]?, fetchLimit: Int?) -> [Self]? { 38 | 39 | let fetchRequest = Self.fetchRequest() 40 | fetchRequest.sortDescriptors = sortDescriptors 41 | fetchRequest.predicate = predicate 42 | fetchRequest.returnsObjectsAsFaults = false 43 | 44 | if let fetchLimit = fetchLimit { 45 | fetchRequest.fetchLimit = fetchLimit 46 | } 47 | 48 | var result: [Self]? 49 | context.performAndWait { () -> Void in 50 | do { 51 | result = try context.fetch(fetchRequest) as? [Self] 52 | } catch { 53 | result = nil 54 | //Report Error 55 | print("CoreData fetch error \(error)") 56 | } 57 | } 58 | return result 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /MyCards/MyCards/NSLayoutConstraint+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutConstraint+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 11/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | extension NSLayoutConstraint { 11 | 12 | class func centeredInSuperview(_ view: UIView) -> [NSLayoutConstraint] { 13 | return [ 14 | centeredInSuperview(view, with: .centerX), 15 | centeredInSuperview(view, with: .centerY) 16 | ] 17 | } 18 | 19 | class func centeredInSuperview(_ view: UIView, with attribute: NSLayoutAttribute) -> NSLayoutConstraint { 20 | return NSLayoutConstraint(item: view, attribute: attribute, relatedBy: .equal, toItem: 21 | view.superview!, attribute: attribute, multiplier: 1, constant: 0) 22 | } 23 | 24 | class func filledInSuperview(_ view: UIView, padding: CGFloat = 0) -> [NSLayoutConstraint] { 25 | guard let superview = view.superview else { return [] } 26 | return [ 27 | view.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: padding), 28 | view.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: -padding), 29 | view.topAnchor.constraint(equalTo: superview.topAnchor, constant: padding), 30 | view.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: -padding) 31 | ] 32 | 33 | } 34 | 35 | class func safelyFilledInSuperview(_ view: UIView, padding: CGFloat = 0) -> [NSLayoutConstraint] { 36 | guard let superview = view.superview else { return [] } 37 | return [ 38 | view.leadingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.leadingAnchor, constant: padding), 39 | view.trailingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.trailingAnchor, constant: -padding), 40 | view.topAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.topAnchor, constant: padding), 41 | view.bottomAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.bottomAnchor, constant: -padding) 42 | ] 43 | } 44 | 45 | class func height2WidthCardRatio(for view: UIView) -> NSLayoutConstraint { 46 | return NSLayoutConstraint(item: view, attribute: 47 | .height, relatedBy: .equal, toItem: view, attribute: 48 | .width, multiplier: .cardRatio, constant: 0) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MyCards/MyCards/NSLayoutFormatOptions+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutFormatOptions+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 16/10/16. 6 | // 7 | 8 | import UIKit 9 | 10 | extension NSLayoutFormatOptions { 11 | static let none: NSLayoutFormatOptions = [] 12 | static let center: NSLayoutFormatOptions = [.alignAllCenterX, .alignAllCenterY] 13 | } 14 | -------------------------------------------------------------------------------- /MyCards/MyCards/NSObject+Builder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Protocols.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 11/11/16. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Builder {} 11 | extension Builder { 12 | public func with(configure: (inout Self) -> Void) -> Self { 13 | var this = self 14 | configure(&this) 15 | return this 16 | } 17 | } 18 | 19 | extension NSObject: Builder {} 20 | -------------------------------------------------------------------------------- /MyCards/MyCards/NetworLoader+Shared.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworLoader+Shared.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 09/04/17. 6 | // 7 | 8 | import Foundation 9 | 10 | extension NetworkLoader { 11 | static let shared: NetworkLoader = NetworkLoader(URL(string: "http://localhost:8000")!) 12 | } 13 | -------------------------------------------------------------------------------- /MyCards/MyCards/NetworkLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkLoader.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 11/11/16. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol DataDownloading { 11 | func download(from endpoint: String, callback: @escaping (Data?) -> Void) 12 | } 13 | 14 | protocol Parser { 15 | func parse(_ json: Data, with decoder: JSONDecoder) throws -> Any 16 | } 17 | 18 | protocol ParsedDataDownloading { 19 | func download(from endpoint: String, parser: Parser, callback: @escaping (Any?) -> Void) 20 | } 21 | 22 | protocol JSONDataConverting { 23 | func json(from object: Any) -> Data? 24 | } 25 | 26 | protocol DataUploading { 27 | func upload(data: Data, to endpoint: String, callback: @escaping (Any?) -> Void) 28 | } 29 | 30 | protocol JSONConvertibleDataUploading { 31 | func upload(object: Any, to endpoint: String, parser: JSONDataConverting, callback: @escaping (Any?) -> Void) 32 | } 33 | 34 | typealias ResourceLoading = ParsedDataDownloading & JSONConvertibleDataUploading 35 | 36 | final class NetworkLoader { 37 | 38 | let webserviceURL: URL 39 | let session: URLSession 40 | 41 | init(_ webserviceURL: URL, session: URLSession = URLSession(configuration: .default)) { 42 | self.webserviceURL = webserviceURL 43 | self.session = session 44 | } 45 | 46 | func url(with endpoint: String) -> URL { 47 | return webserviceURL.appendingPathComponent(endpoint) 48 | } 49 | } 50 | 51 | extension NetworkLoader: DataDownloading { 52 | func download(from endpoint: String, callback: @escaping (Data?) -> Void) { 53 | let url = self.url(with: endpoint) 54 | let task = session.dataTask(with: url) { (data, _, error) -> Void in 55 | guard error == nil, 56 | let data = data 57 | else { print(String(describing: error)); callback(nil); return } 58 | callback(data) 59 | } 60 | task.resume() 61 | } 62 | 63 | } 64 | 65 | extension NetworkLoader: ParsedDataDownloading { 66 | func download(from endpoint: String, parser: Parser, callback: @escaping (Any?) -> Void) { 67 | download(from: endpoint) { json in 68 | guard let data = json else { callback(nil); return; } 69 | let decoder = JSONDecoder() 70 | let parsed = try? parser.parse(data, with: decoder) 71 | callback(parsed) 72 | } 73 | } 74 | } 75 | 76 | extension NetworkLoader: DataUploading { 77 | func upload(data: Data, to endpoint: String, callback: @escaping (Any?) -> Void) { 78 | let url = self.url(with: endpoint) 79 | let request = URLRequest(url: url) 80 | let task = session.uploadTask(with: request, from: nil) { (data, _, error) in 81 | guard error == nil, 82 | let data = data 83 | else { print(String(describing: error)); callback(nil); return } 84 | callback(data.JSONObject) 85 | } 86 | task.resume() 87 | } 88 | } 89 | 90 | extension NetworkLoader: JSONConvertibleDataUploading { 91 | func upload(object: Any, to endpoint: String, parser: JSONDataConverting, callback: @escaping (Any?) -> Void) { 92 | guard let data = parser.json(from: object) else { callback(nil); return } 93 | upload(data: data, to: endpoint) { (data) in 94 | callback(data) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /MyCards/MyCards/NotificationCenter+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationCenter+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 08/01/17. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | protocol NotificationCenterProtocol { 12 | func addObserver(_: Any, selector: Selector, name: NSNotification.Name?, 13 | object: Any?) 14 | 15 | func addObserver(forName: NSNotification.Name?, object: Any?, 16 | queue: OperationQueue?, 17 | using: @escaping (Notification) -> Void) -> NSObjectProtocol 18 | func observeChanges(for type: Entity.Type, 19 | block: @escaping () -> Void) -> NSObjectProtocol 20 | where Entity: NSManagedObject 21 | 22 | func removeObserver(_: Any) 23 | func removeObserver(_: Any, name: NSNotification.Name?, 24 | object: Any?) 25 | 26 | func post(_: Notification) 27 | func post(name: NSNotification.Name, object: Any?) 28 | func post(name: NSNotification.Name, object: Any?, 29 | userInfo: [AnyHashable: Any]?) 30 | } 31 | 32 | extension Notification.Name { 33 | 34 | static func entitiesChanged(_ type: Entity.Type) -> Notification.Name 35 | where Entity: NSManagedObject { 36 | 37 | return Notification.Name("entitiesChanged" + String(describing: type)) 38 | } 39 | } 40 | 41 | extension NotificationCenter: NotificationCenterProtocol { 42 | 43 | func observeChanges(for type: Entity.Type, 44 | block: @escaping () -> Void) -> NSObjectProtocol 45 | where Entity: NSManagedObject { 46 | return addObserver(forName: .entitiesChanged(type), 47 | object: nil, 48 | queue: .main) { (_) in 49 | block() 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MyCards/MyCards/PhotoCamera.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoButton.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 13/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | class PhotoCamera: UIView { 11 | 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | backgroundColor = .clear 15 | } 16 | 17 | required init?(coder aDecoder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | override func draw(_ rect: CGRect) { 22 | // Main Shape 23 | let x = rect.origin.x 24 | let y = rect.origin.y 25 | let width = rect.size.width 26 | let height = rect.size.height 27 | let radius: CGFloat = height / 10.0 28 | let main = UIBezierPath(roundedRect: CGRect(x: x, y: y, width: width, height: height), cornerRadius: radius) 29 | UIColor.lightGray.setFill() 30 | main.fill() 31 | 32 | // Inner Shape 33 | let innerX: CGFloat = x 34 | let innerY: CGFloat = height * 5 / 30.0 35 | let innerWidth: CGFloat = width 36 | let innerHeight: CGFloat = height * 2 / 3.0 37 | let inner = UIBezierPath(rect: CGRect(x: innerX, y: innerY, width: innerWidth, height: innerHeight)) 38 | UIColor.gray.setFill() 39 | inner.fill() 40 | 41 | // Viewfinder 42 | let viewFinderX: CGFloat = width / 16.0 43 | let viewFinderY: CGFloat = height / 12.0 44 | let viewFinderWidth: CGFloat = width / 6.0 45 | let viewFinderHeight: CGFloat = height / 6.0 46 | let viewFinderRadius: CGFloat = width / 10.0 47 | let viewfinder = UIBezierPath(roundedRect: CGRect(x: viewFinderX, y: viewFinderY, width: 48 | viewFinderWidth, height: viewFinderHeight), cornerRadius: viewFinderRadius) 49 | UIColor.lightBlue.setFill() 50 | viewfinder.fill() 51 | UIColor.darkerBlue.setStroke() 52 | viewfinder.lineWidth = 1 53 | viewfinder.stroke() 54 | 55 | // Lense cover 56 | let coverSide: CGFloat = height / (30.0 / 22.0) 57 | let coverX: CGFloat = width / 2.0 - coverSide / 2.0 58 | let coverY: CGFloat = height / 2.0 - coverSide / 2.0 59 | let cover = UIBezierPath(ovalIn: CGRect(x: coverX, y: coverY, width: coverSide, height: coverSide)) 60 | UIColor.darkerBlue.setFill() 61 | cover.fill() 62 | 63 | // Lense 64 | let lenseSide: CGFloat = coverSide * 0.9 65 | let lenseX: CGFloat = width / 2.0 - lenseSide / 2.0 66 | let lenseY: CGFloat = height / 2.0 - lenseSide / 2.0 67 | let lense = UIBezierPath(ovalIn: CGRect(x: lenseX, y: lenseY, width: lenseSide, height: lenseSide)) 68 | UIColor.lightBlue.setFill() 69 | lense.fill() 70 | alpha = 1.0 71 | } 72 | 73 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 74 | alpha = 0.0 75 | setNeedsDisplay() 76 | } 77 | 78 | override var intrinsicContentSize: CGSize { 79 | return CGSize(width: 40, height: 30) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /MyCards/MyCards/PhotoCameraButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 23/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | class PhotoCameraButton: TappableView { 11 | 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | let camera = PhotoCamera(frame: .zero) 15 | camera.translatesAutoresizingMaskIntoConstraints = false 16 | contentView.addSubview(camera) 17 | NSLayoutConstraint.activate(NSLayoutConstraint.safelyFilledInSuperview(camera)) 18 | } 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MyCards/MyCards/PhotoCaptureViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoCaptureViewController.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 22/11/16. 6 | // 7 | 8 | import UIKit 9 | import AVFoundation 10 | 11 | protocol PhotoCaptureViewControllerDelegate: class { 12 | func photoCaptureViewController(_ viewController: PhotoCaptureViewController, didTakePhoto 13 | photo: UIImage, for side: Card.Side) 14 | } 15 | 16 | final class PhotoCaptureViewController: LightStatusBarViewController { 17 | 18 | weak var delegate: PhotoCaptureViewControllerDelegate? 19 | fileprivate let side: Card.Side 20 | fileprivate lazy var previewView: PreviewView = PreviewView().with { 21 | $0.session = self.session 22 | $0.captureButton.tapped = { [unowned self] in self.takePhoto() } 23 | $0.closeButton.tapped = { [unowned self] in self.dismiss() } 24 | $0.controlsAlpha = 0.0 25 | //display preview items in landscape right 26 | $0.captureButton.transform = .rotateRight 27 | $0.closeButton.transform = .rotateRight 28 | } 29 | 30 | // MARK: AVFoundation components 31 | fileprivate let output = AVCapturePhotoOutput().with { 32 | $0.isHighResolutionCaptureEnabled = true 33 | $0.isLivePhotoCaptureEnabled = false 34 | } 35 | fileprivate let session = AVCaptureSession() 36 | fileprivate let queue = DispatchQueue(label: "AV Session Queue", attributes: [], target: nil) 37 | fileprivate var authorizationStatus: AVAuthorizationStatus { 38 | return AVCaptureDevice.authorizationStatus(for: AVMediaType.video) 39 | } 40 | 41 | init(side: Card.Side) { 42 | self.side = side 43 | super.init(nibName: nil, bundle: nil) 44 | } 45 | 46 | required init?(coder aDecoder: NSCoder) { 47 | fatalError("init(coder:) has not been implemented") 48 | } 49 | 50 | // MARK: Lifecycle 51 | override func viewDidLoad() { 52 | super.viewDidLoad() 53 | 54 | configureViews() 55 | configureConstraints() 56 | requestAuthorizationIfNeeded() 57 | configureSession() 58 | } 59 | 60 | override func viewWillAppear(_ animated: Bool) { 61 | super.viewWillAppear(animated) 62 | startSession() 63 | } 64 | 65 | override func viewDidAppear(_ animated: Bool) { 66 | super.viewDidAppear(animated) 67 | UIView.animate(withDuration: 0.25) { 68 | self.previewView.controlsAlpha = 1.0 69 | } 70 | } 71 | 72 | override func viewWillDisappear(_ animated: Bool) { 73 | stopSession() 74 | super.viewWillDisappear(animated) 75 | } 76 | } 77 | 78 | extension PhotoCaptureViewController { 79 | 80 | fileprivate func configureViews() { 81 | view.addSubview(previewView) 82 | view.backgroundColor = .black 83 | } 84 | 85 | fileprivate func configureConstraints() { 86 | 87 | view.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } 88 | 89 | let constraints: [NSLayoutConstraint] = NSLayoutConstraint.filledInSuperview(previewView) 90 | 91 | NSLayoutConstraint.activate(constraints) 92 | } 93 | 94 | fileprivate func requestAuthorizationIfNeeded() { 95 | guard .notDetermined == authorizationStatus else { return } 96 | queue.suspend() 97 | AVCaptureDevice.requestAccess(for: AVMediaType.video) { [unowned self] granted in 98 | guard granted else { return } 99 | self.queue.resume() 100 | } 101 | } 102 | 103 | fileprivate enum Error: Swift.Error { 104 | case noCamera 105 | case cannotAddInput 106 | } 107 | 108 | fileprivate func configureSession() { 109 | queue.async { 110 | guard .authorized == self.authorizationStatus else { return } 111 | guard let camera: AVCaptureDevice = AVCaptureDevice.backVideoCamera else { return } 112 | 113 | defer { self.session.commitConfiguration() } 114 | 115 | self.session.beginConfiguration() 116 | self.session.sessionPreset = AVCaptureSession.Preset.photo 117 | 118 | do { 119 | let input = try AVCaptureDeviceInput(device: camera) 120 | guard self.session.canAddInput(input) else { return } 121 | self.session.addInput(input) 122 | } catch { return } 123 | 124 | guard self.session.canAddOutput(self.output) else { return } 125 | self.session.addOutput(self.output) 126 | } 127 | } 128 | 129 | fileprivate func takePhoto() { 130 | queue.async { [unowned self] in 131 | let photoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg]) 132 | photoSettings.flashMode = .auto 133 | photoSettings.isHighResolutionPhotoEnabled = true 134 | self.output.capturePhoto(with: photoSettings, delegate: self) 135 | } 136 | } 137 | 138 | fileprivate func startSession() { 139 | queue.async { 140 | guard self.authorizationStatus == .authorized else { return } 141 | guard !self.session.isRunning else { return } 142 | self.session.startRunning() 143 | } 144 | } 145 | 146 | fileprivate func stopSession() { 147 | queue.async { 148 | guard self.authorizationStatus == .authorized else { return } 149 | guard self.session.isRunning else { return } 150 | self.session.stopRunning() 151 | } 152 | } 153 | } 154 | 155 | extension PhotoCaptureViewController: AVCapturePhotoCaptureDelegate { 156 | 157 | // codebeat:disable[ARITY] 158 | public func photoOutput(_ output: AVCapturePhotoOutput, 159 | didFinishProcessingPhoto photo: AVCapturePhoto, 160 | error: Swift.Error?) { 161 | guard error == nil, 162 | let data = photo.fileDataRepresentation(), 163 | let photo = process(data) 164 | else { print("Error capturing photo: \(String(describing: error))"); return } 165 | 166 | delegate?.photoCaptureViewController(self, didTakePhoto: photo, for: side) 167 | } 168 | // codebeat:enable[ARITY] 169 | 170 | private func process(_ data: Data) -> UIImage? { 171 | guard let image = UIImage(data: data)?.cgImage else { return nil } 172 | let outline = previewView.outline.frame 173 | let outputRect = previewView.videoPreviewLayer.metadataOutputRectConverted(fromLayerRect: outline) 174 | return cropp(image, to: outline, with: outputRect) 175 | } 176 | 177 | func cropp(_ image: CGImage, to outline: CGRect, with metadataOutputRect: CGRect) -> UIImage? { 178 | 179 | let originalSize: CGSize = CGSize(width: image.width, height: image.height) 180 | 181 | //NOTE: Scale using corresponding CGRect from AVCaptureVideoPreviewLayer 182 | let scaledOutline: CGSize = CGSize(width: 183 | originalSize.width * metadataOutputRect.width, 184 | height: CGFloat(image.height) * metadataOutputRect.height) 185 | 186 | //NOTE: Calculate card outline offset (x,y) 187 | let x: CGFloat = originalSize.width * metadataOutputRect.origin.x 188 | let y: CGFloat = originalSize.height * metadataOutputRect.origin.y 189 | 190 | let rect = CGRect(x: x, 191 | y: y, 192 | width: scaledOutline.width, 193 | height: scaledOutline.height) 194 | 195 | guard let cropped = image.cropping(to: rect) else { return nil } 196 | 197 | let photo = UIImage(cgImage: cropped, scale: 1, orientation: .up) 198 | return photo 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /MyCards/MyCards/PreviewOutline.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewOutline.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 05/02/17. 6 | // 7 | 8 | import UIKit 9 | 10 | class PreviewOutline: UIView { 11 | 12 | let captureButton = PhotoCameraButton.constrained() 13 | let closeButton = CloseButton.constrained() 14 | let outline: UIView = UIView.constrained().with { view in 15 | view.layer.cornerRadius = 10 16 | view.layer.borderWidth = 2 17 | view.layer.borderColor = UIColor.white.cgColor 18 | } 19 | var controlsAlpha: CGFloat = 1.0 { 20 | didSet { 21 | captureButton.alpha = controlsAlpha 22 | closeButton.alpha = controlsAlpha 23 | outline.alpha = controlsAlpha 24 | } 25 | } 26 | 27 | override init(frame: CGRect) { 28 | super.init(frame: frame) 29 | addSubview(outline) 30 | addSubview(captureButton) 31 | addSubview(closeButton) 32 | configureConstraints() 33 | } 34 | 35 | required init?(coder aDecoder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | private func configureConstraints() { 40 | var constraints: [NSLayoutConstraint] = NSLayoutConstraint.centeredInSuperview(outline) 41 | constraints.append(NSLayoutConstraint.centeredInSuperview(captureButton, with: .centerX)) 42 | constraints.append(NSLayoutConstraint.height2WidthCardRatio(for: outline)) 43 | constraints.append(captureButton.heightAnchor.constraint(equalToConstant: 54)) 44 | constraints.append(captureButton.widthAnchor.constraint(equalToConstant: 72)) 45 | constraints.append(closeButton.heightAnchor.constraint(equalToConstant: 40)) 46 | constraints.append(closeButton.widthAnchor.constraint(equalToConstant: 40)) 47 | constraints.append(captureButton.bottomAnchor.constraint(equalTo: 48 | safeAreaLayoutGuide.bottomAnchor, constant: -20)) 49 | constraints.append(closeButton.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20)) 50 | constraints.append(closeButton.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20)) 51 | constraints.append(outline.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: .cardOffsetY)) 52 | constraints.append(outline.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -.cardOffsetY)) 53 | NSLayoutConstraint.activate(constraints) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MyCards/MyCards/PreviewView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewView.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 23/11/16. 6 | // 7 | 8 | import UIKit 9 | import AVFoundation 10 | 11 | final class PreviewView: PreviewOutline { 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | videoPreviewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill 16 | } 17 | 18 | required init?(coder aDecoder: NSCoder) { 19 | fatalError("init(coder:) has not been implemented") 20 | } 21 | 22 | var videoPreviewLayer: AVCaptureVideoPreviewLayer { 23 | //swiftlint:disable force_cast 24 | return layer as! AVCaptureVideoPreviewLayer 25 | //swiftlint:enable force_cast 26 | } 27 | 28 | var session: AVCaptureSession? { 29 | get { 30 | return videoPreviewLayer.session 31 | } 32 | set { 33 | videoPreviewLayer.session = newValue 34 | } 35 | } 36 | 37 | override class var layerClass: AnyClass { 38 | return AVCaptureVideoPreviewLayer.self 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /MyCards/MyCards/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 16/10/16. 6 | // 7 | 8 | import UIKit 9 | 10 | enum ResultError: Error { 11 | case noValue 12 | } 13 | 14 | enum Result { 15 | case success(T) 16 | case failure(Error) 17 | } 18 | 19 | func == (lhs: Result, rhs: Result) -> Bool { 20 | switch (lhs, rhs) { 21 | case (.success(let lhss), .success(let rhss)): 22 | return lhss == rhss 23 | 24 | case (.failure, .failure): 25 | return true 26 | 27 | default: 28 | return false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MyCards/MyCards/String+Localized.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Localized.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 12/11/16. 6 | // 7 | 8 | import Foundation 9 | 10 | func NSLocalizedString(_ key: String) -> String { 11 | return NSLocalizedString(key, comment: "") 12 | } 13 | 14 | extension String { 15 | static let MyCards = NSLocalizedString("My Cards", comment: "") 16 | static let AddNewCard = NSLocalizedString("Add new card", comment: "") 17 | static let CardDetails = NSLocalizedString("Card Details", comment: "") 18 | static let EnterCardName = NSLocalizedString("Enter card name", comment: "") 19 | static let Set = NSLocalizedString("Set", comment: "") 20 | static let frontPhoto = NSLocalizedString("front photo", comment: "") 21 | static let backPhoto = NSLocalizedString("back photo", comment: "") 22 | static let Camera = NSLocalizedString("Camera", comment: "") 23 | static let PhotoLibrary = NSLocalizedString("Photo Library", comment: "") 24 | static let SavedPhotosAlbum = NSLocalizedString("Saved Photos Album", comment: "") 25 | static let Cancel = NSLocalizedString("Cancel", comment: "") 26 | static let OK = NSLocalizedString("OK", comment: "") 27 | static let SelectCardPhoto = NSLocalizedString("Select Card Photo", comment: "") 28 | static let NoName = NSLocalizedString("No Name", comment: "") 29 | } 30 | -------------------------------------------------------------------------------- /MyCards/MyCards/TappableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TappableView.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 13/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | struct AnimationState { 11 | var performed: Bool = false 12 | var restoration: CGAffineTransform = .identity 13 | } 14 | 15 | class TappableView: UIView { 16 | 17 | fileprivate var touchingDownInside: Bool = false 18 | fileprivate let dimmedView: UIView 19 | fileprivate var animationState = AnimationState() 20 | let contentView: UIView 21 | var tapped: (() -> Void)? 22 | 23 | convenience init() { 24 | self.init(frame: .zero) 25 | } 26 | 27 | override init(frame: CGRect) { 28 | contentView = UIView(frame: .zero) 29 | dimmedView = UIView(frame: .zero) 30 | super.init(frame: frame) 31 | configureViews() 32 | configureConstraints() 33 | } 34 | 35 | required init?(coder aDecoder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | override open func touchesBegan(_ touches: Set, with event: UIEvent?) { 40 | super.touchesBegan(touches, with: event) 41 | touchingDownInside = boundsContain(touches) 42 | if touchingDownInside { 43 | animateTap() 44 | } 45 | } 46 | 47 | override open func touchesMoved(_ touches: Set, with event: UIEvent?) { 48 | super.touchesMoved(touches, with: event) 49 | if !boundsContain(touches) { 50 | touchingDownInside = false 51 | undoTapAnimation() 52 | } 53 | } 54 | 55 | override open func touchesEnded(_ touches: Set, with event: UIEvent?) { 56 | super.touchesEnded(touches, with: event) 57 | touchesUp() 58 | } 59 | 60 | override open func touchesCancelled(_ touches: Set, with event: UIEvent?) { 61 | super.touchesCancelled(touches, with: event) 62 | touchingDownInside = false 63 | touchesUp() 64 | } 65 | 66 | fileprivate func configureViews() { 67 | dimmedView.backgroundColor = nil 68 | addSubview(contentView) 69 | contentView.clipsToBounds = true 70 | addSubview(dimmedView) 71 | clipsToBounds = true 72 | } 73 | 74 | fileprivate func configureConstraints() { 75 | subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } 76 | var constraints: [NSLayoutConstraint] = [] 77 | constraints.append(contentsOf: NSLayoutConstraint.filledInSuperview(contentView)) 78 | constraints.append(contentsOf: NSLayoutConstraint.filledInSuperview(dimmedView)) 79 | NSLayoutConstraint.activate(constraints) 80 | } 81 | } 82 | 83 | extension TappableView { 84 | fileprivate func touchesUp(_ touches: Set? = nil) { 85 | undoTapAnimation() 86 | if touchingDownInside { 87 | tapped?() 88 | } 89 | touchingDownInside = false 90 | } 91 | 92 | fileprivate func boundsContain(_ touches: Set) -> Bool { 93 | for touch in touches { 94 | let location: CGPoint = touch.location(in: self) 95 | if bounds.contains(location) { return true } 96 | } 97 | return false 98 | } 99 | 100 | fileprivate func animateTap() { 101 | guard !animationState.performed else { return } 102 | animationState.performed = true 103 | animationState.restoration = transform 104 | UIView.animate(withDuration: 0.2) { 105 | let scale: CGFloat = 0.8 106 | self.transform = self.animationState.restoration.concatenating(CGAffineTransform(scaleX: scale, y: scale)) 107 | self.dimmedView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5) 108 | } 109 | } 110 | 111 | fileprivate func undoTapAnimation() { 112 | guard animationState.performed else { return } 113 | animationState.performed = false 114 | UIView.animate(withDuration: 0.2) { 115 | self.transform = self.animationState.restoration 116 | self.dimmedView.backgroundColor = nil 117 | } 118 | animationState.restoration = .identity 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /MyCards/MyCards/UICollectionView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 04/02/17. 6 | // 7 | 8 | import UIKit 9 | 10 | public extension UICollectionViewCell { 11 | public static var identifier: String { return String(describing: self) } 12 | } 13 | 14 | public extension UICollectionView { 15 | public func register(_ cell: UICollectionViewCell.Type) { 16 | register(cell, forCellWithReuseIdentifier: cell.identifier) 17 | } 18 | 19 | public func dequeueReusableCell(of class: CellClass.Type, 20 | for indexPath: IndexPath, 21 | configure: ((CellClass) -> Void) = { _ in }) -> UICollectionViewCell { 22 | 23 | let cell = dequeueReusableCell(withReuseIdentifier: CellClass.identifier, 24 | for: indexPath) 25 | 26 | if let typedCell = cell as? CellClass { 27 | configure(typedCell) 28 | } 29 | 30 | return cell 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MyCards/MyCards/UIColor+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 20/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIColor { 11 | static let lightBlue = UIColor(red: 0.16, green: 0.59, blue: 0.95, alpha: 1.00) 12 | static let darkBlue = UIColor(red: 0.15, green: 0.47, blue: 0.65, alpha: 1.00) 13 | static let darkerBlue = UIColor(red: 0.11, green: 0.36, blue: 0.52, alpha: 1.00) 14 | static let lightOrange = UIColor(red: 0.99, green: 0.59, blue: 0.20, alpha: 1.00) 15 | static let redOrange = UIColor(red: 0.99, green: 0.42, blue: 0.13, alpha: 1.00) 16 | static let darkOrange = UIColor(red: 0.99, green: 0.42, blue: 0.13, alpha: 1.00) 17 | static let lightRed = UIColor(red: 1.00, green: 0.25, blue: 0.09, alpha: 1.00) 18 | static let orange = UIColor(red: 1.00, green: 0.43, blue: 0.17, alpha: 1.00) 19 | static let titleBlue = UIColor(red: 0.00, green: 0.56, blue: 0.84, alpha: 1.00) 20 | } 21 | -------------------------------------------------------------------------------- /MyCards/MyCards/UIImage+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 30/04/17. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIImage { 11 | 12 | func resized(to size: CGSize) -> UIImage? { 13 | let image = self 14 | UIGraphicsBeginImageContextWithOptions(size, false, 0.0) 15 | image.draw(in: CGRect(origin: CGPoint.zero, size: size)) 16 | defer { 17 | UIGraphicsEndImageContext() 18 | } 19 | guard let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext() else { return nil } 20 | return newImage 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MyCards/MyCards/UIImagePickerController+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImagePickerController+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 20/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIImagePickerController { 11 | class func availableImagePickerSources() -> [UIImagePickerControllerSourceType] { 12 | return UIImagePickerControllerSourceType.allSources.filter { 13 | UIImagePickerController.isSourceTypeAvailable($0) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MyCards/MyCards/UIImagePickerControllerSourceType+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImagePickerControllerSourceType+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 20/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIImagePickerControllerSourceType: CustomStringConvertible { 11 | public var description: String { 12 | switch self { 13 | case .camera: return .Camera 14 | case .photoLibrary: return .PhotoLibrary 15 | case .savedPhotosAlbum: return .SavedPhotosAlbum 16 | } 17 | } 18 | 19 | static let allSources: [UIImagePickerControllerSourceType] = [.camera, .photoLibrary, .savedPhotosAlbum] 20 | } 21 | -------------------------------------------------------------------------------- /MyCards/MyCards/UINavigationController+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 05/12/16. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UINavigationController { 11 | open override var preferredStatusBarStyle: UIStatusBarStyle { 12 | return topViewController?.preferredStatusBarStyle ?? .default 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MyCards/MyCards/UITextField+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITextField+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 03/02/17. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UITextField { 11 | class func makeNameField() -> UITextField { 12 | let name = UITextField() 13 | name.autocorrectionType = .no 14 | name.autocapitalizationType = .none 15 | name.placeholder = .EnterCardName 16 | name.returnKeyType = .done 17 | return name 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /MyCards/MyCards/UIView+Constrained.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Constrained.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 05/02/17. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | class func constrained() -> Self { 12 | let view = self.init(frame: .zero) 13 | view.translatesAutoresizingMaskIntoConstraints = false 14 | return view 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MyCards/MyCards/UIViewController+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Extension.swift 3 | // MyCards 4 | // 5 | // Created by Maciej Piotrowski on 04/12/16. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIViewController { 11 | func dismiss() { 12 | view.endEditing(true) 13 | dismiss(animated: true, completion: nil) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MyCards/MyCards/iTunesArtwork: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftingio/architecture-wars-mvc/8281f0f6ac244c4ed2a89bc19c7c7fb4f38bdb4a/MyCards/MyCards/iTunesArtwork -------------------------------------------------------------------------------- /MyCards/MyCards/iTunesArtwork@2x: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftingio/architecture-wars-mvc/8281f0f6ac244c4ed2a89bc19c7c7fb4f38bdb4a/MyCards/MyCards/iTunesArtwork@2x -------------------------------------------------------------------------------- /MyCards/MyCardsTests/CardParserConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardParserConstants.swift 3 | // MyCardsTests 4 | // 5 | // Created by Paciej on 10/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | static let cardName = "swifting.io" 12 | static let cardIdentifier = "6CD41BC5-1E8F-4947-BE27-B3A024DAFE47" 13 | } 14 | -------------------------------------------------------------------------------- /MyCards/MyCardsTests/CardParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardParserTests.swift 3 | // MyCardsTests 4 | // 5 | // Created by Paciej on 10/12/2017. 6 | // 7 | 8 | import XCTest 9 | @testable import MyCards 10 | 11 | class CardParserTests: XCTestCase { 12 | 13 | var parser: CardParser! 14 | override func setUp() { 15 | super.setUp() 16 | parser = CardParser() 17 | } 18 | 19 | override func tearDown() { 20 | parser = nil 21 | super.tearDown() 22 | } 23 | 24 | func testParsing() { 25 | do { 26 | let data = jsonDataFromMainBundle() 27 | if let cards: [Card] = try parser.parse(data) as? [Card], 28 | let card = cards.first { 29 | XCTAssertNotNil(cards) 30 | XCTAssertFalse(cards.isEmpty) 31 | XCTAssertEqual(card.name, .cardName) 32 | XCTAssertEqual(card.identifier, .cardIdentifier) 33 | XCTAssertNotNil(card.front) 34 | XCTAssertNotNil(card.back) 35 | } else { 36 | XCTFail("no card in json :(") 37 | } 38 | } catch { 39 | XCTFail("parser thrown an error: \(error)") 40 | } 41 | } 42 | 43 | func testConverting() { 44 | let front = UIImage(data: #imageLiteral(resourceName: "front").data!) 45 | let back = UIImage(data: #imageLiteral(resourceName: "back").data!) 46 | let card = Card(identifier: .cardIdentifier, name: .cardName, front: front, back: back) 47 | do { 48 | guard let jsonFromCard = parser.json(from: [card]), 49 | let cards = try parser.parse(jsonFromCard) as? [Card], 50 | let cardFromJSON = cards.first else { 51 | XCTFail("parsing jSON failed") 52 | return 53 | } 54 | XCTAssertNotNil(jsonFromCard) 55 | XCTAssertEqual(card, cardFromJSON) 56 | } catch { 57 | XCTFail("parser thrown an error: \(error)") 58 | } 59 | } 60 | } 61 | 62 | extension CardParserTests { 63 | func jsonDataFromMainBundle() -> Data { 64 | let bundle = Bundle(for: CardParser.self) 65 | guard let url = bundle.url(forResource: "cards", withExtension: "json"), 66 | let data = try? Data(contentsOf: url) else { 67 | XCTFail("cards.json not contained in a bundle!") 68 | return Data() 69 | } 70 | XCTAssertNotNil(data) 71 | return data 72 | } 73 | 74 | func write(_ data: Data, to file: String = "cards.json") -> URL? { 75 | if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { 76 | let fileURL = dir.appendingPathComponent(file) 77 | do { 78 | try data.write(to: fileURL) 79 | return fileURL 80 | } catch { /* error handling here */ } 81 | } 82 | return nil 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /MyCards/MyCardsTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MyCards/iTunesArtwork: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftingio/architecture-wars-mvc/8281f0f6ac244c4ed2a89bc19c7c7fb4f38bdb4a/MyCards/iTunesArtwork -------------------------------------------------------------------------------- /MyCards/iTunesArtwork@2x: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftingio/architecture-wars-mvc/8281f0f6ac244c4ed2a89bc19c7c7fb4f38bdb4a/MyCards/iTunesArtwork@2x -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![Build Status](https://travis-ci.org/swiftingio/architecture-wars-mvc.svg?branch=master)](https://travis-ci.org/swiftingio/architecture-wars-mvc) [![codebeat badge](https://codebeat.co/badges/31d73f22-9469-420d-b019-e8150027432f)](https://codebeat.co/projects/github-com-swiftingio-architecture-wars-mvc) 2 | # My Cards 3 | 4 | Application for storing loalty cards. It is a supporting material for Architecture Wars: MVC strikes back blog post. 5 | 6 | - [\#41 Architecture Wars – MVC strikes back & takes a photo with AVFoundation](https://swifting.io/blog/2017/05/06/41-architecture-wars-mvc-strikes-back-takes-a-photo-with-avfoundation/) 7 | - [\#24 Architecture Wars – A New Hope](https://swifting.io/blog/2016/09/07/architecture-wars-a-new-hope/) 8 | 9 | ![](https://raw.githubusercontent.com/swiftingio/blog/%2341-MVC-strikes-back/5.png) 10 | --------------------------------------------------------------------------------