├── SwiftfulThinkingContinuedLearning.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── nicksarno.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── SwiftfulThinkingContinuedLearning ├── AccessibilityColorsBootcamp.swift ├── AccessibilityTextBootcamp.swift ├── AccessibilityVoiceOverBootcamp.swift ├── AlignmentGuideBootcamp.swift ├── ArraysBootcamp.swift ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── AppIcon.appiconset │ └── Contents.json ├── Contents.json └── steve-jobs.imageset │ ├── Contents.json │ └── steve-jobs.jpg ├── BackgroundThreadBootcamp.swift ├── CacheBootcamp.swift ├── CodableBootcamp.swift ├── CoreDataBootcamp.swift ├── CoreDataContainer.xcdatamodeld └── CoreDataContainer.xcdatamodel │ └── contents ├── CoreDataRelationshipsBootcamp.swift ├── DownloadWithCombine.swift ├── DownloadWithEscapingBootcamp.swift ├── DownloadingImages ├── Models │ └── PhotoModel.swift ├── Utilities │ ├── PhotoModelCacheManager.swift │ ├── PhotoModelDataService.swift │ └── PhotoModelFileManager.swift ├── ViewModels │ ├── DownloadingImagesViewModel.swift │ └── ImageLoadingViewModel.swift └── Views │ ├── DownloadingImageView.swift │ ├── DownloadingImagesBootcamp.swift │ └── DownloadingImagesRow.swift ├── DragGestureBootcamp.swift ├── DragGestureBootcamp2.swift ├── EscapingBootcamp.swift ├── FileManagerBootcamp.swift ├── FruitsContainer.xcdatamodeld └── FruitsContainer.xcdatamodel │ └── contents ├── GeometryReaderBootcamp.swift ├── HapticsBootcamp.swift ├── HashableBootcamp.swift ├── Info.plist ├── LocalNotificationBootcamp.swift ├── LongPressGestureBootcamp.swift ├── MagnificationGestureBootcamp.swift ├── MaskBootcamp.swift ├── MultipleSheetsBootcamp.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json ├── RotationGestureBootcamp.swift ├── ScrollViewPagingBootcamp.swift ├── ScrollViewReaderBootcamp.swift ├── Sounds ├── badum.mp3 └── tada.mp3 ├── SoundsBootcamp.swift ├── SubscriberBootcamp.swift ├── SwiftfulThinkingContinuedLearningApp.swift ├── TimerBootcamp.swift ├── TypealiasBootcamp.swift ├── VisualEffectBootcamp.swift └── WeakSelfBootcamp.swift /SwiftfulThinkingContinuedLearning.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 023DABEC2617F35D00AEE98D /* LocalNotificationBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023DABEB2617F35D00AEE98D /* LocalNotificationBootcamp.swift */; }; 11 | 023DABEF2617FFC900AEE98D /* HashableBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023DABEE2617FFC900AEE98D /* HashableBootcamp.swift */; }; 12 | 023DABF2261819C600AEE98D /* ArraysBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023DABF1261819C600AEE98D /* ArraysBootcamp.swift */; }; 13 | 023DAC23261C295300AEE98D /* CoreDataBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023DAC22261C295300AEE98D /* CoreDataBootcamp.swift */; }; 14 | 023DAC27261C2B8600AEE98D /* FruitsContainer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 023DAC25261C2B8600AEE98D /* FruitsContainer.xcdatamodeld */; }; 15 | 023DAC81261EACBB00AEE98D /* CoreDataRelationshipsBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023DAC80261EACBB00AEE98D /* CoreDataRelationshipsBootcamp.swift */; }; 16 | 023DAC85261EAED200AEE98D /* CoreDataContainer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 023DAC83261EAED200AEE98D /* CoreDataContainer.xcdatamodeld */; }; 17 | 023DAC9126213D9A00AEE98D /* BackgroundThreadBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023DAC9026213D9A00AEE98D /* BackgroundThreadBootcamp.swift */; }; 18 | 023DAC942621478E00AEE98D /* WeakSelfBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023DAC932621478E00AEE98D /* WeakSelfBootcamp.swift */; }; 19 | 023DAC97262155F900AEE98D /* TypealiasBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023DAC96262155F900AEE98D /* TypealiasBootcamp.swift */; }; 20 | 023DAC9A26221BFD00AEE98D /* EscapingBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023DAC9926221BFD00AEE98D /* EscapingBootcamp.swift */; }; 21 | 023DAC9D2622263400AEE98D /* CodableBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023DAC9C2622263400AEE98D /* CodableBootcamp.swift */; }; 22 | 023DACA12622400300AEE98D /* DownloadWithEscapingBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023DACA02622400300AEE98D /* DownloadWithEscapingBootcamp.swift */; }; 23 | 023DACA426225BFC00AEE98D /* DownloadWithCombine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023DACA326225BFC00AEE98D /* DownloadWithCombine.swift */; }; 24 | 026301692607AAA200E216EB /* SwiftfulThinkingContinuedLearningApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026301682607AAA200E216EB /* SwiftfulThinkingContinuedLearningApp.swift */; }; 25 | 0263016D2607AAA500E216EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0263016C2607AAA500E216EB /* Assets.xcassets */; }; 26 | 026301702607AAA500E216EB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0263016F2607AAA500E216EB /* Preview Assets.xcassets */; }; 27 | 0263017A2607AB2A00E216EB /* LongPressGestureBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026301792607AB2A00E216EB /* LongPressGestureBootcamp.swift */; }; 28 | 0263017D2607B21E00E216EB /* MagnificationGestureBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0263017C2607B21E00E216EB /* MagnificationGestureBootcamp.swift */; }; 29 | 026301802607B9D500E216EB /* RotationGestureBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0263017F2607B9D500E216EB /* RotationGestureBootcamp.swift */; }; 30 | 026301832607BCD800E216EB /* DragGestureBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026301822607BCD800E216EB /* DragGestureBootcamp.swift */; }; 31 | 026301862607C23500E216EB /* DragGestureBootcamp2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026301852607C23500E216EB /* DragGestureBootcamp2.swift */; }; 32 | 026301892607D07300E216EB /* ScrollViewReaderBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026301882607D07300E216EB /* ScrollViewReaderBootcamp.swift */; }; 33 | 0263018C2607DAFE00E216EB /* GeometryReaderBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0263018B2607DAFE00E216EB /* GeometryReaderBootcamp.swift */; }; 34 | 0263020726101E4C00E216EB /* MultipleSheetsBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0263020626101E4C00E216EB /* MultipleSheetsBootcamp.swift */; }; 35 | 0263020A2610284F00E216EB /* MaskBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026302092610284F00E216EB /* MaskBootcamp.swift */; }; 36 | 02630215261422B000E216EB /* SoundsBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02630214261422B000E216EB /* SoundsBootcamp.swift */; }; 37 | 0263021A2614281200E216EB /* badum.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 026302182614281100E216EB /* badum.mp3 */; }; 38 | 0263021B2614281200E216EB /* tada.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 026302192614281200E216EB /* tada.mp3 */; }; 39 | 0263021E2614359400E216EB /* HapticsBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0263021D2614359400E216EB /* HapticsBootcamp.swift */; }; 40 | 0264C5D12AB8EDAE0033070B /* AlignmentGuideBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0264C5D02AB8EDAE0033070B /* AlignmentGuideBootcamp.swift */; }; 41 | 0279E6982637D4C700094483 /* CacheBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279E6972637D4C700094483 /* CacheBootcamp.swift */; }; 42 | 0279E6BF263A4E3700094483 /* DownloadingImagesBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279E6BE263A4E3700094483 /* DownloadingImagesBootcamp.swift */; }; 43 | 0279E6C3263A4FA600094483 /* PhotoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279E6C2263A4FA600094483 /* PhotoModel.swift */; }; 44 | 0279E6C6263A503200094483 /* DownloadingImagesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279E6C5263A503200094483 /* DownloadingImagesViewModel.swift */; }; 45 | 0279E6C9263A513200094483 /* PhotoModelDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279E6C8263A513200094483 /* PhotoModelDataService.swift */; }; 46 | 0279E6CD263A568700094483 /* DownloadingImagesRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279E6CC263A568700094483 /* DownloadingImagesRow.swift */; }; 47 | 0279E6D0263A571C00094483 /* DownloadingImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279E6CF263A571C00094483 /* DownloadingImageView.swift */; }; 48 | 0279E6D3263A57FA00094483 /* ImageLoadingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279E6D2263A57FA00094483 /* ImageLoadingViewModel.swift */; }; 49 | 0279E6D9263A608400094483 /* PhotoModelCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279E6D8263A608400094483 /* PhotoModelCacheManager.swift */; }; 50 | 0279E6DC263A619500094483 /* PhotoModelFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279E6DB263A619500094483 /* PhotoModelFileManager.swift */; }; 51 | 028E664B2B66EA8200580761 /* VisualEffectBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028E664A2B66EA8200580761 /* VisualEffectBootcamp.swift */; }; 52 | 028E664D2B66F00F00580761 /* ScrollViewPagingBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028E664C2B66F00F00580761 /* ScrollViewPagingBootcamp.swift */; }; 53 | 02931A6E2A427E9F001BE6A5 /* AccessibilityTextBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02931A6D2A427E9F001BE6A5 /* AccessibilityTextBootcamp.swift */; }; 54 | 02931A702A42869D001BE6A5 /* AccessibilityColorsBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02931A6F2A42869D001BE6A5 /* AccessibilityColorsBootcamp.swift */; }; 55 | 02931A722A4290F7001BE6A5 /* AccessibilityVoiceOverBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02931A712A4290F7001BE6A5 /* AccessibilityVoiceOverBootcamp.swift */; }; 56 | 029FB28D262B9FD30062D169 /* TimerBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029FB28C262B9FD30062D169 /* TimerBootcamp.swift */; }; 57 | 029FB290262BB2AE0062D169 /* SubscriberBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029FB28F262BB2AE0062D169 /* SubscriberBootcamp.swift */; }; 58 | 02B2838A26326E2500FF11BC /* FileManagerBootcamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B2838926326E2500FF11BC /* FileManagerBootcamp.swift */; }; 59 | /* End PBXBuildFile section */ 60 | 61 | /* Begin PBXFileReference section */ 62 | 023DABEB2617F35D00AEE98D /* LocalNotificationBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationBootcamp.swift; sourceTree = ""; }; 63 | 023DABEE2617FFC900AEE98D /* HashableBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashableBootcamp.swift; sourceTree = ""; }; 64 | 023DABF1261819C600AEE98D /* ArraysBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArraysBootcamp.swift; sourceTree = ""; }; 65 | 023DAC22261C295300AEE98D /* CoreDataBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataBootcamp.swift; sourceTree = ""; }; 66 | 023DAC26261C2B8600AEE98D /* FruitsContainer.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FruitsContainer.xcdatamodel; sourceTree = ""; }; 67 | 023DAC80261EACBB00AEE98D /* CoreDataRelationshipsBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataRelationshipsBootcamp.swift; sourceTree = ""; }; 68 | 023DAC84261EAED200AEE98D /* CoreDataContainer.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreDataContainer.xcdatamodel; sourceTree = ""; }; 69 | 023DAC9026213D9A00AEE98D /* BackgroundThreadBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundThreadBootcamp.swift; sourceTree = ""; }; 70 | 023DAC932621478E00AEE98D /* WeakSelfBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakSelfBootcamp.swift; sourceTree = ""; }; 71 | 023DAC96262155F900AEE98D /* TypealiasBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypealiasBootcamp.swift; sourceTree = ""; }; 72 | 023DAC9926221BFD00AEE98D /* EscapingBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EscapingBootcamp.swift; sourceTree = ""; }; 73 | 023DAC9C2622263400AEE98D /* CodableBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableBootcamp.swift; sourceTree = ""; }; 74 | 023DACA02622400300AEE98D /* DownloadWithEscapingBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadWithEscapingBootcamp.swift; sourceTree = ""; }; 75 | 023DACA326225BFC00AEE98D /* DownloadWithCombine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadWithCombine.swift; sourceTree = ""; }; 76 | 026301652607AAA200E216EB /* SwiftfulThinkingContinuedLearning.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftfulThinkingContinuedLearning.app; sourceTree = BUILT_PRODUCTS_DIR; }; 77 | 026301682607AAA200E216EB /* SwiftfulThinkingContinuedLearningApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfulThinkingContinuedLearningApp.swift; sourceTree = ""; }; 78 | 0263016C2607AAA500E216EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 79 | 0263016F2607AAA500E216EB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 80 | 026301712607AAA500E216EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 81 | 026301792607AB2A00E216EB /* LongPressGestureBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPressGestureBootcamp.swift; sourceTree = ""; }; 82 | 0263017C2607B21E00E216EB /* MagnificationGestureBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagnificationGestureBootcamp.swift; sourceTree = ""; }; 83 | 0263017F2607B9D500E216EB /* RotationGestureBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotationGestureBootcamp.swift; sourceTree = ""; }; 84 | 026301822607BCD800E216EB /* DragGestureBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragGestureBootcamp.swift; sourceTree = ""; }; 85 | 026301852607C23500E216EB /* DragGestureBootcamp2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragGestureBootcamp2.swift; sourceTree = ""; }; 86 | 026301882607D07300E216EB /* ScrollViewReaderBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewReaderBootcamp.swift; sourceTree = ""; }; 87 | 0263018B2607DAFE00E216EB /* GeometryReaderBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryReaderBootcamp.swift; sourceTree = ""; }; 88 | 0263020626101E4C00E216EB /* MultipleSheetsBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleSheetsBootcamp.swift; sourceTree = ""; }; 89 | 026302092610284F00E216EB /* MaskBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskBootcamp.swift; sourceTree = ""; }; 90 | 02630214261422B000E216EB /* SoundsBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundsBootcamp.swift; sourceTree = ""; }; 91 | 026302182614281100E216EB /* badum.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = badum.mp3; sourceTree = ""; }; 92 | 026302192614281200E216EB /* tada.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = tada.mp3; sourceTree = ""; }; 93 | 0263021D2614359400E216EB /* HapticsBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticsBootcamp.swift; sourceTree = ""; }; 94 | 0264C5D02AB8EDAE0033070B /* AlignmentGuideBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignmentGuideBootcamp.swift; sourceTree = ""; }; 95 | 0279E6972637D4C700094483 /* CacheBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheBootcamp.swift; sourceTree = ""; }; 96 | 0279E6BE263A4E3700094483 /* DownloadingImagesBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadingImagesBootcamp.swift; sourceTree = ""; }; 97 | 0279E6C2263A4FA600094483 /* PhotoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoModel.swift; sourceTree = ""; }; 98 | 0279E6C5263A503200094483 /* DownloadingImagesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadingImagesViewModel.swift; sourceTree = ""; }; 99 | 0279E6C8263A513200094483 /* PhotoModelDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoModelDataService.swift; sourceTree = ""; }; 100 | 0279E6CC263A568700094483 /* DownloadingImagesRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadingImagesRow.swift; sourceTree = ""; }; 101 | 0279E6CF263A571C00094483 /* DownloadingImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadingImageView.swift; sourceTree = ""; }; 102 | 0279E6D2263A57FA00094483 /* ImageLoadingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoadingViewModel.swift; sourceTree = ""; }; 103 | 0279E6D8263A608400094483 /* PhotoModelCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoModelCacheManager.swift; sourceTree = ""; }; 104 | 0279E6DB263A619500094483 /* PhotoModelFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoModelFileManager.swift; sourceTree = ""; }; 105 | 028E664A2B66EA8200580761 /* VisualEffectBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectBootcamp.swift; sourceTree = ""; }; 106 | 028E664C2B66F00F00580761 /* ScrollViewPagingBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewPagingBootcamp.swift; sourceTree = ""; }; 107 | 02931A6D2A427E9F001BE6A5 /* AccessibilityTextBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityTextBootcamp.swift; sourceTree = ""; }; 108 | 02931A6F2A42869D001BE6A5 /* AccessibilityColorsBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityColorsBootcamp.swift; sourceTree = ""; }; 109 | 02931A712A4290F7001BE6A5 /* AccessibilityVoiceOverBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityVoiceOverBootcamp.swift; sourceTree = ""; }; 110 | 029FB28C262B9FD30062D169 /* TimerBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerBootcamp.swift; sourceTree = ""; }; 111 | 029FB28F262BB2AE0062D169 /* SubscriberBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriberBootcamp.swift; sourceTree = ""; }; 112 | 02B2838926326E2500FF11BC /* FileManagerBootcamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerBootcamp.swift; sourceTree = ""; }; 113 | /* End PBXFileReference section */ 114 | 115 | /* Begin PBXFrameworksBuildPhase section */ 116 | 026301622607AAA200E216EB /* Frameworks */ = { 117 | isa = PBXFrameworksBuildPhase; 118 | buildActionMask = 2147483647; 119 | files = ( 120 | ); 121 | runOnlyForDeploymentPostprocessing = 0; 122 | }; 123 | /* End PBXFrameworksBuildPhase section */ 124 | 125 | /* Begin PBXGroup section */ 126 | 0263015C2607AAA200E216EB = { 127 | isa = PBXGroup; 128 | children = ( 129 | 026301672607AAA200E216EB /* SwiftfulThinkingContinuedLearning */, 130 | 026301662607AAA200E216EB /* Products */, 131 | ); 132 | sourceTree = ""; 133 | }; 134 | 026301662607AAA200E216EB /* Products */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 026301652607AAA200E216EB /* SwiftfulThinkingContinuedLearning.app */, 138 | ); 139 | name = Products; 140 | sourceTree = ""; 141 | }; 142 | 026301672607AAA200E216EB /* SwiftfulThinkingContinuedLearning */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 026301682607AAA200E216EB /* SwiftfulThinkingContinuedLearningApp.swift */, 146 | 026301792607AB2A00E216EB /* LongPressGestureBootcamp.swift */, 147 | 0263017C2607B21E00E216EB /* MagnificationGestureBootcamp.swift */, 148 | 0263017F2607B9D500E216EB /* RotationGestureBootcamp.swift */, 149 | 026301822607BCD800E216EB /* DragGestureBootcamp.swift */, 150 | 026301852607C23500E216EB /* DragGestureBootcamp2.swift */, 151 | 026301882607D07300E216EB /* ScrollViewReaderBootcamp.swift */, 152 | 0263018B2607DAFE00E216EB /* GeometryReaderBootcamp.swift */, 153 | 0263020626101E4C00E216EB /* MultipleSheetsBootcamp.swift */, 154 | 026302092610284F00E216EB /* MaskBootcamp.swift */, 155 | 02630214261422B000E216EB /* SoundsBootcamp.swift */, 156 | 02630217261427F000E216EB /* Sounds */, 157 | 0263021D2614359400E216EB /* HapticsBootcamp.swift */, 158 | 023DABEB2617F35D00AEE98D /* LocalNotificationBootcamp.swift */, 159 | 023DABEE2617FFC900AEE98D /* HashableBootcamp.swift */, 160 | 023DABF1261819C600AEE98D /* ArraysBootcamp.swift */, 161 | 023DAC22261C295300AEE98D /* CoreDataBootcamp.swift */, 162 | 023DAC25261C2B8600AEE98D /* FruitsContainer.xcdatamodeld */, 163 | 023DAC80261EACBB00AEE98D /* CoreDataRelationshipsBootcamp.swift */, 164 | 023DAC83261EAED200AEE98D /* CoreDataContainer.xcdatamodeld */, 165 | 023DAC9026213D9A00AEE98D /* BackgroundThreadBootcamp.swift */, 166 | 023DAC932621478E00AEE98D /* WeakSelfBootcamp.swift */, 167 | 023DAC96262155F900AEE98D /* TypealiasBootcamp.swift */, 168 | 023DAC9926221BFD00AEE98D /* EscapingBootcamp.swift */, 169 | 023DAC9C2622263400AEE98D /* CodableBootcamp.swift */, 170 | 023DACA02622400300AEE98D /* DownloadWithEscapingBootcamp.swift */, 171 | 023DACA326225BFC00AEE98D /* DownloadWithCombine.swift */, 172 | 029FB28C262B9FD30062D169 /* TimerBootcamp.swift */, 173 | 029FB28F262BB2AE0062D169 /* SubscriberBootcamp.swift */, 174 | 02B2838926326E2500FF11BC /* FileManagerBootcamp.swift */, 175 | 0279E6972637D4C700094483 /* CacheBootcamp.swift */, 176 | 0279E6B3263A4D9400094483 /* DownloadingImages */, 177 | 02931A6D2A427E9F001BE6A5 /* AccessibilityTextBootcamp.swift */, 178 | 02931A6F2A42869D001BE6A5 /* AccessibilityColorsBootcamp.swift */, 179 | 02931A712A4290F7001BE6A5 /* AccessibilityVoiceOverBootcamp.swift */, 180 | 0264C5D02AB8EDAE0033070B /* AlignmentGuideBootcamp.swift */, 181 | 028E664A2B66EA8200580761 /* VisualEffectBootcamp.swift */, 182 | 028E664C2B66F00F00580761 /* ScrollViewPagingBootcamp.swift */, 183 | 0263016C2607AAA500E216EB /* Assets.xcassets */, 184 | 026301712607AAA500E216EB /* Info.plist */, 185 | 0263016E2607AAA500E216EB /* Preview Content */, 186 | ); 187 | path = SwiftfulThinkingContinuedLearning; 188 | sourceTree = ""; 189 | }; 190 | 0263016E2607AAA500E216EB /* Preview Content */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | 0263016F2607AAA500E216EB /* Preview Assets.xcassets */, 194 | ); 195 | path = "Preview Content"; 196 | sourceTree = ""; 197 | }; 198 | 02630217261427F000E216EB /* Sounds */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | 026302182614281100E216EB /* badum.mp3 */, 202 | 026302192614281200E216EB /* tada.mp3 */, 203 | ); 204 | path = Sounds; 205 | sourceTree = ""; 206 | }; 207 | 0279E6B3263A4D9400094483 /* DownloadingImages */ = { 208 | isa = PBXGroup; 209 | children = ( 210 | 0279E6BD263A4E0600094483 /* Utilities */, 211 | 0279E6BB263A4DF200094483 /* ViewModels */, 212 | 0279E6BA263A4DEC00094483 /* Views */, 213 | 0279E6B9263A4DE500094483 /* Models */, 214 | ); 215 | path = DownloadingImages; 216 | sourceTree = ""; 217 | }; 218 | 0279E6B9263A4DE500094483 /* Models */ = { 219 | isa = PBXGroup; 220 | children = ( 221 | 0279E6C2263A4FA600094483 /* PhotoModel.swift */, 222 | ); 223 | path = Models; 224 | sourceTree = ""; 225 | }; 226 | 0279E6BA263A4DEC00094483 /* Views */ = { 227 | isa = PBXGroup; 228 | children = ( 229 | 0279E6BE263A4E3700094483 /* DownloadingImagesBootcamp.swift */, 230 | 0279E6CC263A568700094483 /* DownloadingImagesRow.swift */, 231 | 0279E6CF263A571C00094483 /* DownloadingImageView.swift */, 232 | ); 233 | path = Views; 234 | sourceTree = ""; 235 | }; 236 | 0279E6BB263A4DF200094483 /* ViewModels */ = { 237 | isa = PBXGroup; 238 | children = ( 239 | 0279E6C5263A503200094483 /* DownloadingImagesViewModel.swift */, 240 | 0279E6D2263A57FA00094483 /* ImageLoadingViewModel.swift */, 241 | ); 242 | path = ViewModels; 243 | sourceTree = ""; 244 | }; 245 | 0279E6BD263A4E0600094483 /* Utilities */ = { 246 | isa = PBXGroup; 247 | children = ( 248 | 0279E6C8263A513200094483 /* PhotoModelDataService.swift */, 249 | 0279E6D8263A608400094483 /* PhotoModelCacheManager.swift */, 250 | 0279E6DB263A619500094483 /* PhotoModelFileManager.swift */, 251 | ); 252 | path = Utilities; 253 | sourceTree = ""; 254 | }; 255 | /* End PBXGroup section */ 256 | 257 | /* Begin PBXNativeTarget section */ 258 | 026301642607AAA200E216EB /* SwiftfulThinkingContinuedLearning */ = { 259 | isa = PBXNativeTarget; 260 | buildConfigurationList = 026301742607AAA500E216EB /* Build configuration list for PBXNativeTarget "SwiftfulThinkingContinuedLearning" */; 261 | buildPhases = ( 262 | 026301612607AAA200E216EB /* Sources */, 263 | 026301622607AAA200E216EB /* Frameworks */, 264 | 026301632607AAA200E216EB /* Resources */, 265 | ); 266 | buildRules = ( 267 | ); 268 | dependencies = ( 269 | ); 270 | name = SwiftfulThinkingContinuedLearning; 271 | productName = SwiftfulThinkingContinuedLearning; 272 | productReference = 026301652607AAA200E216EB /* SwiftfulThinkingContinuedLearning.app */; 273 | productType = "com.apple.product-type.application"; 274 | }; 275 | /* End PBXNativeTarget section */ 276 | 277 | /* Begin PBXProject section */ 278 | 0263015D2607AAA200E216EB /* Project object */ = { 279 | isa = PBXProject; 280 | attributes = { 281 | LastSwiftUpdateCheck = 1230; 282 | LastUpgradeCheck = 1230; 283 | TargetAttributes = { 284 | 026301642607AAA200E216EB = { 285 | CreatedOnToolsVersion = 12.3; 286 | }; 287 | }; 288 | }; 289 | buildConfigurationList = 026301602607AAA200E216EB /* Build configuration list for PBXProject "SwiftfulThinkingContinuedLearning" */; 290 | compatibilityVersion = "Xcode 9.3"; 291 | developmentRegion = en; 292 | hasScannedForEncodings = 0; 293 | knownRegions = ( 294 | en, 295 | Base, 296 | ); 297 | mainGroup = 0263015C2607AAA200E216EB; 298 | productRefGroup = 026301662607AAA200E216EB /* Products */; 299 | projectDirPath = ""; 300 | projectRoot = ""; 301 | targets = ( 302 | 026301642607AAA200E216EB /* SwiftfulThinkingContinuedLearning */, 303 | ); 304 | }; 305 | /* End PBXProject section */ 306 | 307 | /* Begin PBXResourcesBuildPhase section */ 308 | 026301632607AAA200E216EB /* Resources */ = { 309 | isa = PBXResourcesBuildPhase; 310 | buildActionMask = 2147483647; 311 | files = ( 312 | 0263021B2614281200E216EB /* tada.mp3 in Resources */, 313 | 0263021A2614281200E216EB /* badum.mp3 in Resources */, 314 | 026301702607AAA500E216EB /* Preview Assets.xcassets in Resources */, 315 | 0263016D2607AAA500E216EB /* Assets.xcassets in Resources */, 316 | ); 317 | runOnlyForDeploymentPostprocessing = 0; 318 | }; 319 | /* End PBXResourcesBuildPhase section */ 320 | 321 | /* Begin PBXSourcesBuildPhase section */ 322 | 026301612607AAA200E216EB /* Sources */ = { 323 | isa = PBXSourcesBuildPhase; 324 | buildActionMask = 2147483647; 325 | files = ( 326 | 023DAC9A26221BFD00AEE98D /* EscapingBootcamp.swift in Sources */, 327 | 02931A702A42869D001BE6A5 /* AccessibilityColorsBootcamp.swift in Sources */, 328 | 023DAC27261C2B8600AEE98D /* FruitsContainer.xcdatamodeld in Sources */, 329 | 023DACA426225BFC00AEE98D /* DownloadWithCombine.swift in Sources */, 330 | 028E664D2B66F00F00580761 /* ScrollViewPagingBootcamp.swift in Sources */, 331 | 023DABEF2617FFC900AEE98D /* HashableBootcamp.swift in Sources */, 332 | 0279E6DC263A619500094483 /* PhotoModelFileManager.swift in Sources */, 333 | 0279E6982637D4C700094483 /* CacheBootcamp.swift in Sources */, 334 | 0279E6CD263A568700094483 /* DownloadingImagesRow.swift in Sources */, 335 | 02630215261422B000E216EB /* SoundsBootcamp.swift in Sources */, 336 | 0279E6C9263A513200094483 /* PhotoModelDataService.swift in Sources */, 337 | 023DAC9126213D9A00AEE98D /* BackgroundThreadBootcamp.swift in Sources */, 338 | 02B2838A26326E2500FF11BC /* FileManagerBootcamp.swift in Sources */, 339 | 026301862607C23500E216EB /* DragGestureBootcamp2.swift in Sources */, 340 | 023DAC23261C295300AEE98D /* CoreDataBootcamp.swift in Sources */, 341 | 0263020A2610284F00E216EB /* MaskBootcamp.swift in Sources */, 342 | 0279E6D9263A608400094483 /* PhotoModelCacheManager.swift in Sources */, 343 | 026301832607BCD800E216EB /* DragGestureBootcamp.swift in Sources */, 344 | 029FB290262BB2AE0062D169 /* SubscriberBootcamp.swift in Sources */, 345 | 023DAC85261EAED200AEE98D /* CoreDataContainer.xcdatamodeld in Sources */, 346 | 028E664B2B66EA8200580761 /* VisualEffectBootcamp.swift in Sources */, 347 | 023DABF2261819C600AEE98D /* ArraysBootcamp.swift in Sources */, 348 | 02931A722A4290F7001BE6A5 /* AccessibilityVoiceOverBootcamp.swift in Sources */, 349 | 023DABEC2617F35D00AEE98D /* LocalNotificationBootcamp.swift in Sources */, 350 | 026301892607D07300E216EB /* ScrollViewReaderBootcamp.swift in Sources */, 351 | 023DAC97262155F900AEE98D /* TypealiasBootcamp.swift in Sources */, 352 | 0263020726101E4C00E216EB /* MultipleSheetsBootcamp.swift in Sources */, 353 | 023DACA12622400300AEE98D /* DownloadWithEscapingBootcamp.swift in Sources */, 354 | 023DAC9D2622263400AEE98D /* CodableBootcamp.swift in Sources */, 355 | 023DAC942621478E00AEE98D /* WeakSelfBootcamp.swift in Sources */, 356 | 0264C5D12AB8EDAE0033070B /* AlignmentGuideBootcamp.swift in Sources */, 357 | 026301692607AAA200E216EB /* SwiftfulThinkingContinuedLearningApp.swift in Sources */, 358 | 0263017D2607B21E00E216EB /* MagnificationGestureBootcamp.swift in Sources */, 359 | 02931A6E2A427E9F001BE6A5 /* AccessibilityTextBootcamp.swift in Sources */, 360 | 0279E6D3263A57FA00094483 /* ImageLoadingViewModel.swift in Sources */, 361 | 0263018C2607DAFE00E216EB /* GeometryReaderBootcamp.swift in Sources */, 362 | 0263021E2614359400E216EB /* HapticsBootcamp.swift in Sources */, 363 | 0279E6BF263A4E3700094483 /* DownloadingImagesBootcamp.swift in Sources */, 364 | 0279E6C6263A503200094483 /* DownloadingImagesViewModel.swift in Sources */, 365 | 029FB28D262B9FD30062D169 /* TimerBootcamp.swift in Sources */, 366 | 023DAC81261EACBB00AEE98D /* CoreDataRelationshipsBootcamp.swift in Sources */, 367 | 0279E6C3263A4FA600094483 /* PhotoModel.swift in Sources */, 368 | 0279E6D0263A571C00094483 /* DownloadingImageView.swift in Sources */, 369 | 0263017A2607AB2A00E216EB /* LongPressGestureBootcamp.swift in Sources */, 370 | 026301802607B9D500E216EB /* RotationGestureBootcamp.swift in Sources */, 371 | ); 372 | runOnlyForDeploymentPostprocessing = 0; 373 | }; 374 | /* End PBXSourcesBuildPhase section */ 375 | 376 | /* Begin XCBuildConfiguration section */ 377 | 026301722607AAA500E216EB /* Debug */ = { 378 | isa = XCBuildConfiguration; 379 | buildSettings = { 380 | ALWAYS_SEARCH_USER_PATHS = NO; 381 | CLANG_ANALYZER_NONNULL = YES; 382 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 383 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 384 | CLANG_CXX_LIBRARY = "libc++"; 385 | CLANG_ENABLE_MODULES = YES; 386 | CLANG_ENABLE_OBJC_ARC = YES; 387 | CLANG_ENABLE_OBJC_WEAK = YES; 388 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 389 | CLANG_WARN_BOOL_CONVERSION = YES; 390 | CLANG_WARN_COMMA = YES; 391 | CLANG_WARN_CONSTANT_CONVERSION = YES; 392 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 393 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 394 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 395 | CLANG_WARN_EMPTY_BODY = YES; 396 | CLANG_WARN_ENUM_CONVERSION = YES; 397 | CLANG_WARN_INFINITE_RECURSION = YES; 398 | CLANG_WARN_INT_CONVERSION = YES; 399 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 400 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 401 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 402 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 403 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 404 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 405 | CLANG_WARN_STRICT_PROTOTYPES = YES; 406 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 407 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 408 | CLANG_WARN_UNREACHABLE_CODE = YES; 409 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 410 | COPY_PHASE_STRIP = NO; 411 | DEBUG_INFORMATION_FORMAT = dwarf; 412 | ENABLE_STRICT_OBJC_MSGSEND = YES; 413 | ENABLE_TESTABILITY = YES; 414 | GCC_C_LANGUAGE_STANDARD = gnu11; 415 | GCC_DYNAMIC_NO_PIC = NO; 416 | GCC_NO_COMMON_BLOCKS = YES; 417 | GCC_OPTIMIZATION_LEVEL = 0; 418 | GCC_PREPROCESSOR_DEFINITIONS = ( 419 | "DEBUG=1", 420 | "$(inherited)", 421 | ); 422 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 423 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 424 | GCC_WARN_UNDECLARED_SELECTOR = YES; 425 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 426 | GCC_WARN_UNUSED_FUNCTION = YES; 427 | GCC_WARN_UNUSED_VARIABLE = YES; 428 | IPHONEOS_DEPLOYMENT_TARGET = 14.3; 429 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 430 | MTL_FAST_MATH = YES; 431 | ONLY_ACTIVE_ARCH = YES; 432 | SDKROOT = iphoneos; 433 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 434 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 435 | }; 436 | name = Debug; 437 | }; 438 | 026301732607AAA500E216EB /* Release */ = { 439 | isa = XCBuildConfiguration; 440 | buildSettings = { 441 | ALWAYS_SEARCH_USER_PATHS = NO; 442 | CLANG_ANALYZER_NONNULL = YES; 443 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 444 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 445 | CLANG_CXX_LIBRARY = "libc++"; 446 | CLANG_ENABLE_MODULES = YES; 447 | CLANG_ENABLE_OBJC_ARC = YES; 448 | CLANG_ENABLE_OBJC_WEAK = YES; 449 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 450 | CLANG_WARN_BOOL_CONVERSION = YES; 451 | CLANG_WARN_COMMA = YES; 452 | CLANG_WARN_CONSTANT_CONVERSION = YES; 453 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 454 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 455 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 456 | CLANG_WARN_EMPTY_BODY = YES; 457 | CLANG_WARN_ENUM_CONVERSION = YES; 458 | CLANG_WARN_INFINITE_RECURSION = YES; 459 | CLANG_WARN_INT_CONVERSION = YES; 460 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 461 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 462 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 463 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 464 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 465 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 466 | CLANG_WARN_STRICT_PROTOTYPES = YES; 467 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 468 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 469 | CLANG_WARN_UNREACHABLE_CODE = YES; 470 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 471 | COPY_PHASE_STRIP = NO; 472 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 473 | ENABLE_NS_ASSERTIONS = NO; 474 | ENABLE_STRICT_OBJC_MSGSEND = YES; 475 | GCC_C_LANGUAGE_STANDARD = gnu11; 476 | GCC_NO_COMMON_BLOCKS = YES; 477 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 478 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 479 | GCC_WARN_UNDECLARED_SELECTOR = YES; 480 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 481 | GCC_WARN_UNUSED_FUNCTION = YES; 482 | GCC_WARN_UNUSED_VARIABLE = YES; 483 | IPHONEOS_DEPLOYMENT_TARGET = 14.3; 484 | MTL_ENABLE_DEBUG_INFO = NO; 485 | MTL_FAST_MATH = YES; 486 | SDKROOT = iphoneos; 487 | SWIFT_COMPILATION_MODE = wholemodule; 488 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 489 | VALIDATE_PRODUCT = YES; 490 | }; 491 | name = Release; 492 | }; 493 | 026301752607AAA500E216EB /* Debug */ = { 494 | isa = XCBuildConfiguration; 495 | buildSettings = { 496 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 497 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 498 | CODE_SIGN_STYLE = Automatic; 499 | DEVELOPMENT_ASSET_PATHS = "\"SwiftfulThinkingContinuedLearning/Preview Content\""; 500 | DEVELOPMENT_TEAM = 9JN8RSJMX3; 501 | ENABLE_PREVIEWS = YES; 502 | INFOPLIST_FILE = SwiftfulThinkingContinuedLearning/Info.plist; 503 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 504 | LD_RUNPATH_SEARCH_PATHS = ( 505 | "$(inherited)", 506 | "@executable_path/Frameworks", 507 | ); 508 | PRODUCT_BUNDLE_IDENTIFIER = com.nicksarno.SwiftfulThinkingContinuedLearning; 509 | PRODUCT_NAME = "$(TARGET_NAME)"; 510 | SWIFT_VERSION = 5.0; 511 | TARGETED_DEVICE_FAMILY = "1,2"; 512 | }; 513 | name = Debug; 514 | }; 515 | 026301762607AAA500E216EB /* Release */ = { 516 | isa = XCBuildConfiguration; 517 | buildSettings = { 518 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 519 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 520 | CODE_SIGN_STYLE = Automatic; 521 | DEVELOPMENT_ASSET_PATHS = "\"SwiftfulThinkingContinuedLearning/Preview Content\""; 522 | DEVELOPMENT_TEAM = 9JN8RSJMX3; 523 | ENABLE_PREVIEWS = YES; 524 | INFOPLIST_FILE = SwiftfulThinkingContinuedLearning/Info.plist; 525 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 526 | LD_RUNPATH_SEARCH_PATHS = ( 527 | "$(inherited)", 528 | "@executable_path/Frameworks", 529 | ); 530 | PRODUCT_BUNDLE_IDENTIFIER = com.nicksarno.SwiftfulThinkingContinuedLearning; 531 | PRODUCT_NAME = "$(TARGET_NAME)"; 532 | SWIFT_VERSION = 5.0; 533 | TARGETED_DEVICE_FAMILY = "1,2"; 534 | }; 535 | name = Release; 536 | }; 537 | /* End XCBuildConfiguration section */ 538 | 539 | /* Begin XCConfigurationList section */ 540 | 026301602607AAA200E216EB /* Build configuration list for PBXProject "SwiftfulThinkingContinuedLearning" */ = { 541 | isa = XCConfigurationList; 542 | buildConfigurations = ( 543 | 026301722607AAA500E216EB /* Debug */, 544 | 026301732607AAA500E216EB /* Release */, 545 | ); 546 | defaultConfigurationIsVisible = 0; 547 | defaultConfigurationName = Release; 548 | }; 549 | 026301742607AAA500E216EB /* Build configuration list for PBXNativeTarget "SwiftfulThinkingContinuedLearning" */ = { 550 | isa = XCConfigurationList; 551 | buildConfigurations = ( 552 | 026301752607AAA500E216EB /* Debug */, 553 | 026301762607AAA500E216EB /* Release */, 554 | ); 555 | defaultConfigurationIsVisible = 0; 556 | defaultConfigurationName = Release; 557 | }; 558 | /* End XCConfigurationList section */ 559 | 560 | /* Begin XCVersionGroup section */ 561 | 023DAC25261C2B8600AEE98D /* FruitsContainer.xcdatamodeld */ = { 562 | isa = XCVersionGroup; 563 | children = ( 564 | 023DAC26261C2B8600AEE98D /* FruitsContainer.xcdatamodel */, 565 | ); 566 | currentVersion = 023DAC26261C2B8600AEE98D /* FruitsContainer.xcdatamodel */; 567 | path = FruitsContainer.xcdatamodeld; 568 | sourceTree = ""; 569 | versionGroupType = wrapper.xcdatamodel; 570 | }; 571 | 023DAC83261EAED200AEE98D /* CoreDataContainer.xcdatamodeld */ = { 572 | isa = XCVersionGroup; 573 | children = ( 574 | 023DAC84261EAED200AEE98D /* CoreDataContainer.xcdatamodel */, 575 | ); 576 | currentVersion = 023DAC84261EAED200AEE98D /* CoreDataContainer.xcdatamodel */; 577 | path = CoreDataContainer.xcdatamodeld; 578 | sourceTree = ""; 579 | versionGroupType = wrapper.xcdatamodel; 580 | }; 581 | /* End XCVersionGroup section */ 582 | }; 583 | rootObject = 0263015D2607AAA200E216EB /* Project object */; 584 | } 585 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning.xcodeproj/xcuserdata/nicksarno.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SwiftfulThinkingContinuedLearning.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/AccessibilityColorsBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessibilityColorsBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 6/20/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AccessibilityColorsBootcamp: View { 11 | 12 | @Environment(\.accessibilityReduceTransparency) var reduceTransparency 13 | @Environment(\.colorSchemeContrast) var colorSchemeContrast 14 | @Environment(\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor 15 | @Environment(\.accessibilityInvertColors) var invertColors 16 | 17 | var body: some View { 18 | NavigationStack { 19 | VStack { 20 | 21 | Button("Button 1") { 22 | 23 | } 24 | .foregroundColor(colorSchemeContrast == .increased ? .white : .primary) 25 | .buttonStyle(.borderedProminent) 26 | 27 | Button("Button 2") { 28 | 29 | } 30 | .foregroundColor(.primary) 31 | .buttonStyle(.borderedProminent) 32 | .tint(.orange) 33 | 34 | Button("Button 3") { 35 | 36 | } 37 | .foregroundColor(.white) 38 | .foregroundColor(.primary) 39 | .buttonStyle(.borderedProminent) 40 | .tint(.green) 41 | 42 | Button("Button 4") { 43 | 44 | } 45 | .foregroundColor(differentiateWithoutColor ? .white : .green) 46 | .buttonStyle(.borderedProminent) 47 | .tint(differentiateWithoutColor ? .black : .purple) 48 | } 49 | .font(.largeTitle) 50 | // .navigationTitle("Hi") 51 | .frame(maxWidth: .infinity, maxHeight: .infinity) 52 | .ignoresSafeArea() 53 | // .background(reduceTransparency ? Color.black : Color.black.opacity(0.5)) 54 | } 55 | } 56 | } 57 | 58 | struct AccessibilityColorsBootcamp_Previews: PreviewProvider { 59 | static var previews: some View { 60 | AccessibilityColorsBootcamp() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/AccessibilityTextBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessibilityTextBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 6/20/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // Dynamic Text 11 | 12 | struct AccessibilityTextBootcamp: View { 13 | 14 | @Environment(\.sizeCategory) var sizeCategory 15 | 16 | var body: some View { 17 | NavigationStack { 18 | List { 19 | ForEach(0..<10) { _ in 20 | VStack(alignment: .leading, spacing: 8) { 21 | HStack { 22 | Image(systemName: "heart.fill") 23 | // .font(.system(size: 20)) 24 | 25 | Text("Welcome to my app") 26 | .truncationMode(.tail) 27 | } 28 | .font(.title) 29 | 30 | Text("This is some longer text that expands to multiple lines.") 31 | .font(.subheadline) 32 | .frame(maxWidth: .infinity, alignment: .leading) 33 | .lineLimit(3) 34 | .minimumScaleFactor(sizeCategory.customMinScaleFactor) 35 | } 36 | // .frame(height: 100) 37 | .background(Color.red) 38 | } 39 | } 40 | .listStyle(PlainListStyle()) 41 | .navigationTitle("Hello, world!") 42 | } 43 | } 44 | } 45 | 46 | extension ContentSizeCategory { 47 | 48 | var customMinScaleFactor: CGFloat { 49 | switch self { 50 | case .extraSmall, .small, .medium: 51 | return 1.0 52 | case .large, .extraLarge, .extraExtraLarge: 53 | return 0.8 54 | default: 55 | return 0.85 56 | } 57 | } 58 | 59 | } 60 | 61 | 62 | struct AccessibilityTextBootcamp_Previews: PreviewProvider { 63 | static var previews: some View { 64 | AccessibilityTextBootcamp() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/AccessibilityVoiceOverBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessibilityVoiceOverBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 6/20/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AccessibilityVoiceOverBootcamp: View { 11 | 12 | @State private var isActive: Bool = false 13 | 14 | var body: some View { 15 | NavigationStack { 16 | Form { 17 | Section { 18 | Toggle("Volume", isOn: $isActive) 19 | 20 | HStack { 21 | Text("Volume") 22 | Spacer() 23 | 24 | Text(isActive ? "TRUE" : "FALSE") 25 | .accessibilityHidden(true) 26 | } 27 | .background(Color.black.opacity(0.001)) 28 | .onTapGesture { 29 | isActive.toggle() 30 | } 31 | .accessibilityElement(children: .combine) 32 | .accessibilityAddTraits(.isButton) 33 | .accessibilityValue(isActive ? "is on" : "is off") 34 | .accessibilityHint("Double tap to toggle setting.") 35 | .accessibilityAction { 36 | isActive.toggle() 37 | } 38 | 39 | } header: { 40 | Text("PREFERENCES") 41 | } 42 | 43 | Section { 44 | Button("Favorites") { 45 | 46 | } 47 | .accessibilityRemoveTraits(.isButton) 48 | 49 | Button { 50 | 51 | } label: { 52 | Image(systemName: "heart.fill") 53 | } 54 | .accessibilityLabel("Favorites") 55 | 56 | Text("Favorites") 57 | .accessibilityAddTraits(.isButton) 58 | .onTapGesture { 59 | 60 | } 61 | 62 | } header: { 63 | Text("APPLICATION") 64 | } 65 | 66 | VStack { 67 | Text("CONTENT") 68 | .frame(maxWidth: .infinity, alignment: .leading) 69 | .foregroundColor(.secondary) 70 | .font(.caption) 71 | .accessibilityAddTraits(.isHeader) 72 | 73 | ScrollView(.horizontal, showsIndicators: false) { 74 | HStack(spacing: 8) { 75 | 76 | ForEach(0..<10) { x in 77 | VStack { 78 | Image("steve-jobs") 79 | .resizable() 80 | .scaledToFill() 81 | .frame(width: 100, height: 100) 82 | .cornerRadius(10) 83 | 84 | Text("Item \(x)") 85 | } 86 | .onTapGesture { 87 | 88 | } 89 | .accessibilityElement(children: .combine) 90 | .accessibilityAddTraits(.isButton) 91 | .accessibilityLabel("Item \(x). Image of Steve Jobs.") 92 | .accessibilityHint("Double tap to open.") 93 | .accessibilityAction { 94 | 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | } 102 | .navigationTitle("Settings") 103 | } 104 | } 105 | } 106 | 107 | struct AccessibilityVoiceOverBootcamp_Previews: PreviewProvider { 108 | static var previews: some View { 109 | AccessibilityVoiceOverBootcamp() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/AlignmentGuideBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlignmentGuideBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 9/18/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // https://swiftui-lab.com/alignment-guides/ 11 | 12 | struct AlignmentGuideBootcamp: View { 13 | var body: some View { 14 | VStack(alignment: .leading) { 15 | Text("Hello!") 16 | .background(Color.blue) 17 | .alignmentGuide(.leading) { dimensions in 18 | return dimensions.width * 0.5 19 | } 20 | 21 | Text("This is some other text!") 22 | .background(Color.red) 23 | } 24 | .background(Color.orange) 25 | } 26 | } 27 | 28 | struct AlignmentChildView: View { 29 | var body: some View { 30 | VStack(alignment: .leading, spacing: 20) { 31 | row(title: "Row 1", showIcon: false) 32 | row(title: "Row 2", showIcon: true) 33 | row(title: "Row 3", showIcon: false) 34 | } 35 | .padding(16) 36 | .background(Color.white) 37 | .cornerRadius(10) 38 | .shadow(radius: 10) 39 | .padding(40) 40 | } 41 | 42 | private func row(title: String, showIcon: Bool) -> some View { 43 | HStack(spacing: 10) { 44 | if showIcon { 45 | Image(systemName: "info.circle") 46 | .frame(width: 30, height: 30) 47 | } 48 | 49 | Text(title) 50 | 51 | Spacer() 52 | } 53 | .alignmentGuide(.leading) { dimensions in 54 | return showIcon ? 40 : 0 55 | } 56 | } 57 | } 58 | 59 | struct AlignmentGuideBootcamp_Previews: PreviewProvider { 60 | static var previews: some View { 61 | AlignmentChildView() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/ArraysBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArraysBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/2/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UserModel: Identifiable { 11 | let id = UUID().uuidString 12 | let name: String? 13 | let points: Int 14 | let isVerified: Bool 15 | } 16 | 17 | class ArrayModificationViewModel: ObservableObject { 18 | 19 | @Published var dataArray: [UserModel] = [] 20 | @Published var filteredArray: [UserModel] = [] 21 | @Published var mappedArray: [String] = [] 22 | 23 | init() { 24 | getUsers() 25 | updateFilteredArray() 26 | } 27 | 28 | func updateFilteredArray() { 29 | 30 | // sort 31 | /* 32 | // filteredArray = dataArray.sorted { (user1, user2) -> Bool in 33 | // return user1.points > user2.points 34 | // } 35 | 36 | // filteredArray = dataArray.sorted(by: { $0.points < $1.points }) 37 | */ 38 | 39 | // filter 40 | /* 41 | // filteredArray = dataArray.filter({ (user) -> Bool in 42 | // return user.isVerified 43 | // }) 44 | 45 | filteredArray = dataArray.filter({ $0.isVerified }) 46 | */ 47 | 48 | // map 49 | /* 50 | // mappedArray = dataArray.map({ (user) -> String in 51 | // return user.name ?? "ERROR" 52 | // }) 53 | 54 | // mappedArray = dataArray.map({ $0.name }) 55 | 56 | // mappedArray = dataArray.compactMap({ (user) -> String? in 57 | // return user.name 58 | // }) 59 | 60 | // mappedArray = dataArray.compactMap({ $0.name }) 61 | */ 62 | 63 | mappedArray = dataArray 64 | .sorted(by: { $0.points > $1.points }) 65 | .filter({ $0.isVerified }) 66 | .compactMap({ $0.name }) 67 | 68 | } 69 | 70 | func getUsers() { 71 | let user1 = UserModel(name: "Nick", points: 5, isVerified: true) 72 | let user2 = UserModel(name: "Chris", points: 0, isVerified: false) 73 | let user3 = UserModel(name: nil, points: 20, isVerified: true) 74 | let user4 = UserModel(name: "Emily", points: 50, isVerified: false) 75 | let user5 = UserModel(name: "Samantha", points: 45, isVerified: true) 76 | let user6 = UserModel(name: "Jason", points: 23, isVerified: false) 77 | let user7 = UserModel(name: "Sarah", points: 76, isVerified: true) 78 | let user8 = UserModel(name: nil, points: 45, isVerified: false) 79 | let user9 = UserModel(name: "Steve", points: 1, isVerified: true) 80 | let user10 = UserModel(name: "Amanda", points: 100, isVerified: false) 81 | self.dataArray.append(contentsOf: [ 82 | user1, 83 | user2, 84 | user3, 85 | user4, 86 | user5, 87 | user6, 88 | user7, 89 | user8, 90 | user9, 91 | user10, 92 | ]) 93 | } 94 | 95 | } 96 | 97 | struct ArraysBootcamp: View { 98 | 99 | @StateObject var vm = ArrayModificationViewModel() 100 | 101 | var body: some View { 102 | ScrollView { 103 | VStack(spacing: 10) { 104 | ForEach(vm.mappedArray, id: \.self) { name in 105 | Text(name) 106 | .font(.title) 107 | } 108 | // ForEach(vm.filteredArray) { user in 109 | // VStack(alignment: .leading) { 110 | // Text(user.name) 111 | // .font(.headline) 112 | // HStack { 113 | // Text("Points: \(user.points)") 114 | // Spacer() 115 | // if user.isVerified { 116 | // Image(systemName: "flame.fill") 117 | // } 118 | // } 119 | // } 120 | // .foregroundColor(.white) 121 | // .padding() 122 | // .background(Color.blue.cornerRadius(10)) 123 | // .padding(.horizontal) 124 | // } 125 | } 126 | } 127 | } 128 | } 129 | 130 | struct ArraysBootcamp_Previews: PreviewProvider { 131 | static var previews: some View { 132 | ArraysBootcamp() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/Assets.xcassets/steve-jobs.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "steve-jobs.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/Assets.xcassets/steve-jobs.imageset/steve-jobs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftfulThinking/SwiftUI-Continued-Learning/cce4f6bb3be15362933ea118edbdbf4acd0c87bf/SwiftfulThinkingContinuedLearning/Assets.xcassets/steve-jobs.imageset/steve-jobs.jpg -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/BackgroundThreadBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundThreadBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/9/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class BackgroundThreadViewModel: ObservableObject { 11 | 12 | @Published var dataArray: [String] = [] 13 | 14 | 15 | func fetchData() { 16 | 17 | DispatchQueue.global(qos: .background).async { 18 | let newData = self.downloadData() 19 | 20 | print("CHECK 1: \(Thread.isMainThread)") 21 | print("CHECK 1: \(Thread.current)") 22 | 23 | DispatchQueue.main.async { 24 | self.dataArray = newData 25 | print("CHECK 2: \(Thread.isMainThread)") 26 | print("CHECK 2: \(Thread.current)") 27 | } 28 | 29 | } 30 | 31 | } 32 | 33 | private func downloadData() -> [String] { 34 | var data: [String] = [] 35 | for x in 0..<100 { 36 | data.append("\(x)") 37 | print(data) 38 | } 39 | return data 40 | } 41 | 42 | 43 | } 44 | 45 | struct BackgroundThreadBootcamp: View { 46 | 47 | @StateObject var vm = BackgroundThreadViewModel() 48 | 49 | var body: some View { 50 | ScrollView { 51 | LazyVStack(spacing: 10) { 52 | Text("LOAD DATA") 53 | .font(.largeTitle) 54 | .fontWeight(.semibold) 55 | .onTapGesture { 56 | vm.fetchData() 57 | } 58 | 59 | ForEach(vm.dataArray, id: \.self) { item in 60 | Text(item) 61 | .font(.headline) 62 | .foregroundColor(.red) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | struct BackgroundThreadBootcamp_Previews: PreviewProvider { 70 | static var previews: some View { 71 | BackgroundThreadBootcamp() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/CacheBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/27/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class CacheManager { 11 | 12 | static let instance = CacheManager() // Singleton 13 | private init() { } 14 | 15 | var imageCache: NSCache = { 16 | let cache = NSCache() 17 | cache.countLimit = 100 18 | cache.totalCostLimit = 1024 * 1024 * 100 // 100mb 19 | return cache 20 | }() 21 | 22 | func add(image: UIImage, name: String) -> String { 23 | imageCache.setObject(image, forKey: name as NSString) 24 | return "Added to cache!" 25 | } 26 | 27 | func remove(name: String) -> String { 28 | imageCache.removeObject(forKey: name as NSString) 29 | return "Removed from cache!" 30 | } 31 | 32 | func get(name: String) -> UIImage? { 33 | return imageCache.object(forKey: name as NSString) 34 | } 35 | 36 | } 37 | 38 | class CacheViewModel: ObservableObject { 39 | 40 | @Published var startingImage: UIImage? = nil 41 | @Published var cachedImage: UIImage? = nil 42 | @Published var infoMessage: String = "" 43 | let imageName: String = "steve-jobs" 44 | let manager = CacheManager.instance 45 | 46 | init() { 47 | getImageFromAssetsFolder() 48 | } 49 | 50 | func getImageFromAssetsFolder() { 51 | startingImage = UIImage(named: imageName) 52 | } 53 | 54 | func saveToCache() { 55 | guard let image = startingImage else { return } 56 | infoMessage = manager.add(image: image, name: imageName) 57 | } 58 | 59 | func removeFromCache() { 60 | infoMessage = manager.remove(name: imageName) 61 | } 62 | 63 | func getFromCache() { 64 | if let returnedImage = manager.get(name: imageName) { 65 | cachedImage = returnedImage 66 | infoMessage = "Got image from Cache" 67 | } else { 68 | infoMessage = "Image not found in Cache" 69 | } 70 | } 71 | 72 | } 73 | 74 | struct CacheBootcamp: View { 75 | 76 | @StateObject var vm = CacheViewModel() 77 | 78 | var body: some View { 79 | NavigationView { 80 | VStack { 81 | if let image = vm.startingImage { 82 | Image(uiImage: image) 83 | .resizable() 84 | .scaledToFill() 85 | .frame(width: 200, height: 200) 86 | .clipped() 87 | .cornerRadius(10) 88 | } 89 | 90 | Text(vm.infoMessage) 91 | .font(.headline) 92 | .foregroundColor(.purple) 93 | 94 | HStack { 95 | Button(action: { 96 | vm.saveToCache() 97 | }, label: { 98 | Text("Save to Cache") 99 | .font(.headline) 100 | .foregroundColor(.white) 101 | .padding() 102 | .background(Color.blue) 103 | .cornerRadius(10) 104 | }) 105 | Button(action: { 106 | vm.removeFromCache() 107 | }, label: { 108 | Text("Delete from Cache") 109 | .font(.headline) 110 | .foregroundColor(.white) 111 | .padding() 112 | .background(Color.red) 113 | .cornerRadius(10) 114 | }) 115 | } 116 | 117 | Button(action: { 118 | vm.getFromCache() 119 | }, label: { 120 | Text("Get from Cache") 121 | .font(.headline) 122 | .foregroundColor(.white) 123 | .padding() 124 | .background(Color.green) 125 | .cornerRadius(10) 126 | }) 127 | 128 | if let image = vm.cachedImage { 129 | Image(uiImage: image) 130 | .resizable() 131 | .scaledToFill() 132 | .frame(width: 200, height: 200) 133 | .clipped() 134 | .cornerRadius(10) 135 | } 136 | 137 | Spacer() 138 | } 139 | .navigationTitle("Cache Bootcamp") 140 | } 141 | } 142 | } 143 | 144 | struct CacheBootcamp_Previews: PreviewProvider { 145 | static var previews: some View { 146 | CacheBootcamp() 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/CodableBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/10/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // Codable = Decodable + Encodable 11 | 12 | struct CustomerModel: Identifiable, Codable { 13 | let id: String 14 | let name: String 15 | let points: Int 16 | let isPremium: Bool 17 | 18 | // enum CodingKeys: String, CodingKey { 19 | // case id 20 | // case name 21 | // case points 22 | // case isPremium 23 | // } 24 | // 25 | // init(id: String, name: String, points: Int, isPremium: Bool) { 26 | // self.id = id 27 | // self.name = name 28 | // self.points = points 29 | // self.isPremium = isPremium 30 | // } 31 | // 32 | // init(from decoder: Decoder) throws { 33 | // let container = try decoder.container(keyedBy: CodingKeys.self) 34 | // self.id = try container.decode(String.self, forKey: .id) 35 | // self.name = try container.decode(String.self, forKey: .name) 36 | // self.points = try container.decode(Int.self, forKey: .points) 37 | // self.isPremium = try container.decode(Bool.self, forKey: .isPremium) 38 | // } 39 | // 40 | // func encode(to encoder: Encoder) throws { 41 | // var container = encoder.container(keyedBy: CodingKeys.self) 42 | // try container.encode(id, forKey: .id) 43 | // try container.encode(name, forKey: .name) 44 | // try container.encode(points, forKey: .points) 45 | // try container.encode(isPremium, forKey: .isPremium) 46 | // } 47 | 48 | } 49 | 50 | class CodableViewModel: ObservableObject { 51 | 52 | @Published var customer: CustomerModel? = nil 53 | 54 | init() { 55 | getData() 56 | } 57 | 58 | func getData() { 59 | 60 | guard let data = getJSONData() else { return } 61 | self.customer = try? JSONDecoder().decode(CustomerModel.self, from: data) 62 | 63 | // do { 64 | // self.customer = try JSONDecoder().decode(CustomerModel.self, from: data) 65 | // } catch let error { 66 | // print("Error decoding. \(error)") 67 | // } 68 | 69 | // if 70 | // let localData = try? JSONSerialization.jsonObject(with: data, options: []), 71 | // let dictionary = localData as? [String:Any], 72 | // let id = dictionary["id"] as? String, 73 | // let name = dictionary["name"] as? String, 74 | // let points = dictionary["points"] as? Int, 75 | // let isPremium = dictionary["isPremium"] as? Bool { 76 | // 77 | // let newCustomer = CustomerModel(id: id, name: name, points: points, isPremium: isPremium) 78 | // customer = newCustomer 79 | // } 80 | } 81 | 82 | func getJSONData() -> Data? { 83 | 84 | let customer = CustomerModel(id: "111", name: "Emily", points: 100, isPremium: false) 85 | let jsonData = try? JSONEncoder().encode(customer) 86 | // let dictionary: [String:Any] = [ 87 | // "id" : "12345", 88 | // "name" : "Joe", 89 | // "points" : 5, 90 | // "isPremium" : true 91 | // ] 92 | // let jsonData = try? JSONSerialization.data(withJSONObject: dictionary, options: []) 93 | return jsonData 94 | } 95 | 96 | } 97 | 98 | struct CodableBootcamp: View { 99 | 100 | @StateObject var vm = CodableViewModel() 101 | 102 | var body: some View { 103 | VStack(spacing: 20) { 104 | if let customer = vm.customer { 105 | Text(customer.id) 106 | Text(customer.name) 107 | Text("\(customer.points)") 108 | Text(customer.isPremium.description) 109 | } 110 | } 111 | } 112 | } 113 | 114 | struct CodableBootcamp_Previews: PreviewProvider { 115 | static var previews: some View { 116 | CodableBootcamp() 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/CoreDataBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/6/21. 6 | // 7 | 8 | import SwiftUI 9 | import CoreData 10 | 11 | // View - UI 12 | // Model - data point 13 | // ViewModel - manages the data for a view 14 | 15 | class CoreDataViewModel: ObservableObject { 16 | 17 | let container: NSPersistentContainer 18 | @Published var savedEntities: [FruitEntity] = [] 19 | 20 | init() { 21 | container = NSPersistentContainer(name: "FruitsContainer") 22 | container.loadPersistentStores { (description, error) in 23 | if let error = error { 24 | print("ERROR LOADING CORE DATA. \(error)") 25 | } 26 | } 27 | fetchFruits() 28 | } 29 | 30 | func fetchFruits() { 31 | let request = NSFetchRequest(entityName: "FruitEntity") 32 | 33 | do { 34 | savedEntities = try container.viewContext.fetch(request) 35 | } catch let error { 36 | print("Error fetching. \(error)") 37 | } 38 | } 39 | 40 | func addFruit(text: String) { 41 | let newFruit = FruitEntity(context: container.viewContext) 42 | newFruit.name = text 43 | saveData() 44 | } 45 | 46 | func updateFruit(entity: FruitEntity) { 47 | let currentName = entity.name ?? "" 48 | let newName = currentName + "!" 49 | entity.name = newName 50 | saveData() 51 | } 52 | 53 | func deleteFruit(indexSet: IndexSet) { 54 | guard let index = indexSet.first else { return } 55 | let entity = savedEntities[index] 56 | container.viewContext.delete(entity) 57 | saveData() 58 | } 59 | 60 | func saveData() { 61 | do { 62 | try container.viewContext.save() 63 | fetchFruits() 64 | } catch let error { 65 | print("Error saving. \(error)") 66 | } 67 | } 68 | 69 | } 70 | 71 | struct CoreDataBootcamp: View { 72 | 73 | @StateObject var vm = CoreDataViewModel() 74 | @State var textFieldText: String = "" 75 | 76 | var body: some View { 77 | NavigationView { 78 | VStack(spacing: 20) { 79 | TextField("Add fruit here...", text: $textFieldText) 80 | .font(.headline) 81 | .padding(.leading) 82 | .frame(height: 55) 83 | .background(Color(#colorLiteral(red: 0.921431005, green: 0.9214526415, blue: 0.9214410186, alpha: 1))) 84 | .cornerRadius(10) 85 | .padding(.horizontal) 86 | 87 | Button(action: { 88 | guard !textFieldText.isEmpty else { return } 89 | vm.addFruit(text: textFieldText) 90 | textFieldText = "" 91 | }, label: { 92 | Text("Save") 93 | .font(.headline) 94 | .foregroundColor(.white) 95 | .frame(height: 55) 96 | .frame(maxWidth: .infinity) 97 | .background(Color(#colorLiteral(red: 1, green: 0.1857388616, blue: 0.5733950138, alpha: 1))) 98 | .cornerRadius(10) 99 | }) 100 | .padding(.horizontal) 101 | 102 | List { 103 | ForEach(vm.savedEntities) { entity in 104 | Text(entity.name ?? "NO NAME") 105 | .onTapGesture { 106 | vm.updateFruit(entity: entity) 107 | } 108 | } 109 | .onDelete(perform: vm.deleteFruit) 110 | } 111 | .listStyle(PlainListStyle()) 112 | } 113 | .navigationTitle("Fruits") 114 | } 115 | } 116 | } 117 | 118 | struct CoreDataBootcamp_Previews: PreviewProvider { 119 | static var previews: some View { 120 | CoreDataBootcamp() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/CoreDataContainer.xcdatamodeld/CoreDataContainer.xcdatamodel/contents: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/CoreDataRelationshipsBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataRelationshipsBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/7/21. 6 | // 7 | 8 | import SwiftUI 9 | import CoreData 10 | 11 | // 3 entities 12 | // BusinessEntity 13 | // DepartmentEntity 14 | // EmployeeEntity 15 | 16 | class CoreDataManager { 17 | 18 | static let instance = CoreDataManager() // Singleton 19 | let container: NSPersistentContainer 20 | let context: NSManagedObjectContext 21 | 22 | init() { 23 | container = NSPersistentContainer(name: "CoreDataContainer") 24 | container.loadPersistentStores { (description, error) in 25 | if let error = error { 26 | print("Error loading Core Data. \(error)") 27 | } 28 | } 29 | context = container.viewContext 30 | } 31 | 32 | func save() { 33 | do { 34 | try context.save() 35 | print("Saved successfully!") 36 | } catch let error { 37 | print("Error saving Core Data. \(error.localizedDescription)") 38 | } 39 | } 40 | 41 | } 42 | 43 | class CoreDataRelationshipViewModel: ObservableObject { 44 | 45 | let manager = CoreDataManager.instance 46 | @Published var businesses: [BusinessEntity] = [] 47 | @Published var departments: [DepartmentEntity] = [] 48 | @Published var employees: [EmployeeEntity] = [] 49 | 50 | init() { 51 | getBusinesses() 52 | getDepartments() 53 | getEmployees() 54 | } 55 | 56 | func getBusinesses() { 57 | let request = NSFetchRequest(entityName: "BusinessEntity") 58 | 59 | let sort = NSSortDescriptor(keyPath: \BusinessEntity.name, ascending: true) 60 | request.sortDescriptors = [sort] 61 | 62 | //let filter = NSPredicate(format: "name == %@", "Apple") 63 | //request.predicate = filter 64 | 65 | do { 66 | businesses = try manager.context.fetch(request) 67 | } catch let error { 68 | print("Error fetching. \(error.localizedDescription)") 69 | } 70 | } 71 | 72 | func getDepartments() { 73 | let request = NSFetchRequest(entityName: "DepartmentEntity") 74 | 75 | do { 76 | departments = try manager.context.fetch(request) 77 | } catch let error { 78 | print("Error fetching. \(error.localizedDescription)") 79 | } 80 | } 81 | 82 | func getEmployees() { 83 | let request = NSFetchRequest(entityName: "EmployeeEntity") 84 | 85 | do { 86 | employees = try manager.context.fetch(request) 87 | } catch let error { 88 | print("Error fetching. \(error.localizedDescription)") 89 | } 90 | } 91 | 92 | func getEmployees(forBusiness business: BusinessEntity) { 93 | let request = NSFetchRequest(entityName: "EmployeeEntity") 94 | 95 | let filter = NSPredicate(format: "business == %@", business) 96 | request.predicate = filter 97 | 98 | do { 99 | employees = try manager.context.fetch(request) 100 | } catch let error { 101 | print("Error fetching. \(error.localizedDescription)") 102 | } 103 | } 104 | 105 | func updateBusiness() { 106 | 107 | let existingBusiness = businesses[2] 108 | existingBusiness.addToDepartments(departments[1]) 109 | save() 110 | 111 | } 112 | 113 | func addBusiness() { 114 | let newBusiness = BusinessEntity(context: manager.context) 115 | newBusiness.name = "Facebook" 116 | 117 | // add existing departments to the new business 118 | //newBusiness.departments = [departments[0], departments[1]] 119 | newBusiness.departments = [departments[0]] 120 | // add existing employees to the new business 121 | //newBusiness.employees = [employees[1]] 122 | 123 | // add new business to existing department 124 | //newBusiness.addToDepartments(<#T##value: DepartmentEntity##DepartmentEntity#>) 125 | 126 | // add new business to existing employee 127 | //newBusiness.addToEmployees(<#T##value: EmployeeEntity##EmployeeEntity#>) 128 | 129 | save() 130 | } 131 | 132 | func addDepartment() { 133 | let newDepartment = DepartmentEntity(context: manager.context) 134 | newDepartment.name = "Finance" 135 | newDepartment.businesses = [businesses[0], businesses[1], businesses[2]] 136 | newDepartment.addToEmployees(employees[1]) 137 | 138 | //newDepartment.employees = [employees[1]] 139 | //newDepartment.addToEmployees(employees[1]) 140 | 141 | save() 142 | } 143 | 144 | func addEmployee() { 145 | let newEmployee = EmployeeEntity(context: manager.context) 146 | newEmployee.age = 21 147 | newEmployee.dateJoined = Date() 148 | newEmployee.name = "John" 149 | 150 | newEmployee.business = businesses[2] 151 | newEmployee.department = departments[1] 152 | save() 153 | } 154 | 155 | func deleteDepartment() { 156 | let department = departments[1] 157 | manager.context.delete(department) 158 | save() 159 | } 160 | 161 | func save() { 162 | businesses.removeAll() 163 | departments.removeAll() 164 | employees.removeAll() 165 | 166 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { 167 | self.manager.save() 168 | self.getBusinesses() 169 | self.getDepartments() 170 | self.getEmployees() 171 | } 172 | } 173 | 174 | } 175 | 176 | struct CoreDataRelationshipsBootcamp: View { 177 | 178 | @StateObject var vm = CoreDataRelationshipViewModel() 179 | 180 | var body: some View { 181 | NavigationView { 182 | ScrollView { 183 | VStack(spacing: 20) { 184 | Button(action: { 185 | vm.deleteDepartment() 186 | }, label: { 187 | Text("Perform Action") 188 | .foregroundColor(.white) 189 | .frame(height: 55) 190 | .frame(maxWidth: .infinity) 191 | .background(Color.blue.cornerRadius(10)) 192 | }) 193 | 194 | ScrollView(.horizontal, showsIndicators: true, content: { 195 | HStack(alignment: .top) { 196 | ForEach(vm.businesses) { business in 197 | BusinessView(entity: business) 198 | } 199 | } 200 | }) 201 | 202 | ScrollView(.horizontal, showsIndicators: true, content: { 203 | HStack(alignment: .top) { 204 | ForEach(vm.departments) { department in 205 | DepartmentView(entity: department) 206 | } 207 | } 208 | }) 209 | 210 | ScrollView(.horizontal, showsIndicators: true, content: { 211 | HStack(alignment: .top) { 212 | ForEach(vm.employees) { employee in 213 | EmployeeView(entity: employee) 214 | } 215 | } 216 | }) 217 | } 218 | .padding() 219 | } 220 | .navigationTitle("Relationships") 221 | } 222 | } 223 | } 224 | 225 | struct CoreDataRelationshipsBootcamp_Previews: PreviewProvider { 226 | static var previews: some View { 227 | CoreDataRelationshipsBootcamp() 228 | } 229 | } 230 | 231 | struct BusinessView: View { 232 | 233 | let entity: BusinessEntity 234 | 235 | var body: some View { 236 | VStack(alignment: .leading, spacing: 20, content: { 237 | Text("Name: \(entity.name ?? "")") 238 | .bold() 239 | 240 | if let departments = entity.departments?.allObjects as? [DepartmentEntity] { 241 | Text("Departments:") 242 | .bold() 243 | ForEach(departments) { department in 244 | Text(department.name ?? "") 245 | } 246 | } 247 | if let employees = entity.employees?.allObjects as? [EmployeeEntity] { 248 | Text("Employees:") 249 | .bold() 250 | ForEach(employees) { employee in 251 | Text(employee.name ?? "") 252 | } 253 | } 254 | }) 255 | .padding() 256 | .frame(maxWidth: 300, alignment: .leading) 257 | .background(Color.gray.opacity(0.5)) 258 | .cornerRadius(10) 259 | .shadow(radius: 10) 260 | } 261 | } 262 | 263 | struct DepartmentView: View { 264 | 265 | let entity: DepartmentEntity 266 | 267 | var body: some View { 268 | VStack(alignment: .leading, spacing: 20, content: { 269 | Text("Name: \(entity.name ?? "")") 270 | .bold() 271 | 272 | if let businesses = entity.businesses?.allObjects as? [BusinessEntity] { 273 | Text("Businesses:") 274 | .bold() 275 | ForEach(businesses) { business in 276 | Text(business.name ?? "") 277 | } 278 | } 279 | if let employees = entity.employees?.allObjects as? [EmployeeEntity] { 280 | Text("Employees:") 281 | .bold() 282 | ForEach(employees) { employee in 283 | Text(employee.name ?? "") 284 | } 285 | } 286 | }) 287 | .padding() 288 | .frame(maxWidth: 300, alignment: .leading) 289 | .background(Color.green.opacity(0.5)) 290 | .cornerRadius(10) 291 | .shadow(radius: 10) 292 | } 293 | } 294 | 295 | struct EmployeeView: View { 296 | 297 | let entity: EmployeeEntity 298 | 299 | var body: some View { 300 | VStack(alignment: .leading, spacing: 20, content: { 301 | Text("Name: \(entity.name ?? "")") 302 | .bold() 303 | 304 | Text("Age: \(entity.age)") 305 | Text("Date joined: \(entity.dateJoined ?? Date())") 306 | 307 | Text("Business:") 308 | .bold() 309 | 310 | Text(entity.business?.name ?? "") 311 | 312 | Text("Department:") 313 | .bold() 314 | 315 | Text(entity.department?.name ?? "") 316 | }) 317 | .padding() 318 | .frame(maxWidth: 300, alignment: .leading) 319 | .background(Color.blue.opacity(0.5)) 320 | .cornerRadius(10) 321 | .shadow(radius: 10) 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/DownloadWithCombine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadWithCombine.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/10/21. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | struct PostModel: Identifiable, Codable { 12 | let userId: Int 13 | let id: Int 14 | let title: String 15 | let body: String 16 | } 17 | 18 | class DownloadWithCombineViewModel: ObservableObject { 19 | 20 | @Published var posts: [PostModel] = [] 21 | var cancellables = Set() 22 | 23 | init() { 24 | getPosts() 25 | } 26 | 27 | func getPosts() { 28 | guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return } 29 | 30 | // Combine discussion: 31 | /* 32 | // 1. sign up for monthly subscription for package to be delivered 33 | // 2. the company would make the package behind the scene 34 | // 3. recieve the package at your front door 35 | // 4. make sure the box isn't damaged 36 | // 5. open and make sure the item is correct 37 | // 6. use the item!!!! 38 | // 7. cancellable at any time!! 39 | 40 | // 1. create the publisher 41 | // 2. subscribe publisher on background thread 42 | // 3. recieve on main thread 43 | // 4. tryMap (check that the data is good) 44 | // 5. decode (decode data into PostModels) 45 | // 6. sink (put the item into our app) 46 | // 7. store (cancel subscription if needed) 47 | */ 48 | 49 | URLSession.shared.dataTaskPublisher(for: url) 50 | //.subscribe(on: DispatchQueue.global(qos: .background)) 51 | .receive(on: DispatchQueue.main) 52 | .tryMap(handleOutput) 53 | .decode(type: [PostModel].self, decoder: JSONDecoder()) 54 | .replaceError(with: []) 55 | .sink(receiveValue: { [weak self] (returnedPosts) in 56 | self?.posts = returnedPosts 57 | }) 58 | .store(in: &cancellables) 59 | } 60 | 61 | func handleOutput(output: URLSession.DataTaskPublisher.Output) throws -> Data { 62 | guard 63 | let response = output.response as? HTTPURLResponse, 64 | response.statusCode >= 200 && response.statusCode < 300 else { 65 | throw URLError(.badServerResponse) 66 | } 67 | return output.data 68 | } 69 | 70 | } 71 | 72 | struct DownloadWithCombine: View { 73 | 74 | @StateObject var vm = DownloadWithCombineViewModel() 75 | 76 | var body: some View { 77 | List { 78 | ForEach(vm.posts) { post in 79 | VStack(alignment: .leading) { 80 | Text(post.title) 81 | .font(.headline) 82 | Text(post.body) 83 | .foregroundColor(.gray) 84 | } 85 | .frame(maxWidth: .infinity, alignment: .leading) 86 | } 87 | } 88 | } 89 | } 90 | 91 | struct DownloadWithCombine_Previews: PreviewProvider { 92 | static var previews: some View { 93 | DownloadWithCombine() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/DownloadWithEscapingBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadWithEscapingBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/10/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | //struct PostModel: Identifiable, Codable { 11 | // let userId: Int 12 | // let id: Int 13 | // let title: String 14 | // let body: String 15 | //} 16 | 17 | class DownloadWithEscapingViewModel: ObservableObject { 18 | 19 | @Published var posts: [PostModel] = [] 20 | 21 | init() { 22 | getPosts() 23 | } 24 | 25 | func getPosts() { 26 | 27 | guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return } 28 | 29 | downloadData(fromURL: url) { (returnedData) in 30 | if let data = returnedData { 31 | guard let newPosts = try? JSONDecoder().decode([PostModel].self, from: data) else { return } 32 | DispatchQueue.main.async { [weak self] in 33 | self?.posts = newPosts 34 | } 35 | } else { 36 | print("No data returned.") 37 | } 38 | } 39 | } 40 | 41 | func downloadData(fromURL url: URL, completionHandler: @escaping (_ data: Data?) -> ()) { 42 | 43 | URLSession.shared.dataTask(with: url) { (data, response, error) in 44 | guard 45 | let data = data, 46 | error == nil, 47 | let response = response as? HTTPURLResponse, 48 | response.statusCode >= 200 && response.statusCode < 300 else { 49 | print("Error downloading data.") 50 | completionHandler(nil) 51 | return 52 | } 53 | 54 | completionHandler(data) 55 | 56 | }.resume() 57 | } 58 | 59 | } 60 | 61 | struct DownloadWithEscapingBootcamp: View { 62 | 63 | @StateObject var vm = DownloadWithEscapingViewModel() 64 | 65 | var body: some View { 66 | List { 67 | ForEach(vm.posts) { post in 68 | VStack(alignment: .leading) { 69 | Text(post.title) 70 | .font(.headline) 71 | Text(post.body) 72 | .foregroundColor(.gray) 73 | } 74 | .frame(maxWidth: .infinity, alignment: .leading) 75 | } 76 | } 77 | } 78 | } 79 | 80 | struct DownloadWithEscapingBootcamp_Previews: PreviewProvider { 81 | static var previews: some View { 82 | DownloadWithEscapingBootcamp() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/DownloadingImages/Models/PhotoModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoModel.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/28/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct PhotoModel: Identifiable, Codable { 11 | let albumId: Int 12 | let id: Int 13 | let title: String 14 | let url: String 15 | let thumbnailUrl: String 16 | } 17 | 18 | /* 19 | 20 | { 21 | "albumId": 1, 22 | "id": 1, 23 | "title": "accusamus beatae ad facilis cum similique qui sunt", 24 | "url": "https://via.placeholder.com/600/92c952", 25 | "thumbnailUrl": "https://via.placeholder.com/150/92c952" 26 | } 27 | 28 | */ 29 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/DownloadingImages/Utilities/PhotoModelCacheManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoModelCacheManager.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/28/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | class PhotoModelCacheManager { 12 | 13 | static let instance = PhotoModelCacheManager() 14 | private init() { } 15 | 16 | var photoCache: NSCache = { 17 | var cache = NSCache() 18 | cache.countLimit = 200 19 | cache.totalCostLimit = 1024 * 1024 * 200 // 200mb 20 | return cache 21 | }() 22 | 23 | func add(key: String, value: UIImage) { 24 | photoCache.setObject(value, forKey: key as NSString) 25 | } 26 | 27 | func get(key: String) -> UIImage? { 28 | return photoCache.object(forKey: key as NSString) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/DownloadingImages/Utilities/PhotoModelDataService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoModelDataService.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/28/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | class PhotoModelDataService { 12 | 13 | static let instance = PhotoModelDataService() // Singleton 14 | 15 | @Published var photoModels: [PhotoModel] = [] 16 | var cancellables = Set() 17 | 18 | private init() { 19 | downloadData() 20 | } 21 | 22 | func downloadData() { 23 | guard let url = URL(string: "https://jsonplaceholder.typicode.com/photos") else { return } 24 | 25 | URLSession.shared.dataTaskPublisher(for: url) 26 | .subscribe(on: DispatchQueue.global(qos: .background)) 27 | .receive(on: DispatchQueue.main) 28 | .tryMap(handleOutput) 29 | .decode(type: [PhotoModel].self, decoder: JSONDecoder()) 30 | .sink { (completion) in 31 | switch completion { 32 | case .finished: 33 | break 34 | case .failure(let error): 35 | print("Error downloading data. \(error)") 36 | } 37 | } receiveValue: { [weak self] (returnedPhotoModels) in 38 | self?.photoModels = returnedPhotoModels 39 | } 40 | .store(in: &cancellables) 41 | } 42 | 43 | private func handleOutput(output: URLSession.DataTaskPublisher.Output) throws -> Data { 44 | guard 45 | let response = output.response as? HTTPURLResponse, 46 | response.statusCode >= 200 && response.statusCode < 300 else { 47 | throw URLError(.badServerResponse) 48 | } 49 | return output.data 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/DownloadingImages/Utilities/PhotoModelFileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoModelFileManager.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/28/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | class PhotoModelFileManager { 12 | 13 | static let instance = PhotoModelFileManager() 14 | let folderName = "downloaded_photos" 15 | 16 | private init() { 17 | createFolderIfNeeded() 18 | } 19 | 20 | private func createFolderIfNeeded() { 21 | guard let url = getFolderPath() else { return } 22 | 23 | if !FileManager.default.fileExists(atPath: url.path) { 24 | do { 25 | try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) 26 | print("Created folder!") 27 | } catch let error { 28 | print("Error creating folder. \(error)") 29 | } 30 | } 31 | } 32 | 33 | private func getFolderPath() -> URL? { 34 | return FileManager 35 | .default 36 | .urls(for: .cachesDirectory, in: .userDomainMask) 37 | .first? 38 | .appendingPathComponent(folderName) 39 | } 40 | 41 | // ... /downloaded_photos/ 42 | // ... /downloaded_photos/image_name.png 43 | private func getImagePath(key: String) -> URL? { 44 | guard let folder = getFolderPath() else { 45 | return nil 46 | } 47 | return folder.appendingPathComponent(key + ".png") 48 | } 49 | 50 | func add(key: String, value: UIImage) { 51 | guard 52 | let data = value.pngData(), 53 | let url = getImagePath(key: key) else { return } 54 | 55 | do { 56 | try data.write(to: url) 57 | } catch let error { 58 | print("Error saving to file manager. \(error)") 59 | } 60 | } 61 | 62 | func get(key: String) -> UIImage? { 63 | guard 64 | let url = getImagePath(key: key), 65 | FileManager.default.fileExists(atPath: url.path) else { 66 | return nil 67 | } 68 | return UIImage(contentsOfFile: url.path) 69 | } 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/DownloadingImages/ViewModels/DownloadingImagesViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadingImagesViewModel.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/28/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | class DownloadingImagesViewModel: ObservableObject { 12 | 13 | @Published var dataArray: [PhotoModel] = [] 14 | var cancellables = Set() 15 | 16 | let dataService = PhotoModelDataService.instance 17 | 18 | init() { 19 | addSubscribers() 20 | } 21 | 22 | func addSubscribers() { 23 | dataService.$photoModels 24 | .sink { [weak self] (returnedPhotoModels) in 25 | self?.dataArray = returnedPhotoModels 26 | } 27 | .store(in: &cancellables) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/DownloadingImages/ViewModels/ImageLoadingViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageLoadingViewModel.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/28/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import Combine 11 | 12 | class ImageLoadingViewModel: ObservableObject { 13 | 14 | @Published var image: UIImage? = nil 15 | @Published var isLoading: Bool = false 16 | 17 | var cancellables = Set() 18 | let manager = PhotoModelFileManager.instance 19 | 20 | let urlString: String 21 | let imageKey: String 22 | 23 | init(url: String, key: String) { 24 | urlString = url 25 | imageKey = key 26 | getImage() 27 | } 28 | 29 | func getImage() { 30 | if let savedImage = manager.get(key: imageKey) { 31 | image = savedImage 32 | print("Getting saved image!") 33 | } else { 34 | downloadImage() 35 | print("Downloading image now!") 36 | } 37 | } 38 | 39 | func downloadImage() { 40 | isLoading = true 41 | guard let url = URL(string: urlString) else { 42 | isLoading = false 43 | return 44 | } 45 | 46 | URLSession.shared.dataTaskPublisher(for: url) 47 | .map { UIImage(data: $0.data) } 48 | .receive(on: DispatchQueue.main) 49 | .sink { [weak self] (_) in 50 | self?.isLoading = false 51 | } receiveValue: { [weak self] (returnedImage) in 52 | guard 53 | let self = self, 54 | let image = returnedImage else { return } 55 | 56 | self.image = image 57 | self.manager.add(key: self.imageKey, value: image) 58 | } 59 | .store(in: &cancellables) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/DownloadingImages/Views/DownloadingImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadingImageView.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/28/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DownloadingImageView: View { 11 | 12 | @StateObject var loader: ImageLoadingViewModel 13 | 14 | init(url: String, key: String) { 15 | _loader = StateObject(wrappedValue: ImageLoadingViewModel(url: url, key: key)) 16 | } 17 | 18 | var body: some View { 19 | ZStack { 20 | if loader.isLoading { 21 | ProgressView() 22 | } else if let image = loader.image { 23 | Image(uiImage: image) 24 | .resizable() 25 | .clipShape(Circle()) 26 | } 27 | } 28 | } 29 | } 30 | 31 | struct DownloadingImageView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | DownloadingImageView(url: "https://via.placeholder.com/600/92c952", key: "1") 34 | .frame(width: 75, height: 75) 35 | .previewLayout(.sizeThatFits) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/DownloadingImages/Views/DownloadingImagesBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadingImagesBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/28/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // Codable 11 | // background threads 12 | // weak self 13 | // Combine 14 | // Publishers and Subscribers 15 | // FileManager 16 | // NSCache 17 | 18 | struct DownloadingImagesBootcamp: View { 19 | 20 | @StateObject var vm = DownloadingImagesViewModel() 21 | 22 | var body: some View { 23 | NavigationView { 24 | List { 25 | ForEach(vm.dataArray) { model in 26 | DownloadingImagesRow(model: model) 27 | } 28 | } 29 | .navigationTitle("Downloading Images!") 30 | } 31 | } 32 | } 33 | 34 | struct DownloadingImagesBootcamp_Previews: PreviewProvider { 35 | static var previews: some View { 36 | DownloadingImagesBootcamp() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/DownloadingImages/Views/DownloadingImagesRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadingImagesRow.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/28/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DownloadingImagesRow: View { 11 | 12 | let model: PhotoModel 13 | 14 | var body: some View { 15 | HStack { 16 | DownloadingImageView(url: model.url, key: "\(model.id)") 17 | .frame(width: 75, height: 75) 18 | VStack(alignment: .leading) { 19 | Text(model.title) 20 | .font(.headline) 21 | Text(model.url) 22 | .foregroundColor(.gray) 23 | .italic() 24 | } 25 | .frame(maxWidth: .infinity, alignment: .leading) 26 | } 27 | } 28 | } 29 | 30 | struct DownloadingImagesRow_Previews: PreviewProvider { 31 | static var previews: some View { 32 | DownloadingImagesRow(model: PhotoModel(albumId: 1, id: 1, title: "Title", url: "https://via.placeholder.com/600/92c952", thumbnailUrl: "https://via.placeholder.com/600/92c952")) 33 | .padding() 34 | .previewLayout(.sizeThatFits) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/DragGestureBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DragGestureBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 3/21/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DragGestureBootcamp: View { 11 | 12 | @State var offset: CGSize = .zero 13 | 14 | var body: some View { 15 | ZStack { 16 | 17 | VStack { 18 | Text("\(offset.width)") 19 | Spacer() 20 | } 21 | 22 | RoundedRectangle(cornerRadius: 20) 23 | .frame(width: 300, height: 500) 24 | .offset(offset) 25 | .scaleEffect(getScaleAmount()) 26 | .rotationEffect(Angle(degrees: getRotationAmount())) 27 | .gesture( 28 | DragGesture() 29 | .onChanged { value in 30 | withAnimation(.spring()) { 31 | offset = value.translation 32 | } 33 | } 34 | .onEnded { value in 35 | withAnimation(.spring()) { 36 | offset = .zero 37 | } 38 | } 39 | ) 40 | } 41 | } 42 | 43 | func getScaleAmount() -> CGFloat { 44 | let max = UIScreen.main.bounds.width / 2 45 | let currentAmount = abs(offset.width) 46 | let percentage = currentAmount / max 47 | return 1.0 - min(percentage, 0.5) * 0.5 48 | } 49 | 50 | func getRotationAmount() -> Double { 51 | let max = UIScreen.main.bounds.width / 2 52 | let currentAmount = offset.width 53 | let percentage = currentAmount / max 54 | let percentageAsDouble = Double(percentage) 55 | let maxAngle: Double = 10 56 | return percentageAsDouble * maxAngle 57 | } 58 | } 59 | 60 | struct DragGestureBootcamp_Previews: PreviewProvider { 61 | static var previews: some View { 62 | DragGestureBootcamp() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/DragGestureBootcamp2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DragGestureBootcamp2.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 3/21/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DragGestureBootcamp2: View { 11 | 12 | @State var startingOffsetY: CGFloat = UIScreen.main.bounds.height * 0.85 13 | @State var currentDragOffsetY: CGFloat = 0 14 | @State var endingOffsetY: CGFloat = 0 15 | 16 | var body: some View { 17 | ZStack { 18 | Color.green.ignoresSafeArea() 19 | 20 | MySignUpView() 21 | .offset(y: startingOffsetY) 22 | .offset(y: currentDragOffsetY) 23 | .offset(y: endingOffsetY) 24 | .gesture( 25 | DragGesture() 26 | .onChanged { value in 27 | withAnimation(.spring()) { 28 | currentDragOffsetY = value.translation.height 29 | } 30 | } 31 | .onEnded { value in 32 | withAnimation(.spring()) { 33 | if currentDragOffsetY < -150 { 34 | endingOffsetY = -startingOffsetY 35 | } else if endingOffsetY != 0 && currentDragOffsetY > 150 { 36 | endingOffsetY = 0 37 | } 38 | currentDragOffsetY = 0 39 | } 40 | } 41 | ) 42 | 43 | } 44 | .ignoresSafeArea(edges: .bottom) 45 | } 46 | } 47 | 48 | struct DragGestureBootcamp2_Previews: PreviewProvider { 49 | static var previews: some View { 50 | DragGestureBootcamp2() 51 | } 52 | } 53 | 54 | struct MySignUpView: View { 55 | var body: some View { 56 | VStack(spacing: 20) { 57 | Image(systemName: "chevron.up") 58 | .padding(.top) 59 | Text("Sign up") 60 | .font(.headline) 61 | .fontWeight(.semibold) 62 | 63 | Image(systemName: "flame.fill") 64 | .resizable() 65 | .scaledToFit() 66 | .frame(width: 100, height: 100) 67 | 68 | Text("This is the decription for our app. This is my favorite SwiftUI course and I recommend to all of my friends to subscribe to Swiftful Thinking!") 69 | .multilineTextAlignment(.center) 70 | 71 | Text("CREATE AN ACCOUNT") 72 | .foregroundColor(.white) 73 | .font(.headline) 74 | .padding() 75 | .padding(.horizontal) 76 | .background(Color.black.cornerRadius(10)) 77 | Spacer() 78 | } 79 | .frame(maxWidth: .infinity) 80 | .background(Color.white) 81 | .cornerRadius(30) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/EscapingBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EscapingBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/10/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class EscapingViewModel: ObservableObject { 11 | 12 | @Published var text: String = "Hello" 13 | 14 | func getData() { 15 | downloadData5 { [weak self] (returnedResult) in 16 | self?.text = returnedResult.data 17 | } 18 | } 19 | 20 | func downloadData() -> String { 21 | return "New data!" 22 | } 23 | 24 | func downloadData2(completionHandler: (_ data: String) -> ()) { 25 | completionHandler("New data!") 26 | } 27 | 28 | func downloadData3(completionHandler: @escaping (_ data: String) -> ()) { 29 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { 30 | completionHandler("New data!") 31 | } 32 | } 33 | 34 | func downloadData4(completionHandler: @escaping (DownloadResult) -> ()) { 35 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { 36 | let result = DownloadResult(data: "New data!") 37 | completionHandler(result) 38 | } 39 | } 40 | 41 | func downloadData5(completionHandler: @escaping DownloadCompletion) { 42 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { 43 | let result = DownloadResult(data: "New data!") 44 | completionHandler(result) 45 | } 46 | } 47 | } 48 | 49 | struct DownloadResult { 50 | let data: String 51 | } 52 | 53 | typealias DownloadCompletion = (DownloadResult) -> () 54 | 55 | struct EscapingBootcamp: View { 56 | 57 | @StateObject var vm = EscapingViewModel() 58 | 59 | var body: some View { 60 | Text(vm.text) 61 | .font(.largeTitle) 62 | .fontWeight(.semibold) 63 | .foregroundColor(.blue) 64 | .onTapGesture { 65 | vm.getData() 66 | } 67 | } 68 | } 69 | 70 | struct EscapingBootcamp_Previews: PreviewProvider { 71 | static var previews: some View { 72 | EscapingBootcamp() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/FileManagerBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManagerBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/22/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class LocalFileManager { 11 | 12 | static let instance = LocalFileManager() 13 | let folderName = "MyApp_Images" 14 | 15 | init() { 16 | createFolderIfNeeded() 17 | } 18 | 19 | func createFolderIfNeeded() { 20 | guard 21 | let path = FileManager 22 | .default 23 | .urls(for: .cachesDirectory, in: .userDomainMask) 24 | .first? 25 | .appendingPathComponent(folderName) 26 | .path else { 27 | return 28 | } 29 | 30 | if !FileManager.default.fileExists(atPath: path) { 31 | do { 32 | try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) 33 | print("Success creating folder.") 34 | } catch let error { 35 | print("Error creating folder. \(error)") 36 | } 37 | } 38 | } 39 | 40 | func deleteFolder() { 41 | guard 42 | let path = FileManager 43 | .default 44 | .urls(for: .cachesDirectory, in: .userDomainMask) 45 | .first? 46 | .appendingPathComponent(folderName) 47 | .path else { 48 | return 49 | } 50 | do { 51 | try FileManager.default.removeItem(atPath: path) 52 | print("Success deleting folder.") 53 | } catch let error { 54 | print("Error deleting folder. \(error)") 55 | } 56 | } 57 | 58 | func saveImage(image: UIImage, name: String) -> String { 59 | guard 60 | let data = image.jpegData(compressionQuality: 1.0), 61 | let path = getPathForImage(name: name) else { 62 | return "Error getting data." 63 | } 64 | 65 | do { 66 | try data.write(to: path) 67 | print(path) 68 | return "Success saving!" 69 | } catch let error { 70 | return "Error saving. \(error)" 71 | } 72 | } 73 | 74 | func getImage(name: String) -> UIImage? { 75 | guard 76 | let path = getPathForImage(name: name)?.path, 77 | FileManager.default.fileExists(atPath: path) else { 78 | print("Error getting path.") 79 | return nil 80 | } 81 | 82 | return UIImage(contentsOfFile: path) 83 | } 84 | 85 | func deleteImage(name: String) -> String { 86 | guard 87 | let path = getPathForImage(name: name)?.path, 88 | FileManager.default.fileExists(atPath: path) else { 89 | return "Error getting path." 90 | } 91 | 92 | do { 93 | try FileManager.default.removeItem(atPath: path) 94 | return "Sucessfully deleted." 95 | } catch let error { 96 | return "Error deleting image. \(error)" 97 | } 98 | 99 | } 100 | 101 | 102 | func getPathForImage(name: String) -> URL? { 103 | guard 104 | let path = FileManager 105 | .default 106 | .urls(for: .cachesDirectory, in: .userDomainMask) 107 | .first? 108 | .appendingPathComponent(folderName) 109 | .appendingPathComponent("\(name).jpg") else { 110 | print("Error getting path.") 111 | return nil 112 | } 113 | 114 | return path 115 | } 116 | 117 | } 118 | 119 | class FileManagerViewModel: ObservableObject { 120 | 121 | @Published var image: UIImage? = nil 122 | let imageName: String = "steve-jobs" 123 | let manager = LocalFileManager.instance 124 | @Published var infoMessage: String = "" 125 | 126 | init() { 127 | getImageFromAssetsFolder() 128 | //getImageFromFileManager() 129 | } 130 | 131 | func getImageFromAssetsFolder() { 132 | image = UIImage(named: imageName) 133 | } 134 | 135 | func getImageFromFileManager() { 136 | image = manager.getImage(name: imageName) 137 | } 138 | 139 | func saveImage() { 140 | guard let image = image else { return } 141 | infoMessage = manager.saveImage(image: image, name: imageName) 142 | } 143 | 144 | func deleteImage() { 145 | infoMessage = manager.deleteImage(name: imageName) 146 | manager.deleteFolder() 147 | } 148 | 149 | } 150 | 151 | struct FileManagerBootcamp: View { 152 | 153 | @StateObject var vm = FileManagerViewModel() 154 | 155 | var body: some View { 156 | NavigationView { 157 | VStack { 158 | 159 | if let image = vm.image { 160 | Image(uiImage: image) 161 | .resizable() 162 | .scaledToFill() 163 | .frame(width: 200, height: 200) 164 | .clipped() 165 | .cornerRadius(10) 166 | } 167 | 168 | HStack { 169 | Button(action: { 170 | vm.saveImage() 171 | }, label: { 172 | Text("Save to FM") 173 | .foregroundColor(.white) 174 | .font(.headline) 175 | .padding() 176 | .padding(.horizontal) 177 | .background(Color.blue) 178 | .cornerRadius(10) 179 | }) 180 | Button(action: { 181 | vm.deleteImage() 182 | }, label: { 183 | Text("Delete from FM") 184 | .foregroundColor(.white) 185 | .font(.headline) 186 | .padding() 187 | .padding(.horizontal) 188 | .background(Color.red) 189 | .cornerRadius(10) 190 | }) 191 | } 192 | 193 | Text(vm.infoMessage) 194 | .font(.largeTitle) 195 | .fontWeight(.semibold) 196 | .foregroundColor(.purple) 197 | 198 | 199 | 200 | Spacer() 201 | } 202 | .navigationTitle("File Manager") 203 | } 204 | } 205 | } 206 | 207 | struct FileManagerBootcamp_Previews: PreviewProvider { 208 | static var previews: some View { 209 | FileManagerBootcamp() 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/FruitsContainer.xcdatamodeld/FruitsContainer.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/GeometryReaderBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeometryReaderBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 3/21/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GeometryReaderBootcamp: View { 11 | 12 | func getPercentage(geo: GeometryProxy) -> Double { 13 | let maxDistance = UIScreen.main.bounds.width / 2 14 | let currentX = geo.frame(in: .global).midX 15 | return Double(1 - (currentX / maxDistance)) 16 | } 17 | 18 | var body: some View { 19 | ScrollView(.horizontal, showsIndicators: false, content: { 20 | HStack { 21 | ForEach(0..<20) { index in 22 | GeometryReader { geometry in 23 | RoundedRectangle(cornerRadius: 20) 24 | .rotation3DEffect( 25 | Angle(degrees: getPercentage(geo: geometry) * 40), 26 | axis: (x: 0.0, y: 1.0, z: 0.0)) 27 | } 28 | .frame(width: 300, height: 250) 29 | .padding() 30 | } 31 | } 32 | }) 33 | // GeometryReader { geometry in 34 | // HStack(spacing: 0) { 35 | // Rectangle() 36 | // .fill(Color.red) 37 | // .frame(width: geometry.size.width * 0.6666) 38 | // 39 | // Rectangle().fill(Color.blue) 40 | // } 41 | // .ignoresSafeArea() 42 | // } 43 | } 44 | } 45 | 46 | struct GeometryReaderBootcamp_Previews: PreviewProvider { 47 | static var previews: some View { 48 | GeometryReaderBootcamp() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/HapticsBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HapticsBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 3/30/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class HapticManager { 11 | 12 | static let instance = HapticManager() // Singleton 13 | 14 | func notification(type: UINotificationFeedbackGenerator.FeedbackType) { 15 | let generator = UINotificationFeedbackGenerator() 16 | generator.notificationOccurred(type) 17 | } 18 | 19 | func impact(style: UIImpactFeedbackGenerator.FeedbackStyle) { 20 | let generator = UIImpactFeedbackGenerator(style: style) 21 | generator.impactOccurred() 22 | } 23 | 24 | } 25 | 26 | struct HapticsBootcamp: View { 27 | var body: some View { 28 | VStack(spacing: 20) { 29 | 30 | Button("success") { HapticManager.instance.notification(type: .success) } 31 | Button("warning") { HapticManager.instance.notification(type: .warning) } 32 | Button("error") { HapticManager.instance.notification(type: .error) } 33 | Divider() 34 | Button("soft") { HapticManager.instance.impact(style: .soft) } 35 | Button("light") { HapticManager.instance.impact(style: .light) } 36 | Button("medium") { HapticManager.instance.impact(style: .medium) } 37 | Button("rigid") { HapticManager.instance.impact(style: .rigid) } 38 | Button("heavy") { HapticManager.instance.impact(style: .heavy) } 39 | } 40 | } 41 | } 42 | 43 | struct HapticsBootcamp_Previews: PreviewProvider { 44 | static var previews: some View { 45 | HapticsBootcamp() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/HashableBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HashableBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/2/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MyCustomModel: Hashable { 11 | let title: String 12 | 13 | func hash(into hasher: inout Hasher) { 14 | hasher.combine(title) 15 | } 16 | } 17 | 18 | struct HashableBootcamp: View { 19 | 20 | let data: [MyCustomModel] = [ 21 | MyCustomModel(title: "ONE"), 22 | MyCustomModel(title: "TWO"), 23 | MyCustomModel(title: "THREE"), 24 | MyCustomModel(title: "FOUR"), 25 | MyCustomModel(title: "FIVE"), 26 | ] 27 | 28 | var body: some View { 29 | ScrollView { 30 | VStack(spacing: 40) { 31 | ForEach(data, id: \.self) { item in 32 | Text(item.hashValue.description) 33 | .font(.headline) 34 | } 35 | } 36 | } 37 | } 38 | } 39 | 40 | struct HashableBootcamp_Previews: PreviewProvider { 41 | static var previews: some View { 42 | HashableBootcamp() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/LocalNotificationBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalNotificationBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/2/21. 6 | // 7 | 8 | import SwiftUI 9 | import UserNotifications 10 | import CoreLocation 11 | 12 | class NotificationManager { 13 | 14 | static let instance = NotificationManager() // Singleton 15 | 16 | func requestAuthorization() { 17 | let options: UNAuthorizationOptions = [.alert, .sound, .badge] 18 | UNUserNotificationCenter.current().requestAuthorization(options: options) { (success, error) in 19 | if let error = error { 20 | print("ERROR: \(error)") 21 | } else { 22 | print("SUCCESS") 23 | } 24 | } 25 | } 26 | 27 | func scheduleNotification() { 28 | 29 | 30 | let content = UNMutableNotificationContent() 31 | content.title = "This is my first notification!" 32 | content.subtitle = "This was sooooo easy!" 33 | content.sound = .default 34 | content.badge = 1 // NSNumber(value: UIApplication.shared.applicationIconBadgeNumber + 1) 35 | 36 | // time 37 | let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5.0, repeats: false) 38 | 39 | // calendar 40 | // var dateComponents = DateComponents() 41 | // dateComponents.hour = 8 42 | // dateComponents.minute = 0 43 | // dateComponents.weekday = 2 44 | // let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true) 45 | 46 | // location 47 | // let coordinates = CLLocationCoordinate2D( 48 | // latitude: 40.00, 49 | // longitude: 50.00) 50 | // let region = CLCircularRegion( 51 | // center: coordinates, 52 | // radius: 100, 53 | // identifier: UUID().uuidString) 54 | // region.notifyOnEntry = true 55 | // region.notifyOnExit = true 56 | // let trigger = UNLocationNotificationTrigger(region: region, repeats: true) 57 | 58 | let request = UNNotificationRequest( 59 | identifier: UUID().uuidString, 60 | content: content, 61 | trigger: trigger) 62 | UNUserNotificationCenter.current().add(request) 63 | 64 | } 65 | 66 | func cancelNotification() { 67 | UNUserNotificationCenter.current().removeAllPendingNotificationRequests() 68 | UNUserNotificationCenter.current().removeAllDeliveredNotifications() 69 | } 70 | 71 | } 72 | 73 | struct LocalNotificationBootcamp: View { 74 | var body: some View { 75 | VStack(spacing: 40) { 76 | Button("Request permission") { 77 | NotificationManager.instance.requestAuthorization() 78 | } 79 | Button("Schedule notification") { 80 | NotificationManager.instance.scheduleNotification() 81 | } 82 | Button("Cancel notification") { 83 | NotificationManager.instance.cancelNotification() 84 | } 85 | } 86 | .onAppear { 87 | UIApplication.shared.applicationIconBadgeNumber = 0 88 | } 89 | } 90 | } 91 | 92 | struct LocalNotificationBootcamp_Previews: PreviewProvider { 93 | static var previews: some View { 94 | LocalNotificationBootcamp() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/LongPressGestureBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LongPressGestureBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 3/21/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LongPressGestureBootcamp: View { 11 | 12 | @State var isComplete: Bool = false 13 | @State var isSuccess: Bool = false 14 | var body: some View { 15 | 16 | VStack { 17 | Rectangle() 18 | .fill(isSuccess ? Color.green : Color.blue) 19 | .frame(maxWidth: isComplete ? .infinity : 0) 20 | .frame(height: 55) 21 | .frame(maxWidth: .infinity, alignment: .leading) 22 | .background(Color.gray) 23 | 24 | HStack { 25 | Text("CLICK HERE") 26 | .foregroundColor(.white) 27 | .padding() 28 | .background(Color.black) 29 | .cornerRadius(10) 30 | .onLongPressGesture(minimumDuration: 1.0, maximumDistance: 50) { (isPressing) in 31 | // start of press -> min duration 32 | if isPressing { 33 | withAnimation(.easeInOut(duration: 1.0)) { 34 | isComplete = true 35 | } 36 | } else { 37 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 38 | if !isSuccess { 39 | withAnimation(.easeInOut) { 40 | isComplete = false 41 | } 42 | } 43 | } 44 | } 45 | 46 | } perform: { 47 | // at the min duration 48 | withAnimation(.easeInOut) { 49 | isSuccess = true 50 | } 51 | } 52 | 53 | 54 | Text("RESET") 55 | .foregroundColor(.white) 56 | .padding() 57 | .background(Color.black) 58 | .cornerRadius(10) 59 | .onTapGesture { 60 | isComplete = false 61 | isSuccess = false 62 | } 63 | } 64 | } 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | // Text(isComplete ? "COMPLETED" : "NOT COMPLETE") 76 | // .padding() 77 | // .padding(.horizontal) 78 | // .background(isComplete ? Color.green : Color.gray) 79 | // .cornerRadius(10) 80 | //// .onTapGesture { 81 | //// isComplete.toggle() 82 | //// } 83 | // .onLongPressGesture(minimumDuration: 5.0, maximumDistance: 50) { 84 | // isComplete.toggle() 85 | // } 86 | 87 | } 88 | } 89 | 90 | struct LongPressGestureBootcamp_Previews: PreviewProvider { 91 | static var previews: some View { 92 | LongPressGestureBootcamp() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/MagnificationGestureBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MagnificationGestureBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 3/21/21. 6 | // 7 | 8 | 9 | import SwiftUI 10 | 11 | struct MagnificationGestureBootcamp: View { 12 | 13 | @State var currentAmount: CGFloat = 0 14 | @State var lastAmount: CGFloat = 0 15 | 16 | var body: some View { 17 | VStack(spacing: 10) { 18 | HStack { 19 | Circle().frame(width: 35, height: 35) 20 | Text("Swiftful Thinking") 21 | Spacer() 22 | Image(systemName: "ellipsis") 23 | } 24 | .padding(.horizontal) 25 | Image("steve-jobs") 26 | .resizable() 27 | .scaledToFill() 28 | .frame(height: 300) 29 | .clipped() 30 | .scaleEffect(1 + currentAmount) 31 | .gesture( 32 | MagnificationGesture() 33 | .onChanged { value in 34 | currentAmount = value - 1 35 | } 36 | .onEnded { value in 37 | withAnimation(.spring()) { 38 | currentAmount = 0 39 | } 40 | } 41 | ) 42 | 43 | HStack { 44 | Image(systemName: "heart.fill") 45 | Image(systemName: "text.bubble.fill") 46 | Spacer() 47 | } 48 | .padding(.horizontal) 49 | .font(.headline) 50 | Text("This is the caption for my photo!") 51 | .frame(maxWidth: .infinity, alignment: .leading) 52 | .padding(.horizontal) 53 | 54 | } 55 | 56 | // Text("Hello, World!") 57 | // .font(.title) 58 | // .padding(40) 59 | // .background(Color.red.cornerRadius(10)) 60 | // .scaleEffect(1 + currentAmount + lastAmount) 61 | // .gesture( 62 | // MagnificationGesture() 63 | // .onChanged { value in 64 | // currentAmount = value - 1 65 | // } 66 | // .onEnded { value in 67 | // lastAmount += currentAmount 68 | // currentAmount = 0 69 | // } 70 | // ) 71 | } 72 | } 73 | 74 | struct MagnificationGestureBootcamp_Previews: PreviewProvider { 75 | static var previews: some View { 76 | MagnificationGestureBootcamp() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/MaskBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaskBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 3/27/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MaskBootcamp: View { 11 | 12 | @State var rating: Int = 0 13 | 14 | var body: some View { 15 | ZStack { 16 | starsView 17 | .overlay(overlayView.mask(starsView)) 18 | } 19 | } 20 | 21 | private var overlayView: some View { 22 | GeometryReader { geometry in 23 | ZStack(alignment: .leading) { 24 | Rectangle() 25 | //.foregroundColor(.yellow) 26 | .fill(LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing)) 27 | .frame(width: CGFloat(rating) / 5 * geometry.size.width) 28 | } 29 | } 30 | .allowsHitTesting(false) 31 | } 32 | 33 | private var starsView: some View { 34 | HStack { 35 | ForEach(1..<6) { index in 36 | Image(systemName: "star.fill") 37 | .font(.largeTitle) 38 | .foregroundColor(Color.gray) 39 | .onTapGesture { 40 | withAnimation(.easeInOut) { 41 | rating = index 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | struct MaskBootcamp_Previews: PreviewProvider { 50 | static var previews: some View { 51 | MaskBootcamp() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/MultipleSheetsBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultipleSheetsBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 3/27/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RandomModel: Identifiable { 11 | let id = UUID().uuidString 12 | let title: String 13 | } 14 | 15 | // 1 - use a binding 16 | // 2 - use multiple .sheets 17 | // 3 - use $item 18 | 19 | struct MultipleSheetsBootcamp: View { 20 | 21 | @State var selectedModel: RandomModel? = nil 22 | 23 | var body: some View { 24 | ScrollView { 25 | VStack(spacing: 20) { 26 | ForEach(0..<50) { index in 27 | Button("Button \(index)") { 28 | selectedModel = RandomModel(title: "\(index)") 29 | } 30 | } 31 | } 32 | .sheet(item: $selectedModel) { model in 33 | NextScreen(selectedModel: model) 34 | } 35 | } 36 | } 37 | } 38 | 39 | struct NextScreen: View { 40 | 41 | let selectedModel: RandomModel 42 | 43 | var body: some View { 44 | Text(selectedModel.title) 45 | .font(.largeTitle) 46 | } 47 | } 48 | 49 | struct MultipleSheetsBootcamp_Previews: PreviewProvider { 50 | static var previews: some View { 51 | MultipleSheetsBootcamp() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/RotationGestureBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RotationGestureBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 3/21/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RotationGestureBootcamp: View { 11 | 12 | @State var angle: Angle = Angle(degrees: 0) 13 | 14 | var body: some View { 15 | Text("Hello, World!") 16 | .font(.largeTitle) 17 | .fontWeight(.semibold) 18 | .foregroundColor(.white) 19 | .padding(50) 20 | .background(Color.blue.cornerRadius(10)) 21 | .rotationEffect(angle) 22 | .gesture( 23 | RotationGesture() 24 | .onChanged { value in 25 | angle = value 26 | } 27 | .onEnded { value in 28 | withAnimation(.spring()) { 29 | angle = Angle(degrees: 0) 30 | } 31 | } 32 | ) 33 | } 34 | } 35 | 36 | struct RotationGestureBootcamp_Previews: PreviewProvider { 37 | static var previews: some View { 38 | RotationGestureBootcamp() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/ScrollViewPagingBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollViewPagingBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 1/28/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ScrollViewPagingBootcamp: View { 11 | 12 | @State private var scrollPosition: Int? = nil 13 | 14 | var body: some View { 15 | VStack { 16 | Button("SCROLL TO") { 17 | scrollPosition = (0..<20).randomElement()! 18 | } 19 | 20 | ScrollView(.horizontal) { 21 | HStack(spacing: 0) { 22 | ForEach(0..<20) { index in 23 | Rectangle() 24 | .frame(width: 300, height: 300) 25 | .cornerRadius(10) 26 | .overlay( 27 | Text("\(index)").foregroundColor(.white) 28 | ) 29 | .frame(maxWidth: .infinity) 30 | .padding(10) 31 | .id(index) 32 | .scrollTransition(.interactive.threshold(.visible(0.9))) { content, phase in 33 | content 34 | .opacity(phase.isIdentity ? 1 : 0) 35 | .offset(y: phase.isIdentity ? 0 : -100) 36 | } 37 | // .containerRelativeFrame(.horizontal, alignment: .center) 38 | } 39 | } 40 | .padding(.vertical, 100) 41 | } 42 | .ignoresSafeArea() 43 | .scrollTargetLayout() 44 | .scrollTargetBehavior(.viewAligned) 45 | .scrollBounceBehavior(.basedOnSize) 46 | .scrollPosition(id: $scrollPosition, anchor: .center) 47 | .animation(.smooth, value: scrollPosition) 48 | } 49 | 50 | // ScrollView { 51 | // VStack(spacing: 0) { 52 | // ForEach(0..<20) { index in 53 | // Rectangle() 54 | //// .frame(width: 300, height: 300) 55 | // .overlay( 56 | // Text("\(index)").foregroundColor(.white) 57 | // ) 58 | // .frame(maxWidth: .infinity) 59 | //// .padding(.vertical, 10) 60 | // .containerRelativeFrame(.vertical, alignment: .center) 61 | // } 62 | // } 63 | // } 64 | // .ignoresSafeArea() 65 | // .scrollTargetLayout() 66 | // .scrollTargetBehavior(.paging) 67 | // .scrollBounceBehavior(.basedOnSize) 68 | } 69 | } 70 | 71 | #Preview { 72 | ScrollViewPagingBootcamp() 73 | } 74 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/ScrollViewReaderBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollViewReaderBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 3/21/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ScrollViewReaderBootcamp: View { 11 | 12 | @State var scrollToIndex: Int = 0 13 | @State var textFieldText: String = "" 14 | 15 | var body: some View { 16 | VStack { 17 | TextField("Enter a # here...", text: $textFieldText) 18 | .frame(height: 55) 19 | .border(Color.gray) 20 | .padding(.horizontal) 21 | .keyboardType(.numberPad) 22 | 23 | Button("SCROLL NOW") { 24 | withAnimation(.spring()) { 25 | if let index = Int(textFieldText) { 26 | scrollToIndex = index 27 | } 28 | } 29 | } 30 | 31 | 32 | ScrollView { 33 | ScrollViewReader { proxy in 34 | ForEach(0..<50) { index in 35 | Text("This is item #\(index)") 36 | .font(.headline) 37 | .frame(height: 200) 38 | .frame(maxWidth: .infinity) 39 | .background(Color.white) 40 | .cornerRadius(10) 41 | .shadow(radius: 10) 42 | .padding() 43 | .id(index) 44 | } 45 | .onChange(of: scrollToIndex, perform: { value in 46 | withAnimation(.spring()) { 47 | proxy.scrollTo(value, anchor: .top) 48 | } 49 | }) 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | struct ScrollViewReaderBootcamp_Previews: PreviewProvider { 57 | static var previews: some View { 58 | ScrollViewReaderBootcamp() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/Sounds/badum.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftfulThinking/SwiftUI-Continued-Learning/cce4f6bb3be15362933ea118edbdbf4acd0c87bf/SwiftfulThinkingContinuedLearning/Sounds/badum.mp3 -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/Sounds/tada.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftfulThinking/SwiftUI-Continued-Learning/cce4f6bb3be15362933ea118edbdbf4acd0c87bf/SwiftfulThinkingContinuedLearning/Sounds/tada.mp3 -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/SoundsBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SoundsBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 3/30/21. 6 | // 7 | 8 | import SwiftUI 9 | import AVKit 10 | 11 | class SoundManager { 12 | 13 | static let instance = SoundManager() // Singleton 14 | 15 | var player: AVAudioPlayer? 16 | 17 | enum SoundOption: String { 18 | case tada 19 | case badum 20 | } 21 | 22 | func playSound(sound: SoundOption) { 23 | 24 | guard let url = Bundle.main.url(forResource: sound.rawValue, withExtension: ".mp3") else { return } 25 | 26 | do { 27 | player = try AVAudioPlayer(contentsOf: url) 28 | player?.play() 29 | } catch let error { 30 | print("Error playing sound. \(error.localizedDescription)") 31 | } 32 | } 33 | } 34 | 35 | struct SoundsBootcamp: View { 36 | 37 | var body: some View { 38 | VStack(spacing: 40) { 39 | Button("Play sound 1") { 40 | SoundManager.instance.playSound(sound: .tada) 41 | } 42 | Button("Play sound 2") { 43 | SoundManager.instance.playSound(sound: .badum) 44 | } 45 | } 46 | } 47 | } 48 | 49 | struct SoundsBootcamp_Previews: PreviewProvider { 50 | static var previews: some View { 51 | SoundsBootcamp() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/SubscriberBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubscriberBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/17/21. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | class SubscriberViewModel: ObservableObject { 12 | 13 | @Published var count: Int = 0 14 | var cancellables = Set() 15 | 16 | @Published var textFieldText: String = "" 17 | @Published var textIsValid: Bool = false 18 | 19 | @Published var showButton: Bool = false 20 | 21 | init() { 22 | setUpTimer() 23 | addTextFieldSubscriber() 24 | addButtonSubscriber() 25 | } 26 | 27 | func addTextFieldSubscriber() { 28 | $textFieldText 29 | .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main) 30 | .map { (text) -> Bool in 31 | if text.count > 3 { 32 | return true 33 | } 34 | return false 35 | } 36 | //.assign(to: \.textIsValid, on: self) 37 | .sink(receiveValue: { [weak self] (isValid) in 38 | self?.textIsValid = isValid 39 | }) 40 | .store(in: &cancellables) 41 | } 42 | 43 | func setUpTimer() { 44 | Timer 45 | .publish(every: 1, on: .main, in: .common) 46 | .autoconnect() 47 | .sink { [weak self] _ in 48 | guard let self = self else { return } 49 | self.count += 1 50 | } 51 | .store(in: &cancellables) 52 | } 53 | 54 | func addButtonSubscriber() { 55 | $textIsValid 56 | .combineLatest($count) 57 | .sink { [weak self] (isValid, count) in 58 | guard let self = self else { return } 59 | if isValid && count >= 10 { 60 | self.showButton = true 61 | } else { 62 | self.showButton = false 63 | } 64 | } 65 | .store(in: &cancellables) 66 | } 67 | } 68 | 69 | struct SubscriberBootcamp: View { 70 | 71 | @StateObject var vm = SubscriberViewModel() 72 | 73 | var body: some View { 74 | VStack { 75 | Text("\(vm.count)") 76 | .font(.largeTitle) 77 | 78 | TextField("Type something here...", text: $vm.textFieldText) 79 | .padding(.leading) 80 | .frame(height: 55) 81 | .font(.headline) 82 | .background(Color(#colorLiteral(red: 0.921431005, green: 0.9214526415, blue: 0.9214410186, alpha: 1))) 83 | .cornerRadius(10) 84 | .overlay( 85 | ZStack { 86 | Image(systemName: "xmark") 87 | .foregroundColor(.red) 88 | .opacity( 89 | vm.textFieldText.count < 1 ? 0.0 : 90 | vm.textIsValid ? 0.0 : 1.0) 91 | 92 | Image(systemName: "checkmark") 93 | .foregroundColor(.green) 94 | .opacity(vm.textIsValid ? 1.0 : 0.0) 95 | } 96 | .font(.title) 97 | .padding(.trailing) 98 | 99 | , alignment: .trailing 100 | ) 101 | 102 | Button(action: {}, label: { 103 | Text("Submit".uppercased()) 104 | .font(.headline) 105 | .foregroundColor(.white) 106 | .frame(height: 55) 107 | .frame(maxWidth: .infinity) 108 | .background(Color.blue) 109 | .cornerRadius(10) 110 | .opacity(vm.showButton ? 1.0 : 0.5) 111 | }) 112 | .disabled(!vm.showButton) 113 | } 114 | .padding() 115 | } 116 | } 117 | 118 | struct SubscriberBootcamp_Previews: PreviewProvider { 119 | static var previews: some View { 120 | SubscriberBootcamp() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/SwiftfulThinkingContinuedLearningApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftfulThinkingContinuedLearningApp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 3/21/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SwiftfulThinkingContinuedLearningApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | AlignmentGuideBootcamp() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/TimerBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimerBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/17/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TimerBootcamp: View { 11 | 12 | let timer = Timer.publish(every: 3.0, on: .main, in: .common).autoconnect() 13 | 14 | // Current Time 15 | /* 16 | @State var currentDate: Date = Date() 17 | var dateFormatter: DateFormatter { 18 | let formatter = DateFormatter() 19 | //formatter.dateStyle = .medium 20 | formatter.timeStyle = .medium 21 | return formatter 22 | } 23 | */ 24 | 25 | // Countdown 26 | /* 27 | @State var count: Int = 10 28 | @State var finishedText: String? = nil 29 | */ 30 | 31 | // Countdown to date 32 | /* 33 | @State var timeRemaining: String = "" 34 | let futureDate: Date = Calendar.current.date(byAdding: .hour, value: 1, to: Date()) ?? Date() 35 | 36 | func updateTimeRemaining() { 37 | let remaining = Calendar.current.dateComponents([.minute, .second], from: Date(), to: futureDate) 38 | let minute = remaining.minute ?? 0 39 | let second = remaining.second ?? 0 40 | timeRemaining = "\(minute) minutes, \(second) seconds" 41 | } 42 | */ 43 | 44 | // Animation counter 45 | @State var count: Int = 1 46 | 47 | var body: some View { 48 | ZStack { 49 | RadialGradient( 50 | gradient: Gradient(colors: [Color(#colorLiteral(red: 0.3647058904, green: 0.06666667014, blue: 0.9686274529, alpha: 1)), Color(#colorLiteral(red: 0.09019608051, green: 0, blue: 0.3019607961, alpha: 1))]), 51 | center: .center, 52 | startRadius: 5, 53 | endRadius: 500) 54 | .ignoresSafeArea() 55 | 56 | TabView(selection: $count, 57 | content: { 58 | Rectangle() 59 | .foregroundColor(.red) 60 | .tag(1) 61 | Rectangle() 62 | .foregroundColor(.blue) 63 | .tag(2) 64 | Rectangle() 65 | .foregroundColor(.green) 66 | .tag(3) 67 | Rectangle() 68 | .foregroundColor(.orange) 69 | .tag(4) 70 | Rectangle() 71 | .foregroundColor(.pink) 72 | .tag(5) 73 | }) 74 | .frame(height: 200) 75 | .tabViewStyle(PageTabViewStyle()) 76 | } 77 | .onReceive(timer, perform: { _ in 78 | withAnimation(.default) { 79 | count = count == 5 ? 1 : count + 1 80 | } 81 | }) 82 | } 83 | } 84 | 85 | struct TimerBootcamp_Previews: PreviewProvider { 86 | static var previews: some View { 87 | TimerBootcamp() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/TypealiasBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypealiasBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/9/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MovieModel { 11 | let title: String 12 | let director: String 13 | let count: Int 14 | } 15 | 16 | typealias TVModel = MovieModel 17 | 18 | struct TypealiasBootcamp: View { 19 | 20 | //@State var item: MovieModel = MovieModel(title: "Title", director: "Joe", count: 5) 21 | @State var item: TVModel = TVModel(title: "TV Title", director: "Emmily", count: 10) 22 | 23 | var body: some View { 24 | VStack { 25 | Text(item.title) 26 | Text(item.director) 27 | Text("\(item.count)") 28 | } 29 | } 30 | } 31 | 32 | struct TypealiasBootcamp_Previews: PreviewProvider { 33 | static var previews: some View { 34 | TypealiasBootcamp() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/VisualEffectBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VisualEffectBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 1/28/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 17, *) 11 | struct VisualEffectBootcamp: View { 12 | 13 | // @State private var showSpacer: Bool = false 14 | 15 | var body: some View { 16 | ScrollView { 17 | VStack(spacing: 30) { 18 | ForEach(0..<100) { index in 19 | Rectangle() 20 | .frame(width: 300, height: 200) 21 | .frame(maxWidth: .infinity) 22 | .background(Color.orange) 23 | .visualEffect { content, geometry in 24 | content 25 | .offset(x: geometry.frame(in: .global).minY * 0.5) 26 | } 27 | } 28 | } 29 | } 30 | 31 | // VStack { 32 | // Text("Hello world asdjf ;lkasjdf l;aksdjf l;askdfj asl;dkfj a;sldf !") 33 | // .padding() 34 | // .background(Color.red) 35 | // .visualEffect { content, geometry in 36 | // content 37 | // .grayscale(geometry.frame(in: .global).minY < 300 ? 1 : 0) 38 | // // .grayscale(geometry.size.width >= 200 ? 1 : 0) 39 | // } 40 | // 41 | // if showSpacer { 42 | // Spacer() 43 | // } 44 | // } 45 | // .animation(.easeIn, value: showSpacer) 46 | // .onTapGesture { 47 | // showSpacer.toggle() 48 | // } 49 | } 50 | } 51 | 52 | @available(iOS 17, *) 53 | #Preview { 54 | VisualEffectBootcamp() 55 | } 56 | -------------------------------------------------------------------------------- /SwiftfulThinkingContinuedLearning/WeakSelfBootcamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeakSelfBootcamp.swift 3 | // SwiftfulThinkingContinuedLearning 4 | // 5 | // Created by Nick Sarno on 4/9/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WeakSelfBootcamp: View { 11 | 12 | @AppStorage("count") var count: Int? 13 | 14 | init() { 15 | count = 0 16 | } 17 | 18 | var body: some View { 19 | NavigationView { 20 | NavigationLink("Navigate", destination: WeakSelfSecondScreen()) 21 | .navigationTitle("Screen 1") 22 | } 23 | .overlay( 24 | Text("\(count ?? 0)") 25 | .font(.largeTitle) 26 | .padding() 27 | .background(Color.green.cornerRadius(10)) 28 | , alignment: .topTrailing 29 | ) 30 | } 31 | } 32 | 33 | struct WeakSelfSecondScreen: View { 34 | 35 | @StateObject var vm = WeakSelfSecondScreenViewModel() 36 | 37 | init() { 38 | UINavigationBar.appearance().largeTitleTextAttributes = [.font: UIFont(name: "Arial", size: 32)!] 39 | } 40 | 41 | var body: some View { 42 | VStack { 43 | Text("Second View") 44 | .font(.largeTitle) 45 | .foregroundColor(.red) 46 | 47 | if let data = vm.data { 48 | Text(data) 49 | } 50 | } 51 | } 52 | } 53 | 54 | class WeakSelfSecondScreenViewModel: ObservableObject { 55 | 56 | @Published var data: String? = nil 57 | 58 | init() { 59 | print("INITIALIZE NOW") 60 | let currentCount = UserDefaults.standard.integer(forKey: "count") 61 | UserDefaults.standard.set(currentCount + 1, forKey: "count") 62 | getData() 63 | } 64 | 65 | deinit { 66 | print("DEINITIALIZE NOW") 67 | let currentCount = UserDefaults.standard.integer(forKey: "count") 68 | UserDefaults.standard.set(currentCount - 1, forKey: "count") 69 | } 70 | 71 | func getData() { 72 | 73 | DispatchQueue.main.asyncAfter(deadline: .now() + 500) { [weak self] in 74 | self?.data = "NEW DATA!!!!" 75 | } 76 | 77 | } 78 | 79 | } 80 | 81 | struct WeakSelfBootcamp_Previews: PreviewProvider { 82 | static var previews: some View { 83 | WeakSelfBootcamp() 84 | } 85 | } 86 | --------------------------------------------------------------------------------