diff --git a/CoreStore.sketch b/CoreStore.sketch index ece9425..f422368 100644 Binary files a/CoreStore.sketch and b/CoreStore.sketch differ diff --git a/CoreStore.xcworkspace/contents.xcworkspacedata b/CoreStore.xcworkspace/contents.xcworkspacedata index 8ff0a03..a804436 100644 --- a/CoreStore.xcworkspace/contents.xcworkspacedata +++ b/CoreStore.xcworkspace/contents.xcworkspacedata @@ -1,6 +1,9 @@ + + diff --git a/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/Palette.swift b/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/Palette.swift index 3d05cad..0f0120a 100644 --- a/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/Palette.swift +++ b/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/Palette.swift @@ -38,7 +38,7 @@ final class Palette: CoreStoreObject { return colorName } let colorName: String - switch object.$hue.value % 360 { + switch object.$hue.value { case 0 ..< 20: colorName = "Lower Reds" case 20 ..< 57: colorName = "Oranges and Browns" case 57 ..< 90: colorName = "Yellow-Greens" @@ -47,7 +47,8 @@ final class Palette: CoreStoreObject { case 197 ..< 241: colorName = "Blues" case 241 ..< 297: colorName = "Violets" case 297 ..< 331: colorName = "Magentas" - default: colorName = "Upper Reds" + case 331 ..< 360: colorName = "Upper Reds" + default: colorName = "" } field.primitiveValue = colorName return colorName diff --git a/CoreStoreDemo/appIcons.sketch b/CoreStoreDemo/appIcons.sketch index a933130..260ec2d 100644 Binary files a/CoreStoreDemo/appIcons.sketch and b/CoreStoreDemo/appIcons.sketch differ diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b7a05ce --- /dev/null +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -0,0 +1,698 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + B5A3911D24E5429200E7E8BD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3911C24E5429200E7E8BD /* AppDelegate.swift */; }; + B5A3911F24E5429200E7E8BD /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */; }; + B5A3912124E5429200E7E8BD /* Menu.MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3912024E5429200E7E8BD /* Menu.MainView.swift */; }; + B5A3912324E5429300E7E8BD /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5A3912224E5429300E7E8BD /* Images.xcassets */; }; + B5A3912624E5429300E7E8BD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5A3912524E5429300E7E8BD /* Preview Assets.xcassets */; }; + B5A3912924E5429300E7E8BD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B5A3912724E5429300E7E8BD /* LaunchScreen.storyboard */; }; + B5A3913424E6170500E7E8BD /* Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3913324E6170500E7E8BD /* Menu.swift */; }; + B5A3915324E6537F00E7E8BD /* Menu.ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3915224E6537F00E7E8BD /* Menu.ItemView.swift */; }; + B5A3915924E685EC00E7E8BD /* Classic.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3915824E685EC00E7E8BD /* Classic.swift */; }; + B5A3915B24E685FE00E7E8BD /* Modern.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3915A24E685FE00E7E8BD /* Modern.swift */; }; + B5A3915E24E6922E00E7E8BD /* Modern.PlacemarksDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3915D24E6922E00E7E8BD /* Modern.PlacemarksDemo.swift */; }; + B5A3916024E6925900E7E8BD /* Modern.PlacemarksDemo.MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3915F24E6925900E7E8BD /* Modern.PlacemarksDemo.MapView.swift */; }; + B5A3916224E697BA00E7E8BD /* Modern.PlacemarksDemo.MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3916124E697BA00E7E8BD /* Modern.PlacemarksDemo.MainView.swift */; }; + B5A3916524E698C700E7E8BD /* Modern.PlacemarksDemo.Place.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3916424E698C700E7E8BD /* Modern.PlacemarksDemo.Place.swift */; }; + B5A3916B24E698F900E7E8BD /* CoreStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3916724E698F900E7E8BD /* CoreStore.framework */; }; + B5A3916C24E698F900E7E8BD /* CoreStore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3916724E698F900E7E8BD /* CoreStore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B5A3916D24E698F900E7E8BD /* CoreStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3916824E698F900E7E8BD /* CoreStore.framework */; }; + B5A3916E24E698F900E7E8BD /* CoreStore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3916824E698F900E7E8BD /* CoreStore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B5A3916F24E698F900E7E8BD /* CoreStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3916924E698F900E7E8BD /* CoreStore.framework */; }; + B5A3917024E698F900E7E8BD /* CoreStore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3916924E698F900E7E8BD /* CoreStore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B5A3917124E698F900E7E8BD /* CoreStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3916A24E698F900E7E8BD /* CoreStore.framework */; }; + B5A3917224E698F900E7E8BD /* CoreStore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3916A24E698F900E7E8BD /* CoreStore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B5A3917524E6990200E7E8BD /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3917424E6990200E7E8BD /* MapKit.framework */; }; + B5A3917724E6990700E7E8BD /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3917624E6990700E7E8BD /* CoreLocation.framework */; }; + B5A3917924E6991600E7E8BD /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3917824E6991600E7E8BD /* SwiftUI.framework */; }; + B5A3917C24E6A76C00E7E8BD /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3917B24E6A76C00E7E8BD /* LazyView.swift */; }; + B5A3917E24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3917D24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift */; }; + B5A3918024E787D900E7E8BD /* InstructionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3917F24E787D900E7E8BD /* InstructionsView.swift */; }; + B5A3918324E7A21800E7E8BD /* Modern.TimeZonesDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3918224E7A21800E7E8BD /* Modern.TimeZonesDemo.swift */; }; + B5A3918624E7A54A00E7E8BD /* Modern.TimeZonesDemo.TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3918524E7A54A00E7E8BD /* Modern.TimeZonesDemo.TimeZone.swift */; }; + B5A3918824E7A8F900E7E8BD /* Modern.TimeZonesDemo.MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3918724E7A8F900E7E8BD /* Modern.TimeZonesDemo.MainView.swift */; }; + B5A3918A24E7AD1800E7E8BD /* Modern.TimeZonesDemo.ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3918924E7AD1800E7E8BD /* Modern.TimeZonesDemo.ListView.swift */; }; + B5A3918C24E7B44B00E7E8BD /* Modern.TimeZonesDemo.ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3918B24E7B44B00E7E8BD /* Modern.TimeZonesDemo.ItemView.swift */; }; + B5A3918F24E7E06500E7E8BD /* Modern.ColorsDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3918E24E7E06500E7E8BD /* Modern.ColorsDemo.swift */; }; + B5A3919224E7E0C600E7E8BD /* Modern.ColorsDemo.Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3919124E7E0C600E7E8BD /* Modern.ColorsDemo.Palette.swift */; }; + B5A3919424E7E36700E7E8BD /* Modern.ColorsDemo.SwiftUI.ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3919324E7E36700E7E8BD /* Modern.ColorsDemo.SwiftUI.ListView.swift */; }; + B5A3919624E7E4AC00E7E8BD /* Modern.ColorsDemo.SwiftUI.ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3919524E7E4AC00E7E8BD /* Modern.ColorsDemo.SwiftUI.ItemView.swift */; }; + B5A3919824E7E67000E7E8BD /* Modern.ColorsDemo.Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3919724E7E67000E7E8BD /* Modern.ColorsDemo.Filter.swift */; }; + B5A3919A24E8207A00E7E8BD /* Modern.ColorsDemo.SwiftUI.DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3919924E8207A00E7E8BD /* Modern.ColorsDemo.SwiftUI.DetailView.swift */; }; + B5A3919E24E8EEB600E7E8BD /* Modern.ColorsDemo.SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3919D24E8EEB600E7E8BD /* Modern.ColorsDemo.SwiftUI.swift */; }; + B5A391A024E8F00A00E7E8BD /* Modern.ColorsDemo.UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3919F24E8F00A00E7E8BD /* Modern.ColorsDemo.UIKit.swift */; }; + B5A391A224E8F01F00E7E8BD /* Modern.ColorsDemo.UIKit.ListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391A124E8F01F00E7E8BD /* Modern.ColorsDemo.UIKit.ListViewController.swift */; }; + B5A391A424E8F04300E7E8BD /* Modern.ColorsDemo.UIKit.ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391A324E8F04300E7E8BD /* Modern.ColorsDemo.UIKit.ListView.swift */; }; + B5A391A624E8F4EA00E7E8BD /* Modern.ColorsDemo.MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391A524E8F4EA00E7E8BD /* Modern.ColorsDemo.MainView.swift */; }; + B5A391A824E90F1000E7E8BD /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391A724E90F1000E7E8BD /* UIImage+Extensions.swift */; }; + B5A391AA24E9104300E7E8BD /* Modern.ColorsDemo.UIKit.ItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391A924E9104300E7E8BD /* Modern.ColorsDemo.UIKit.ItemCell.swift */; }; + B5A391AC24E9143B00E7E8BD /* Modern.ColorsDemo.UIKit.DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391AB24E9143B00E7E8BD /* Modern.ColorsDemo.UIKit.DetailView.swift */; }; + B5A391AE24E9150F00E7E8BD /* Modern.ColorsDemo.UIKit.DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391AD24E9150F00E7E8BD /* Modern.ColorsDemo.UIKit.DetailViewController.swift */; }; + B5A391B124E96AF600E7E8BD /* Modern.PokedexDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391B024E96AF600E7E8BD /* Modern.PokedexDemo.swift */; }; + B5A391B424E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391B324E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift */; }; + B5A391B624E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391B524E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift */; }; + B5A391B924E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391B824E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift */; }; + B5A391BB24E970A400E7E8BD /* Modern.PokedexDemo.PokemonType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391BA24E970A400E7E8BD /* Modern.PokedexDemo.PokemonType.swift */; }; + B5A391BD24E977E500E7E8BD /* Modern.PokedexDemo.Ability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391BC24E977E500E7E8BD /* Modern.PokedexDemo.Ability.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + B5A3917324E698F900E7E8BD /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + B5A3916C24E698F900E7E8BD /* CoreStore.framework in Embed Frameworks */, + B5A3917224E698F900E7E8BD /* CoreStore.framework in Embed Frameworks */, + B5A3917024E698F900E7E8BD /* CoreStore.framework in Embed Frameworks */, + B5A3916E24E698F900E7E8BD /* CoreStore.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + B5A3911924E5429200E7E8BD /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B5A3911C24E5429200E7E8BD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + B5A3912024E5429200E7E8BD /* Menu.MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menu.MainView.swift; sourceTree = ""; }; + B5A3912224E5429300E7E8BD /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + B5A3912524E5429300E7E8BD /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + B5A3912824E5429300E7E8BD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + B5A3913324E6170500E7E8BD /* Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menu.swift; sourceTree = ""; }; + B5A3913924E62A9A00E7E8BD /* Rakefile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; fileEncoding = 4; path = Rakefile; sourceTree = SOURCE_ROOT; }; + B5A3914124E62D3900E7E8BD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B5A3915224E6537F00E7E8BD /* Menu.ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menu.ItemView.swift; sourceTree = ""; }; + B5A3915824E685EC00E7E8BD /* Classic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Classic.swift; sourceTree = ""; }; + B5A3915A24E685FE00E7E8BD /* Modern.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.swift; sourceTree = ""; }; + B5A3915D24E6922E00E7E8BD /* Modern.PlacemarksDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PlacemarksDemo.swift; sourceTree = ""; }; + B5A3915F24E6925900E7E8BD /* Modern.PlacemarksDemo.MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PlacemarksDemo.MapView.swift; sourceTree = ""; }; + B5A3916124E697BA00E7E8BD /* Modern.PlacemarksDemo.MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PlacemarksDemo.MainView.swift; sourceTree = ""; }; + B5A3916424E698C700E7E8BD /* Modern.PlacemarksDemo.Place.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PlacemarksDemo.Place.swift; sourceTree = ""; }; + B5A3916724E698F900E7E8BD /* CoreStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CoreStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B5A3916824E698F900E7E8BD /* CoreStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CoreStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B5A3916924E698F900E7E8BD /* CoreStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CoreStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B5A3916A24E698F900E7E8BD /* CoreStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CoreStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B5A3917424E6990200E7E8BD /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.6.sdk/System/Library/Frameworks/MapKit.framework; sourceTree = DEVELOPER_DIR; }; + B5A3917624E6990700E7E8BD /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.6.sdk/System/Library/Frameworks/CoreLocation.framework; sourceTree = DEVELOPER_DIR; }; + B5A3917824E6991600E7E8BD /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.6.sdk/System/Library/Frameworks/SwiftUI.framework; sourceTree = DEVELOPER_DIR; }; + B5A3917B24E6A76C00E7E8BD /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; + B5A3917D24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PlacemarksDemo.Geocoder.swift; sourceTree = ""; }; + B5A3917F24E787D900E7E8BD /* InstructionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstructionsView.swift; sourceTree = ""; }; + B5A3918224E7A21800E7E8BD /* Modern.TimeZonesDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.TimeZonesDemo.swift; sourceTree = ""; }; + B5A3918524E7A54A00E7E8BD /* Modern.TimeZonesDemo.TimeZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.TimeZonesDemo.TimeZone.swift; sourceTree = ""; }; + B5A3918724E7A8F900E7E8BD /* Modern.TimeZonesDemo.MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.TimeZonesDemo.MainView.swift; sourceTree = ""; }; + B5A3918924E7AD1800E7E8BD /* Modern.TimeZonesDemo.ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.TimeZonesDemo.ListView.swift; sourceTree = ""; }; + B5A3918B24E7B44B00E7E8BD /* Modern.TimeZonesDemo.ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.TimeZonesDemo.ItemView.swift; sourceTree = ""; }; + B5A3918E24E7E06500E7E8BD /* Modern.ColorsDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.swift; sourceTree = ""; }; + B5A3919124E7E0C600E7E8BD /* Modern.ColorsDemo.Palette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.Palette.swift; sourceTree = ""; }; + B5A3919324E7E36700E7E8BD /* Modern.ColorsDemo.SwiftUI.ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.SwiftUI.ListView.swift; sourceTree = ""; }; + B5A3919524E7E4AC00E7E8BD /* Modern.ColorsDemo.SwiftUI.ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.SwiftUI.ItemView.swift; sourceTree = ""; }; + B5A3919724E7E67000E7E8BD /* Modern.ColorsDemo.Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.Filter.swift; sourceTree = ""; }; + B5A3919924E8207A00E7E8BD /* Modern.ColorsDemo.SwiftUI.DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.SwiftUI.DetailView.swift; sourceTree = ""; }; + B5A3919D24E8EEB600E7E8BD /* Modern.ColorsDemo.SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.SwiftUI.swift; sourceTree = ""; }; + B5A3919F24E8F00A00E7E8BD /* Modern.ColorsDemo.UIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.UIKit.swift; sourceTree = ""; }; + B5A391A124E8F01F00E7E8BD /* Modern.ColorsDemo.UIKit.ListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.UIKit.ListViewController.swift; sourceTree = ""; }; + B5A391A324E8F04300E7E8BD /* Modern.ColorsDemo.UIKit.ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.UIKit.ListView.swift; sourceTree = ""; }; + B5A391A524E8F4EA00E7E8BD /* Modern.ColorsDemo.MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.MainView.swift; sourceTree = ""; }; + B5A391A724E90F1000E7E8BD /* UIImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extensions.swift"; sourceTree = ""; }; + B5A391A924E9104300E7E8BD /* Modern.ColorsDemo.UIKit.ItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.UIKit.ItemCell.swift; sourceTree = ""; }; + B5A391AB24E9143B00E7E8BD /* Modern.ColorsDemo.UIKit.DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.UIKit.DetailView.swift; sourceTree = ""; }; + B5A391AD24E9150F00E7E8BD /* Modern.ColorsDemo.UIKit.DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.ColorsDemo.UIKit.DetailViewController.swift; sourceTree = ""; }; + B5A391B024E96AF600E7E8BD /* Modern.PokedexDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.swift; sourceTree = ""; }; + B5A391B324E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokemonSpecies.swift; sourceTree = ""; }; + B5A391B524E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.Move.swift; sourceTree = ""; }; + B5A391B824E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokemonForm.swift; sourceTree = ""; }; + B5A391BA24E970A400E7E8BD /* Modern.PokedexDemo.PokemonType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokemonType.swift; sourceTree = ""; }; + B5A391BC24E977E500E7E8BD /* Modern.PokedexDemo.Ability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.Ability.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + B5A3911624E5429200E7E8BD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B5A3917724E6990700E7E8BD /* CoreLocation.framework in Frameworks */, + B5A3916B24E698F900E7E8BD /* CoreStore.framework in Frameworks */, + B5A3917124E698F900E7E8BD /* CoreStore.framework in Frameworks */, + B5A3917924E6991600E7E8BD /* SwiftUI.framework in Frameworks */, + B5A3917524E6990200E7E8BD /* MapKit.framework in Frameworks */, + B5A3916F24E698F900E7E8BD /* CoreStore.framework in Frameworks */, + B5A3916D24E698F900E7E8BD /* CoreStore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B5A3910E24E5424E00E7E8BD = { + isa = PBXGroup; + children = ( + B5A3913D24E62C6C00E7E8BD /* Metadata */, + B5A3911B24E5429200E7E8BD /* Sources */, + B5A3913E24E62CB200E7E8BD /* Resources */, + B5A3911A24E5429200E7E8BD /* Products */, + B5A3916624E698F900E7E8BD /* Frameworks */, + ); + sourceTree = ""; + }; + B5A3911A24E5429200E7E8BD /* Products */ = { + isa = PBXGroup; + children = ( + B5A3911924E5429200E7E8BD /* Demo.app */, + ); + name = Products; + sourceTree = ""; + }; + B5A3911B24E5429200E7E8BD /* Sources */ = { + isa = PBXGroup; + children = ( + B5A3911C24E5429200E7E8BD /* AppDelegate.swift */, + B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */, + B5A3915524E6858A00E7E8BD /* Demos */, + B5A3917A24E6A75F00E7E8BD /* Helpers */, + ); + path = Sources; + sourceTree = ""; + }; + B5A3912424E5429300E7E8BD /* Preview Content */ = { + isa = PBXGroup; + children = ( + B5A3912524E5429300E7E8BD /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + B5A3913D24E62C6C00E7E8BD /* Metadata */ = { + isa = PBXGroup; + children = ( + B5A3913924E62A9A00E7E8BD /* Rakefile */, + B5A3914124E62D3900E7E8BD /* Info.plist */, + ); + name = Metadata; + sourceTree = ""; + }; + B5A3913E24E62CB200E7E8BD /* Resources */ = { + isa = PBXGroup; + children = ( + B5A3912724E5429300E7E8BD /* LaunchScreen.storyboard */, + B5A3912224E5429300E7E8BD /* Images.xcassets */, + B5A3912424E5429300E7E8BD /* Preview Content */, + ); + path = Resources; + sourceTree = ""; + }; + B5A3915424E6857F00E7E8BD /* Menu */ = { + isa = PBXGroup; + children = ( + B5A3913324E6170500E7E8BD /* Menu.swift */, + B5A3912024E5429200E7E8BD /* Menu.MainView.swift */, + B5A3915224E6537F00E7E8BD /* Menu.ItemView.swift */, + ); + path = Menu; + sourceTree = ""; + }; + B5A3915524E6858A00E7E8BD /* Demos */ = { + isa = PBXGroup; + children = ( + B5A3915624E685B700E7E8BD /* Modern */, + B5A3915724E685D300E7E8BD /* Classic */, + ); + path = Demos; + sourceTree = ""; + }; + B5A3915624E685B700E7E8BD /* Modern */ = { + isa = PBXGroup; + children = ( + B5A3915A24E685FE00E7E8BD /* Modern.swift */, + B5A3915C24E6921E00E7E8BD /* PlacemarksDemo */, + B5A3918124E7A1EF00E7E8BD /* TimeZonesDemo */, + B5A3918D24E7DE7A00E7E8BD /* ColorsDemo */, + B5A391AF24E96AD600E7E8BD /* PokedexDemo */, + ); + path = Modern; + sourceTree = ""; + }; + B5A3915724E685D300E7E8BD /* Classic */ = { + isa = PBXGroup; + children = ( + B5A3915824E685EC00E7E8BD /* Classic.swift */, + ); + path = Classic; + sourceTree = ""; + }; + B5A3915C24E6921E00E7E8BD /* PlacemarksDemo */ = { + isa = PBXGroup; + children = ( + B5A3915D24E6922E00E7E8BD /* Modern.PlacemarksDemo.swift */, + B5A3916124E697BA00E7E8BD /* Modern.PlacemarksDemo.MainView.swift */, + B5A3915F24E6925900E7E8BD /* Modern.PlacemarksDemo.MapView.swift */, + B5A3917D24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift */, + B5A3916324E698B300E7E8BD /* Models */, + ); + path = PlacemarksDemo; + sourceTree = ""; + }; + B5A3916324E698B300E7E8BD /* Models */ = { + isa = PBXGroup; + children = ( + B5A3916424E698C700E7E8BD /* Modern.PlacemarksDemo.Place.swift */, + ); + name = Models; + sourceTree = ""; + }; + B5A3916624E698F900E7E8BD /* Frameworks */ = { + isa = PBXGroup; + children = ( + B5A3917824E6991600E7E8BD /* SwiftUI.framework */, + B5A3917624E6990700E7E8BD /* CoreLocation.framework */, + B5A3917424E6990200E7E8BD /* MapKit.framework */, + B5A3916724E698F900E7E8BD /* CoreStore.framework */, + B5A3916824E698F900E7E8BD /* CoreStore.framework */, + B5A3916924E698F900E7E8BD /* CoreStore.framework */, + B5A3916A24E698F900E7E8BD /* CoreStore.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + B5A3917A24E6A75F00E7E8BD /* Helpers */ = { + isa = PBXGroup; + children = ( + B5A3917B24E6A76C00E7E8BD /* LazyView.swift */, + B5A3917F24E787D900E7E8BD /* InstructionsView.swift */, + B5A391A724E90F1000E7E8BD /* UIImage+Extensions.swift */, + B5A3915424E6857F00E7E8BD /* Menu */, + ); + path = Helpers; + sourceTree = ""; + }; + B5A3918124E7A1EF00E7E8BD /* TimeZonesDemo */ = { + isa = PBXGroup; + children = ( + B5A3918224E7A21800E7E8BD /* Modern.TimeZonesDemo.swift */, + B5A3918724E7A8F900E7E8BD /* Modern.TimeZonesDemo.MainView.swift */, + B5A3918924E7AD1800E7E8BD /* Modern.TimeZonesDemo.ListView.swift */, + B5A3918B24E7B44B00E7E8BD /* Modern.TimeZonesDemo.ItemView.swift */, + B5A3918424E7A53300E7E8BD /* Models */, + ); + path = TimeZonesDemo; + sourceTree = ""; + }; + B5A3918424E7A53300E7E8BD /* Models */ = { + isa = PBXGroup; + children = ( + B5A3918524E7A54A00E7E8BD /* Modern.TimeZonesDemo.TimeZone.swift */, + ); + name = Models; + sourceTree = ""; + }; + B5A3918D24E7DE7A00E7E8BD /* ColorsDemo */ = { + isa = PBXGroup; + children = ( + B5A3918E24E7E06500E7E8BD /* Modern.ColorsDemo.swift */, + B5A3919724E7E67000E7E8BD /* Modern.ColorsDemo.Filter.swift */, + B5A391A524E8F4EA00E7E8BD /* Modern.ColorsDemo.MainView.swift */, + B5A3919C24E8EE9000E7E8BD /* UIKit */, + B5A3919B24E8EE8100E7E8BD /* SwiftUI */, + B5A3919024E7E0B000E7E8BD /* Models */, + ); + path = ColorsDemo; + sourceTree = ""; + }; + B5A3919024E7E0B000E7E8BD /* Models */ = { + isa = PBXGroup; + children = ( + B5A3919124E7E0C600E7E8BD /* Modern.ColorsDemo.Palette.swift */, + ); + name = Models; + sourceTree = ""; + }; + B5A3919B24E8EE8100E7E8BD /* SwiftUI */ = { + isa = PBXGroup; + children = ( + B5A3919D24E8EEB600E7E8BD /* Modern.ColorsDemo.SwiftUI.swift */, + B5A3919324E7E36700E7E8BD /* Modern.ColorsDemo.SwiftUI.ListView.swift */, + B5A3919524E7E4AC00E7E8BD /* Modern.ColorsDemo.SwiftUI.ItemView.swift */, + B5A3919924E8207A00E7E8BD /* Modern.ColorsDemo.SwiftUI.DetailView.swift */, + ); + name = SwiftUI; + sourceTree = ""; + }; + B5A3919C24E8EE9000E7E8BD /* UIKit */ = { + isa = PBXGroup; + children = ( + B5A3919F24E8F00A00E7E8BD /* Modern.ColorsDemo.UIKit.swift */, + B5A391A324E8F04300E7E8BD /* Modern.ColorsDemo.UIKit.ListView.swift */, + B5A391A124E8F01F00E7E8BD /* Modern.ColorsDemo.UIKit.ListViewController.swift */, + B5A391A924E9104300E7E8BD /* Modern.ColorsDemo.UIKit.ItemCell.swift */, + B5A391AB24E9143B00E7E8BD /* Modern.ColorsDemo.UIKit.DetailView.swift */, + B5A391AD24E9150F00E7E8BD /* Modern.ColorsDemo.UIKit.DetailViewController.swift */, + ); + name = UIKit; + sourceTree = ""; + }; + B5A391AF24E96AD600E7E8BD /* PokedexDemo */ = { + isa = PBXGroup; + children = ( + B5A391B024E96AF600E7E8BD /* Modern.PokedexDemo.swift */, + B5A391B224E96B7400E7E8BD /* Models */, + ); + path = PokedexDemo; + sourceTree = ""; + }; + B5A391B224E96B7400E7E8BD /* Models */ = { + isa = PBXGroup; + children = ( + B5A391B324E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift */, + B5A391B824E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift */, + B5A391B524E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift */, + B5A391BC24E977E500E7E8BD /* Modern.PokedexDemo.Ability.swift */, + B5A391B724E96E8600E7E8BD /* Attributes */, + ); + name = Models; + sourceTree = ""; + }; + B5A391B724E96E8600E7E8BD /* Attributes */ = { + isa = PBXGroup; + children = ( + B5A391BA24E970A400E7E8BD /* Modern.PokedexDemo.PokemonType.swift */, + ); + name = Attributes; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + B5A3911824E5429200E7E8BD /* Demo */ = { + isa = PBXNativeTarget; + buildConfigurationList = B5A3912B24E5429300E7E8BD /* Build configuration list for PBXNativeTarget "Demo" */; + buildPhases = ( + B5A3911524E5429200E7E8BD /* Sources */, + B5A3911624E5429200E7E8BD /* Frameworks */, + B5A3911724E5429200E7E8BD /* Resources */, + B5A3917324E698F900E7E8BD /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Demo; + productName = Demo; + productReference = B5A3911924E5429200E7E8BD /* Demo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B5A3910F24E5424E00E7E8BD /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1160; + LastUpgradeCheck = 1160; + TargetAttributes = { + B5A3911824E5429200E7E8BD = { + CreatedOnToolsVersion = 11.6; + }; + }; + }; + buildConfigurationList = B5A3911224E5424E00E7E8BD /* Build configuration list for PBXProject "Demo" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B5A3910E24E5424E00E7E8BD; + productRefGroup = B5A3911A24E5429200E7E8BD /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B5A3911824E5429200E7E8BD /* Demo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B5A3911724E5429200E7E8BD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B5A3912924E5429300E7E8BD /* LaunchScreen.storyboard in Resources */, + B5A3912624E5429300E7E8BD /* Preview Assets.xcassets in Resources */, + B5A3912324E5429300E7E8BD /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B5A3911524E5429200E7E8BD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B5A3918824E7A8F900E7E8BD /* Modern.TimeZonesDemo.MainView.swift in Sources */, + B5A391AE24E9150F00E7E8BD /* Modern.ColorsDemo.UIKit.DetailViewController.swift in Sources */, + B5A391A224E8F01F00E7E8BD /* Modern.ColorsDemo.UIKit.ListViewController.swift in Sources */, + B5A391AA24E9104300E7E8BD /* Modern.ColorsDemo.UIKit.ItemCell.swift in Sources */, + B5A3917C24E6A76C00E7E8BD /* LazyView.swift in Sources */, + B5A3918324E7A21800E7E8BD /* Modern.TimeZonesDemo.swift in Sources */, + B5A3915324E6537F00E7E8BD /* Menu.ItemView.swift in Sources */, + B5A3911D24E5429200E7E8BD /* AppDelegate.swift in Sources */, + B5A391A024E8F00A00E7E8BD /* Modern.ColorsDemo.UIKit.swift in Sources */, + B5A3913424E6170500E7E8BD /* Menu.swift in Sources */, + B5A391B924E96F8500E7E8BD /* Modern.PokedexDemo.PokemonForm.swift in Sources */, + B5A3918624E7A54A00E7E8BD /* Modern.TimeZonesDemo.TimeZone.swift in Sources */, + B5A3915B24E685FE00E7E8BD /* Modern.swift in Sources */, + B5A391BB24E970A400E7E8BD /* Modern.PokedexDemo.PokemonType.swift in Sources */, + B5A3919624E7E4AC00E7E8BD /* Modern.ColorsDemo.SwiftUI.ItemView.swift in Sources */, + B5A3919A24E8207A00E7E8BD /* Modern.ColorsDemo.SwiftUI.DetailView.swift in Sources */, + B5A3916024E6925900E7E8BD /* Modern.PlacemarksDemo.MapView.swift in Sources */, + B5A391B124E96AF600E7E8BD /* Modern.PokedexDemo.swift in Sources */, + B5A3918A24E7AD1800E7E8BD /* Modern.TimeZonesDemo.ListView.swift in Sources */, + B5A3911F24E5429200E7E8BD /* SceneDelegate.swift in Sources */, + B5A3915924E685EC00E7E8BD /* Classic.swift in Sources */, + B5A3919824E7E67000E7E8BD /* Modern.ColorsDemo.Filter.swift in Sources */, + B5A391B624E96C5500E7E8BD /* Modern.PokedexDemo.Move.swift in Sources */, + B5A3916224E697BA00E7E8BD /* Modern.PlacemarksDemo.MainView.swift in Sources */, + B5A3917E24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift in Sources */, + B5A391A424E8F04300E7E8BD /* Modern.ColorsDemo.UIKit.ListView.swift in Sources */, + B5A391AC24E9143B00E7E8BD /* Modern.ColorsDemo.UIKit.DetailView.swift in Sources */, + B5A3919224E7E0C600E7E8BD /* Modern.ColorsDemo.Palette.swift in Sources */, + B5A3919424E7E36700E7E8BD /* Modern.ColorsDemo.SwiftUI.ListView.swift in Sources */, + B5A3918F24E7E06500E7E8BD /* Modern.ColorsDemo.swift in Sources */, + B5A3915E24E6922E00E7E8BD /* Modern.PlacemarksDemo.swift in Sources */, + B5A391B424E96C0A00E7E8BD /* Modern.PokedexDemo.PokemonSpecies.swift in Sources */, + B5A391A824E90F1000E7E8BD /* UIImage+Extensions.swift in Sources */, + B5A3918024E787D900E7E8BD /* InstructionsView.swift in Sources */, + B5A3918C24E7B44B00E7E8BD /* Modern.TimeZonesDemo.ItemView.swift in Sources */, + B5A3919E24E8EEB600E7E8BD /* Modern.ColorsDemo.SwiftUI.swift in Sources */, + B5A391A624E8F4EA00E7E8BD /* Modern.ColorsDemo.MainView.swift in Sources */, + B5A3916524E698C700E7E8BD /* Modern.PlacemarksDemo.Place.swift in Sources */, + B5A391BD24E977E500E7E8BD /* Modern.PokedexDemo.Ability.swift in Sources */, + B5A3912124E5429200E7E8BD /* Menu.MainView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + B5A3912724E5429300E7E8BD /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + B5A3912824E5429300E7E8BD /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + B5A3911324E5424E00E7E8BD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + }; + name = Debug; + }; + B5A3911424E5424E00E7E8BD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + }; + name = Release; + }; + B5A3912C24E5429300E7E8BD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = appIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_ASSET_PATHS = "\"Resources/Preview Content\""; + DEVELOPMENT_TEAM = 2JT32EJ5BH; + ENABLE_PREVIEWS = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.johnestropia.Demo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + B5A3912D24E5429300E7E8BD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = appIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_ASSET_PATHS = "\"Resources/Preview Content\""; + DEVELOPMENT_TEAM = 2JT32EJ5BH; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_PREVIEWS = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.johnestropia.Demo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B5A3911224E5424E00E7E8BD /* Build configuration list for PBXProject "Demo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B5A3911324E5424E00E7E8BD /* Debug */, + B5A3911424E5424E00E7E8BD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B5A3912B24E5429300E7E8BD /* Build configuration list for PBXNativeTarget "Demo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B5A3912C24E5429300E7E8BD /* Debug */, + B5A3912D24E5429300E7E8BD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = B5A3910F24E5424E00E7E8BD /* Project object */; +} diff --git a/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme b/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme new file mode 100644 index 0000000..e69c643 --- /dev/null +++ b/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Demo.xcodeproj/xcuserdata/johnestropia.xcuserdatad/xcschemes/xcschememanagement.plist b/Demo/Demo.xcodeproj/xcuserdata/johnestropia.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..5a1cd28 --- /dev/null +++ b/Demo/Demo.xcodeproj/xcuserdata/johnestropia.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,22 @@ + + + + + SchemeUserState + + Demo.xcscheme_^#shared#^_ + + orderHint + 5 + + + SuppressBuildableAutocreation + + B5A3911824E5429200E7E8BD + + primary + + + + + diff --git a/Demo/Info.plist b/Demo/Info.plist new file mode 100644 index 0000000..49e8f4a --- /dev/null +++ b/Demo/Info.plist @@ -0,0 +1,70 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarTintParameters + + UINavigationBar + + Style + UIBarStyleDefault + Translucent + + + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Demo/Rakefile b/Demo/Rakefile new file mode 100644 index 0000000..9159500 --- /dev/null +++ b/Demo/Rakefile @@ -0,0 +1,42 @@ +# coding: utf-8 + +task :format do + + require 'xcodeproj' + require 'fileutils' + + project_path = 'Demo.xcodeproj' + ignore_targets = [] + + # http://www.rubydoc.info/github/CocoaPods/Xcodeproj + project = Xcodeproj::Project.open(project_path) + validTargets = project.targets.select { |target| target.respond_to?(:product_type) } + + validTargets.each do |target| + if ignore_targets.include?(target.display_name) + next + end + case target.product_type + + when 'com.apple.product-type.application', 'com.apple.product-type.framework' + target.source_build_phase.files.sort! do |f1, f2| + result = (f1.display_name.count "+") <=> (f2.display_name.count "+") + if result != 0 + next -result + end + result = (f1.display_name.count "-") <=> (f2.display_name.count "-") + if result != 0 + next -result + end + result = (f1.display_name.count ".") <=> (f2.display_name.count ".") + if result != 0 + next result + end + (f1.display_name <=> f2.display_name) + end + + end + end + + project.save() +end diff --git a/Demo/Resources/Base.lproj/LaunchScreen.storyboard b/Demo/Resources/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..2b871ce --- /dev/null +++ b/Demo/Resources/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Resources/Images.xcassets/AppIcon.appiconset/Contents.json b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..f140363 --- /dev/null +++ b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,115 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "Icon-60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "Icon-60@3x-1.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "Icon-76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "Icon-83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "Mask + Oval 1 + Oval 1 + Oval 1.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "idiom" : "car", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "Icon-60@3x.png", + "idiom" : "car", + "scale" : "3x", + "size" : "60x60" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png new file mode 100644 index 0000000..1d1cc0c Binary files /dev/null and b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-60@3x-1.png b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-60@3x-1.png new file mode 100644 index 0000000..74457f3 Binary files /dev/null and b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-60@3x-1.png differ diff --git a/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png new file mode 100644 index 0000000..74457f3 Binary files /dev/null and b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png differ diff --git a/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-76.png b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-76.png new file mode 100644 index 0000000..bd400e9 Binary files /dev/null and b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-76.png differ diff --git a/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png new file mode 100644 index 0000000..fd39f4a Binary files /dev/null and b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png differ diff --git a/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png new file mode 100644 index 0000000..a1f0ae6 Binary files /dev/null and b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png differ diff --git a/Demo/Resources/Images.xcassets/AppIcon.appiconset/Mask + Oval 1 + Oval 1 + Oval 1.png b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Mask + Oval 1 + Oval 1 + Oval 1.png new file mode 100644 index 0000000..dfdb62a Binary files /dev/null and b/Demo/Resources/Images.xcassets/AppIcon.appiconset/Mask + Oval 1 + Oval 1 + Oval 1.png differ diff --git a/Demo/Resources/Images.xcassets/Contents.json b/Demo/Resources/Images.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Demo/Resources/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/Resources/Images.xcassets/CoreStoreIcon.imageset/Contents.json b/Demo/Resources/Images.xcassets/CoreStoreIcon.imageset/Contents.json new file mode 100644 index 0000000..fafeb8a --- /dev/null +++ b/Demo/Resources/Images.xcassets/CoreStoreIcon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "CoreStoreIcon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/Demo/Resources/Images.xcassets/CoreStoreIcon.imageset/CoreStoreIcon.pdf b/Demo/Resources/Images.xcassets/CoreStoreIcon.imageset/CoreStoreIcon.pdf new file mode 100644 index 0000000..c7ceed9 Binary files /dev/null and b/Demo/Resources/Images.xcassets/CoreStoreIcon.imageset/CoreStoreIcon.pdf differ diff --git a/Demo/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Demo/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Demo/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/Sources/AppDelegate.swift b/Demo/Sources/AppDelegate.swift new file mode 100644 index 0000000..907769e --- /dev/null +++ b/Demo/Sources/AppDelegate.swift @@ -0,0 +1,33 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import UIKit + +// MARK: - AppDelegate + +@UIApplicationMain +@objc final class AppDelegate: UIResponder, UIApplicationDelegate { + + // MARK: UIApplicationDelegate + + @objc dynamic func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + + return true + } + + @objc dynamic func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + + return UISceneConfiguration( + name: "Default Configuration", + sessionRole: connectingSceneSession.role + ) + } +} diff --git a/Demo/Sources/Demos/Classic/Classic.swift b/Demo/Sources/Demos/Classic/Classic.swift new file mode 100644 index 0000000..8df6539 --- /dev/null +++ b/Demo/Sources/Demos/Classic/Classic.swift @@ -0,0 +1,10 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +// MARK: - Classic + +/** +Sample usages for `NSManagedObject` subclasses +*/ +enum Classic {} diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Filter.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Filter.swift new file mode 100644 index 0000000..3e5747e --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Filter.swift @@ -0,0 +1,36 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + + +// MARK: - Modern.ColorsDemo + +extension Modern.ColorsDemo { + + // MARK: - Modern.ColorsDemo.Filter + + enum Filter: String, CaseIterable { + + case all = "All Colors" + case light = "Light Colors" + case dark = "Dark Colors" + + func next() -> Filter { + + let allCases = Self.allCases + return allCases[(allCases.firstIndex(of: self)! + 1) % allCases.count] + } + + func whereClause() -> Where { + + switch self { + + case .all: return .init() + case .light: return (\Modern.ColorsDemo.Palette.$brightness >= 0.9) + case .dark: return (\Modern.ColorsDemo.Palette.$brightness <= 0.4) + } + } + } +} diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift new file mode 100644 index 0000000..bad5ee7 --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift @@ -0,0 +1,190 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import SwiftUI + +// MARK: - Modern.ColorsDemo + +extension Modern.ColorsDemo { + + // MARK: - Modern.ColorsDemo.MainView + + struct MainView: View { + + /** + ⭐️ Sample 1: Setting a sectioned `ListPublisher` declared as an `@ObservedObject` + */ + @ObservedObject + private var listPublisher: ListPublisher + + // MARK: Internal + + init( + listView: @escaping ( + _ listPublisher: ListPublisher, + _ onPaletteTapped: @escaping (ObjectPublisher) -> Void + ) -> ListView, + detailView: @escaping (ObjectPublisher) -> DetailView) { + + self.listView = listView + self.detailView = detailView + self.listPublisher = Modern.ColorsDemo.palettesPublisher + self._filter = Binding( + get: { Modern.ColorsDemo.filter }, + set: { Modern.ColorsDemo.filter = $0 } + ) + } + + + // MARK: View + + var body: some View { + let detailView: AnyView + if let selectedPalette = self.selectedPalette { + + detailView = AnyView( + self.detailView(selectedPalette) + ) + } + else { + + detailView = AnyView(EmptyView()) + } + let listPublisher = self.listPublisher + return VStack(spacing: 0) { + self.listView(listPublisher, { self.selectedPalette = $0 }) + .navigationBarTitle( + Text("Colors (\(listPublisher.snapshot.numberOfItems) objects)") + ) + .frame(minHeight: 0, maxHeight: .infinity) + detailView + .edgesIgnoringSafeArea(.all) + .frame(minHeight: 0, maxHeight: .infinity) + } + .navigationBarItems( + leading: HStack { + EditButton() + Button( + action: { self.clearColors() }, + label: { Text("Clear") } + ) + }, + trailing: HStack { + Button( + action: { self.changeFilter() }, + label: { Text(self.filter.rawValue) } + ) + Button( + action: { self.shuffleColors() }, + label: { Text("Shuffle") } + ) + Button( + action: { self.addColor() }, + label: { Text("Add") } + ) + } + ) + } + + + // MARK: Private + + private let listView: ( + _ listPublisher: ListPublisher, + _ onPaletteTapped: @escaping (ObjectPublisher) -> Void + ) -> ListView + + private let detailView: ( + _ objectPublisher: ObjectPublisher + ) -> DetailView + + @State + private var selectedPalette: ObjectPublisher? + + @Binding + private var filter: Modern.ColorsDemo.Filter + + private func changeFilter() { + + Modern.ColorsDemo.filter = Modern.ColorsDemo.filter.next() + } + + private func clearColors() { + + Modern.ColorsDemo.dataStack.perform( + asynchronous: { transaction in + + try transaction.deleteAll(From()) + }, + completion: { _ in } + ) + } + + private func addColor() { + + Modern.ColorsDemo.dataStack.perform( + asynchronous: { transaction in + + _ = transaction.create(Into()) + }, + completion: { _ in } + ) + } + + private func shuffleColors() { + + Modern.ColorsDemo.dataStack.perform( + asynchronous: { transaction in + + for palette in try transaction.fetchAll(From()) { + + palette.setRandomHue() + } + }, + completion: { _ in } + ) + } + } +} + +#if DEBUG + +struct _Demo_Modern_ColorsDemo_MainView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + let minimumSamples = 10 + try! Modern.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + let missing = minimumSamples + - (try transaction.fetchCount(From())) + guard missing > 0 else { + return + } + for _ in 0..()) + palette.setRandomHue() + } + } + ) + return Modern.ColorsDemo.MainView( + listView: { listPublisher, onPaletteTapped in + Modern.ColorsDemo.SwiftUI.ListView( + listPublisher: listPublisher, + onPaletteTapped: onPaletteTapped + ) + }, + detailView: { objectPublisher in + Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher) + } + ) + } +} + +#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Palette.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Palette.swift new file mode 100644 index 0000000..9467a32 --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Palette.swift @@ -0,0 +1,143 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import UIKit +import CoreStore + +// MARK: - Modern.ColorsDemo + +extension Modern.ColorsDemo { + + // MARK: - Modern.ColorsDemo.Palette + + final class Palette: CoreStoreObject { + + // MARK: Internal + + @Field.Stored( + "hue", + customSetter: { object, field, value in + + Palette.resetVirtualProperties(object) + field.primitiveValue = value + }, + dynamicInitialValue: { Palette.randomHue() } + ) + var hue: Float + + @Field.Stored( + "saturation", + customSetter: { object, field, value in + + Palette.resetVirtualProperties(object) + field.primitiveValue = value + }, + dynamicInitialValue: { Palette.randomSaturation() } + ) + var saturation: Float + + @Field.Stored( + "brightness", + customSetter: { object, field, value in + + Palette.resetVirtualProperties(object) + field.primitiveValue = value + }, + dynamicInitialValue: { Palette.randomBrightness() } + ) + var brightness: Float + + @Field.Virtual( + "colorName", + customGetter: { object, field in + + if let colorName = field.primitiveValue { + + return colorName + } + let colorName: String + switch object.$hue.value * 359 { + + case 0 ..< 20: colorName = "Lower Reds" + case 20 ..< 57: colorName = "Oranges and Browns" + case 57 ..< 90: colorName = "Yellow-Greens" + case 90 ..< 159: colorName = "Greens" + case 159 ..< 197: colorName = "Blue-Greens" + case 197 ..< 241: colorName = "Blues" + case 241 ..< 297: colorName = "Violets" + case 297 ..< 331: colorName = "Magentas" + default: colorName = "Upper Reds" + } + field.primitiveValue = colorName + return colorName + } + ) + var colorName: String + + @Field.Virtual( + "color", + customGetter: { object, field in + + if let color = field.primitiveValue { + + return color + } + let color = UIColor( + hue: CGFloat(object.$hue.value), + saturation: CGFloat(object.$saturation.value), + brightness: CGFloat(object.$brightness.value), + alpha: 1.0 + ) + field.primitiveValue = color + return color + } + ) + var color: UIColor + + @Field.Virtual( + "colorText", + customGetter: { object, field in + + if let colorText = field.primitiveValue { + + return colorText + } + let colorText = "H: \(object.$hue.value * 359)˚, S: \(round(object.$saturation.value * 100.0))%, B: \(round(object.$brightness.value * 100.0))%" + field.primitiveValue = colorText + return colorText + } + ) + var colorText: String + + func setRandomHue() { + + self.hue = Self.randomHue() + } + + + // MARK: Private + + private static func resetVirtualProperties(_ object: ObjectProxy) { + + object.$colorName.primitiveValue = nil + object.$color.primitiveValue = nil + object.$colorText.primitiveValue = nil + } + + private static func randomHue() -> Float { + + return Float.random(in: 0.0 ... 1.0) + } + + private static func randomSaturation() -> Float { + + return Float.random(in: 0.0 ... 1.0) + } + + private static func randomBrightness() -> Float { + + return Float.random(in: 0.0 ... 1.0) + } + } +} diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.DetailView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.DetailView.swift new file mode 100644 index 0000000..a368896 --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.DetailView.swift @@ -0,0 +1,166 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import Combine +import CoreStore +import SwiftUI + +// MARK: - Modern.ColorsDemo.SwiftUI + +extension Modern.ColorsDemo.SwiftUI { + + // MARK: - Modern.ColorsDemo.SwiftUI.DetailView + + struct DetailView: View { + + /** + ⭐️ Sample 1: Setting an `ObjectPublisher` declared as an `@ObservedObject` + */ + @ObservedObject + private var palette: ObjectPublisher + + /** + ⭐️ Sample 2: Setting properties that can be binded to controls (`Slider` in this case) by creating custom `@Binding` instances that updates the store when the values change. + */ + @Binding + private var hue: Float + + @Binding + private var saturation: Float + + @Binding + private var brightness: Float + + init(_ palette: ObjectPublisher) { + + self.palette = palette + self._hue = Binding( + get: { palette.hue ?? 0 }, + set: { percentage in + + Modern.ColorsDemo.dataStack.perform( + asynchronous: { (transaction) in + + let palette = palette.asEditable(in: transaction) + palette?.hue = percentage + }, + completion: { _ in } + ) + } + ) + self._saturation = Binding( + get: { palette.saturation ?? 0 }, + set: { percentage in + + Modern.ColorsDemo.dataStack.perform( + asynchronous: { (transaction) in + + let palette = palette.asEditable(in: transaction) + palette?.saturation = percentage + }, + completion: { _ in } + ) + } + ) + self._brightness = Binding( + get: { palette.brightness ?? 0 }, + set: { percentage in + + Modern.ColorsDemo.dataStack.perform( + asynchronous: { (transaction) in + + let palette = palette.asEditable(in: transaction) + palette?.brightness = percentage + }, + completion: { _ in } + ) + } + ) + } + + + // MARK: View + + var body: some View { + + guard let snapshot = self.palette.snapshot else { + + return AnyView(EmptyView()) + } + return AnyView( + GeometryReader { geometry in + ZStack(alignment: .bottom) { + Color(snapshot.$color) + ZStack { + Color.white + .cornerRadius(10) + .shadow(color: Color(.sRGB, white: 0.5, opacity: 0.3), radius: 2, x: 1, y: 1) + VStack(alignment: .leading, spacing: 10) { + HStack { + Text("H: \(Int(snapshot.$hue * 359))°") + .frame(width: 80) + Slider( + value: self.$hue, + in: 0 ... 1, + step: 1 / 359 + ) + } + HStack { + Text("S: \(Int(snapshot.$saturation * 100))%") + .frame(width: 80) + Slider( + value: self.$saturation, + in: 0 ... 1, + step: 1 / 100 + ) + } + HStack { + Text("B: \(Int(snapshot.$brightness * 100))%") + .frame(width: 80) + Slider( + value: self.$brightness, + in: 0 ... 1, + step: 1 / 100 + ) + } + } + .foregroundColor(Color(.sRGB, white: 0, opacity: 0.8)) + .padding() + } + .fixedSize(horizontal: false, vertical: true) + .padding() + .padding(geometry.safeAreaInsets) + } + } + ) + } + } +} + +#if DEBUG + +struct _Demo_Modern_ColorsDemo_SwiftUI_DetailView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + try! Modern.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + guard (try transaction.fetchCount(From())) <= 0 else { + return + } + let palette = transaction.create(Into()) + palette.setRandomHue() + } + ) + + return Modern.ColorsDemo.SwiftUI.DetailView( + Modern.ColorsDemo.palettesPublisher.snapshot.first! + ) + } +} + +#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ItemView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ItemView.swift new file mode 100644 index 0000000..97dbee9 --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ItemView.swift @@ -0,0 +1,76 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import SwiftUI + +// MARK: - Modern.ColorsDemo.SwiftUI + +extension Modern.ColorsDemo.SwiftUI { + + // MARK: - Modern.ColorsDemo.SwiftUI.ItemView + + struct ItemView: View { + + /** + ⭐️ Sample 1: Setting an `ObjectPublisher` declared as an `@ObservedObject` + */ + @ObservedObject + private var palette: ObjectPublisher + + + // MARK: Internal + + internal init(_ palette: ObjectPublisher) { + + self.palette = palette + } + + + // MARK: View + + var body: some View { + + guard let palette = self.palette.snapshot else { + + return AnyView(EmptyView()) + } + return AnyView( + HStack { + Color(palette.$color) + .cornerRadius(5) + .frame(width: 30, height: 30, alignment: .leading) + Text(palette.$colorText) + } + ) + } + } +} + +#if DEBUG + +struct _Demo_Modern_ColorsDemo_SwiftUI_ItemView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + try! Modern.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + guard (try transaction.fetchCount(From())) <= 0 else { + return + } + let palette = transaction.create(Into()) + palette.setRandomHue() + } + ) + + return Modern.ColorsDemo.SwiftUI.ItemView( + Modern.ColorsDemo.palettesPublisher.snapshot.first! + ) + } +} + +#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ListView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ListView.swift new file mode 100644 index 0000000..fee31e2 --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ListView.swift @@ -0,0 +1,118 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import SwiftUI + +// MARK: - Modern.ColorsDemo.SwiftUI + +extension Modern.ColorsDemo.SwiftUI { + + // MARK: - Modern.ColorsDemo.SwiftUI.ListView + + struct ListView: View { + + /** + ⭐️ Sample 1: Setting a sectioned `ListPublisher` declared as an `@ObservedObject` + */ + @ObservedObject + private var listPublisher: ListPublisher + + /** + ⭐️ Sample 2: Assigning sections and items of the `ListPublisher` to corresponding `View`s + */ + var body: some View { + let listSnapshot = self.listPublisher.snapshot + return List { + ForEach(listSnapshot.sectionIDs, id: \.self) { (sectionID) in + Section(header: Text(sectionID)) { + ForEach(listSnapshot.items(inSectionWithID: sectionID), id: \.self) { palette in + Button( + action: { + self.onPaletteTapped(palette) + }, + label: { + Modern.ColorsDemo.SwiftUI.ItemView(palette) + } + ) + } + .onDelete { itemIndices in + + self.deleteColors(at: itemIndices, in: sectionID) + } + } + } + GeometryReader { geometry in + Spacer(minLength: geometry.safeAreaInsets.bottom) + } + } + .listStyle(PlainListStyle()) + } + + + // MARK: Internal + + init( + listPublisher: ListPublisher, + onPaletteTapped: @escaping (ObjectPublisher) -> Void + ) { + + self.listPublisher = listPublisher + self.onPaletteTapped = onPaletteTapped + } + + + // MARK: Private + + private let onPaletteTapped: (ObjectPublisher) -> Void + + private func deleteColors(at indices: IndexSet, in sectionID: String) { + + let objectIDsToDelete = self.listPublisher.snapshot.itemIDs( + inSectionWithID: sectionID, + atIndices: indices + ) + Modern.ColorsDemo.dataStack.perform( + asynchronous: { transaction in + + transaction.delete(objectIDs: objectIDsToDelete) + }, + completion: { _ in } + ) + } + } +} + +#if DEBUG + +struct _Demo_Modern_ColorsDemo_SwiftUI_ListView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + let minimumSamples = 10 + try! Modern.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + let missing = minimumSamples + - (try transaction.fetchCount(From())) + guard missing > 0 else { + return + } + for _ in 0..()) + palette.setRandomHue() + } + } + ) + return Modern.ColorsDemo.SwiftUI.ListView( + listPublisher: Modern.ColorsDemo.palettesPublisher, + onPaletteTapped: { _ in } + ) + } +} + +#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.swift new file mode 100644 index 0000000..04d723b --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.swift @@ -0,0 +1,13 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + + +// MARK: - Modern.ColorsDemo + +extension Modern.ColorsDemo { + + // MARK: - SwiftUI + + enum SwiftUI {} +} diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailView.swift new file mode 100644 index 0000000..edae002 --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailView.swift @@ -0,0 +1,69 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import Combine +import CoreStore +import SwiftUI + +// MARK: - Modern.ColorsDemo.UIKit + +extension Modern.ColorsDemo.UIKit { + + // MARK: - Modern.ColorsDemo.UIKit.DetailView + + struct DetailView: UIViewControllerRepresentable { + + // MARK: Internal + + init(_ palette: ObjectPublisher) { + + self.palette = palette + } + + // MARK: UIViewControllerRepresentable + + typealias UIViewControllerType = Modern.ColorsDemo.UIKit.DetailViewController + + func makeUIViewController(context: Self.Context) -> UIViewControllerType { + + return UIViewControllerType(self.palette) + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {} + + static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {} + + + // MARK: Private + + private let palette: ObjectPublisher + } +} + +#if DEBUG + +struct _Demo_Modern_ColorsDemo_UIKit_DetailView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + try! Modern.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + guard (try transaction.fetchCount(From())) <= 0 else { + return + } + let palette = transaction.create(Into()) + palette.setRandomHue() + } + ) + + return Modern.ColorsDemo.UIKit.DetailView( + Modern.ColorsDemo.palettesPublisher.snapshot.first! + ) + } +} + +#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailViewController.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailViewController.swift new file mode 100644 index 0000000..7eb8b92 --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailViewController.swift @@ -0,0 +1,285 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import UIKit + + +// MARK: - Modern.ColorsDemo.UIKit + +extension Modern.ColorsDemo.UIKit { + + // MARK: - Modern.ColorsDemo.UIKit.DetailViewController + + final class DetailViewController: UIViewController, ObjectObserver { + + /** + ⭐️ Sample 1: We can normally use `ObjectPublisher` directly, which is simpler. But for this demo, we will be using `ObjectMonitor` instead because we need to keep track of which properties change to prevent our `UISlider` from stuttering. Refer to the `objectMonitor(_:didUpdateObject:changedPersistentKeys:)` implementation below. + */ + init(_ palette: ObjectPublisher) { + + self.palette = Modern.ColorsDemo.dataStack.monitorObject( + palette.object! + ) + super.init(nibName: nil, bundle: nil) + } + + /** + ⭐️ Sample 2: Once the views are created, we can start receiving `ObjectMonitor` updates in our `ObjectObserver` conformance methods. We typically call this at the end of `viewDidLoad`. Note that after the `addObserver` call, only succeeding updates will trigger our `ObjectObserver` methods, so to immediately display the current values, we need to initialize our views once (in this case, using `reloadPaletteInfo(_:changedKeys:)`. + */ + private func startMonitoringObject() { + + self.palette.addObserver(self) + if let palette = self.palette.object { + + self.reloadPaletteInfo(palette, changedKeys: nil) + } + } + + /** + ⭐️ Sample 3: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ObjectMonitor`s safely remove deallocated observers automatically. + */ + deinit { + + self.palette.removeObserver(self) + } + + /** + ⭐️ Sample 4: Our `objectMonitor(_:didUpdateObject:changedPersistentKeys:)` implementation passes a `Set` to our reload method. We can then inspect which values were triggered by each `UISlider`, so we can avoid double-updates that can lag the `UISlider` dragging. + */ + func reloadPaletteInfo( + _ palette: Modern.ColorsDemo.Palette, + changedKeys: Set? + ) { + + self.view.backgroundColor = palette.color + + self.hueLabel.text = "H: \(Int(palette.hue * 359))°" + self.saturationLabel.text = "S: \(Int(palette.saturation * 100))%" + self.brightnessLabel.text = "B: \(Int(palette.brightness * 100))%" + + if changedKeys == nil + || changedKeys?.contains(String(keyPath: \Modern.ColorsDemo.Palette.$hue)) == true { + + self.hueSlider.value = Float(palette.hue) + } + if changedKeys == nil + || changedKeys?.contains(String(keyPath: \Modern.ColorsDemo.Palette.$saturation)) == true { + + self.saturationSlider.value = palette.saturation + } + if changedKeys == nil + || changedKeys?.contains(String(keyPath: \Modern.ColorsDemo.Palette.$brightness)) == true { + + self.brightnessSlider.value = palette.brightness + } + } + + + // MARK: ObjectObserver + + func objectMonitor( + _ monitor: ObjectMonitor, + didUpdateObject object: Modern.ColorsDemo.Palette, + changedPersistentKeys: Set + ) { + + self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys) + } + + + // MARK: UIViewController + + override func viewDidLoad() { + + super.viewDidLoad() + + let view = self.view! + let containerView = UIView() + do { + containerView.translatesAutoresizingMaskIntoConstraints = false + containerView.backgroundColor = UIColor.white + containerView.layer.cornerRadius = 10 + containerView.layer.masksToBounds = true + containerView.layer.shadowColor = UIColor(white: 0.5, alpha: 0.3).cgColor + containerView.layer.shadowOffset = .init(width: 1, height: 1) + containerView.layer.shadowRadius = 2 + + view.addSubview(containerView) + } + + let vStackView = UIStackView() + do { + vStackView.translatesAutoresizingMaskIntoConstraints = false + vStackView.axis = .vertical + vStackView.spacing = 10 + vStackView.distribution = .fill + vStackView.alignment = .fill + + containerView.addSubview(vStackView) + } + + let palette = self.palette.object + let rows: [(label: UILabel, slider: UISlider, initialValue: Float, sliderValueChangedSelector: Selector)] = [ + ( + self.hueLabel, + self.hueSlider, + palette?.hue ?? 0, + #selector(self.hueSliderValueDidChange(_:)) + ), + ( + self.saturationLabel, + self.saturationSlider, + palette?.saturation ?? 0, + #selector(self.saturationSliderValueDidChange(_:)) + ), + ( + self.brightnessLabel, + self.brightnessSlider, + palette?.brightness ?? 0, + #selector(self.brightnessSliderValueDidChange(_:)) + ) + ] + for (label, slider, initialValue, sliderValueChangedSelector) in rows { + + let hStackView = UIStackView() + do { + hStackView.translatesAutoresizingMaskIntoConstraints = false + hStackView.axis = .horizontal + hStackView.spacing = 5 + hStackView.distribution = .fill + hStackView.alignment = .center + + vStackView.addArrangedSubview(hStackView) + } + do { + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor(white: 0, alpha: 0.8) + label.textAlignment = .center + + hStackView.addArrangedSubview(label) + } + do { + slider.translatesAutoresizingMaskIntoConstraints = false + slider.minimumValue = 0 + slider.maximumValue = 1 + slider.value = initialValue + slider.addTarget( + self, + action: sliderValueChangedSelector, + for: .valueChanged + ) + + hStackView.addArrangedSubview(slider) + } + } + + layout: do { + + NSLayoutConstraint.activate( + [ + containerView.leadingAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.leadingAnchor, + constant: 10 + ), + containerView.bottomAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.bottomAnchor, + constant: -10 + ), + containerView.trailingAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.trailingAnchor, + constant: -10 + ), + + vStackView.topAnchor.constraint( + equalTo: containerView.topAnchor, + constant: 15 + ), + vStackView.leadingAnchor.constraint( + equalTo: containerView.leadingAnchor, + constant: 15 + ), + vStackView.bottomAnchor.constraint( + equalTo: containerView.bottomAnchor, + constant: -15 + ), + vStackView.trailingAnchor.constraint( + equalTo: containerView.trailingAnchor, + constant: -15 + ) + ] + ) + NSLayoutConstraint.activate( + rows.map { label, _, _, _ in + label.widthAnchor.constraint(equalToConstant: 80) + } + ) + } + + self.startMonitoringObject() + } + + + // MARK: Private + + private let palette: ObjectMonitor + + @available(*, unavailable) + required init?(coder: NSCoder) { + + fatalError() + } + + private let hueLabel: UILabel = .init() + private let saturationLabel: UILabel = .init() + private let brightnessLabel: UILabel = .init() + private let hueSlider: UISlider = .init() + private let saturationSlider: UISlider = .init() + private let brightnessSlider: UISlider = .init() + + @objc + private dynamic func hueSliderValueDidChange(_ sender: UISlider) { + + let value = sender.value + Modern.ColorsDemo.dataStack.perform( + asynchronous: { [weak self] (transaction) in + + let palette = transaction.edit(self?.palette.object) + palette?.hue = value + }, + completion: { _ in } + ) + } + + @objc + private dynamic func saturationSliderValueDidChange(_ sender: UISlider) { + + let value = sender.value + Modern.ColorsDemo.dataStack.perform( + asynchronous: { [weak self] (transaction) in + + let palette = transaction.edit(self?.palette.object) + palette?.saturation = value + }, + completion: { _ in } + ) + } + + @objc + private dynamic func brightnessSliderValueDidChange(_ sender: UISlider) { + + let value = sender.value + Modern.ColorsDemo.dataStack.perform( + asynchronous: { [weak self] (transaction) in + + let palette = transaction.edit(self?.palette.object) + palette?.brightness = value + }, + completion: { _ in } + ) + } + } +} + + diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ItemCell.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ItemCell.swift new file mode 100644 index 0000000..2d83029 --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ItemCell.swift @@ -0,0 +1,31 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import UIKit + + +// MARK: - Modern.ColorsDemo.UIKit + +extension Modern.ColorsDemo.UIKit { + + // MARK: - Modern.ColorsDemo.UIKit.ItemCell + + final class ItemCell: UITableViewCell { + + // MARK: Internal + + static let reuseIdentifier: String = NSStringFromClass(Modern.ColorsDemo.UIKit.ItemCell.self) + + func setPalette(_ palette: Modern.ColorsDemo.Palette) { + + self.imageView?.image = UIImage( + color: palette.color, + size: .init(width: 30, height: 30), + cornerRadius: 5 + ) + self.textLabel?.text = palette.colorText + } + } +} diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListView.swift new file mode 100644 index 0000000..1965bc8 --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListView.swift @@ -0,0 +1,89 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import SwiftUI + +// MARK: - Modern.ColorsDemo.UIKit + +extension Modern.ColorsDemo.UIKit { + + // MARK: - Modern.ColorsDemo.UIKit.ListView + + struct ListView: UIViewControllerRepresentable { + + // MARK: Internal + + init( + listPublisher: ListPublisher, + onPaletteTapped: @escaping (ObjectPublisher) -> Void + ) { + + self.listPublisher = listPublisher + self.onPaletteTapped = onPaletteTapped + } + + + // MARK: UIViewControllerRepresentable + + typealias UIViewControllerType = Modern.ColorsDemo.UIKit.ListViewController + + func makeUIViewController(context: Self.Context) -> UIViewControllerType { + + return UIViewControllerType( + listPublisher: self.listPublisher, + onPaletteTapped: self.onPaletteTapped + ) + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) { + + uiViewController.setEditing( + context.environment.editMode?.wrappedValue.isEditing == true, + animated: true + ) + } + + static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {} + + + // MARK: Private + + private let listPublisher: ListPublisher + private let onPaletteTapped: (ObjectPublisher) -> Void + } +} + +#if DEBUG + +struct _Demo_Modern_ColorsDemo_UIKit_ListView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + let minimumSamples = 10 + try! Modern.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + let missing = minimumSamples + - (try transaction.fetchCount(From())) + guard missing > 0 else { + return + } + for _ in 0..()) + palette.setRandomHue() + } + } + ) + return Modern.ColorsDemo.UIKit.ListView( + listPublisher: Modern.ColorsDemo.palettesPublisher, + onPaletteTapped: { _ in } + ) + } +} + +#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListViewController.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListViewController.swift new file mode 100644 index 0000000..c18e543 --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListViewController.swift @@ -0,0 +1,139 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import UIKit + + +// MARK: - Modern.ColorsDemo.UIKit + +extension Modern.ColorsDemo.UIKit { + + // MARK: - Modern.ColorsDemo.UIKit.ListViewController + + final class ListViewController: UITableViewController { + + /** + ⭐️ Sample 1: Setting up a `DiffableDataSource.TableViewAdapter` that will manage tableView snapshot updates automatically. We can use the built-in `DiffableDataSource.TableViewAdapter` type directly, but in our case we want to enabled `UITableView` cell deletions so we create a custom subclass `DeletionEnabledDataSource` (see declatation below). + */ + private lazy var dataSource: DiffableDataSource.TableViewAdapter = DeletionEnabledDataSource( + tableView: self.tableView, + dataStack: Modern.ColorsDemo.dataStack, + cellProvider: { (tableView, indexPath, palette) in + + let cell = tableView.dequeueReusableCell( + withIdentifier: Modern.ColorsDemo.UIKit.ItemCell.reuseIdentifier, + for: indexPath + ) as! Modern.ColorsDemo.UIKit.ItemCell + cell.setPalette(palette) + return cell + } + ) + + /** + ⭐️ Sample 2: Once the views are created, we can start binding `ListPublisher` updates to the `DiffableDataSource`. We typically call this at the end of `viewDidLoad`. Note that the `addObserver`'s closure argument will only be called on the succeeding updates, so to immediately display the current values, we need to call `dataSource.apply()` once. + */ + private func startObservingList() { + + self.listPublisher.addObserver(self) { (listPublisher) in + + self.dataSource.apply( + listPublisher.snapshot, + animatingDifferences: true + ) + } + self.dataSource.apply( + listPublisher.snapshot, + animatingDifferences: false + ) + } + + /** + ⭐️ Sample 3: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ListPublisher`s safely remove deallocated observers automatically. + */ + deinit { + + self.listPublisher.removeObserver(self) + } + + /** + ⭐️ Sample 4: This is the custom `DiffableDataSource.TableViewAdapter` subclass we wrote that enabled swipe-to-delete gestures on the `UITableView`. + */ + final class DeletionEnabledDataSource: DiffableDataSource.TableViewAdapter { + + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + + switch editingStyle { + + case .delete: + guard let itemID = self.itemID(for: indexPath) else { + + return + } + self.dataStack.perform( + asynchronous: { (transaction) in + + transaction.delete(objectIDs: [itemID]) + }, + completion: { _ in } + ) + + default: + break + } + } + } + + + // MARK: Internal + + init( + listPublisher: ListPublisher, + onPaletteTapped: @escaping (ObjectPublisher) -> Void + ) { + + self.listPublisher = listPublisher + self.onPaletteTapped = onPaletteTapped + + super.init(style: .plain) + } + + + // MARK: UIViewController + + override func viewDidLoad() { + + super.viewDidLoad() + + self.tableView.register( + Modern.ColorsDemo.UIKit.ItemCell.self, + forCellReuseIdentifier: Modern.ColorsDemo.UIKit.ItemCell.reuseIdentifier + ) + + self.startObservingList() + } + + + // MARK: UITableViewDelegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + self.onPaletteTapped( + self.listPublisher.snapshot[indexPath] + ) + } + + + // MARK: Private + + private let listPublisher: ListPublisher + private let onPaletteTapped: (ObjectPublisher) -> Void + + @available(*, unavailable) + required init?(coder: NSCoder) { + + fatalError() + } + } +} diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.swift new file mode 100644 index 0000000..5c3f1ea --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.swift @@ -0,0 +1,13 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + + +// MARK: - Modern.ColorsDemo + +extension Modern.ColorsDemo { + + // MARK: - UIKit + + enum UIKit {} +} diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift new file mode 100644 index 0000000..d8b7ed0 --- /dev/null +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift @@ -0,0 +1,66 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + +// MARK: - Modern + +extension Modern { + + // MARK: - Modern.ColorsDemo + + /** + Sample usages for observing lists or single instances of `CoreStoreObject`s + */ + enum ColorsDemo { + + // MARK: Internal + + static let dataStack: DataStack = { + + let dataStack = DataStack( + CoreStoreSchema( + modelVersion: "V1", + entities: [ + Entity("Palette") + ], + versionLock: [ + "Palette": [0xbaf4eaee9353176a, 0xdd6ca918cc2b0c38, 0xd04fad8882d7cc34, 0x3e90ca38c091503f] + ] + ) + ) + + /** + - Important: `addStorageAndWait(_:)` was used here to simplify initializing the demo, but in practice the asynchronous function variants are recommended. + */ + try! dataStack.addStorageAndWait( + SQLiteStore( + fileName: "Modern.ColorsDemo.sqlite", + localStorageOptions: .recreateStoreOnModelMismatch + ) + ) + return dataStack + }() + + static let palettesPublisher: ListPublisher = Modern.ColorsDemo.dataStack.publishList( + From() + .sectionBy(\.$colorName) + .where(Modern.ColorsDemo.filter.whereClause()) + .orderBy(.ascending(\.$hue)) + ) + + static var filter: Modern.ColorsDemo.Filter = .all { + + didSet { + + try! Modern.ColorsDemo.palettesPublisher.refetch( + From() + .sectionBy(\.$colorName) + .where(self.filter.whereClause()) + .orderBy(.ascending(\.$hue)) + ) + } + } + } +} diff --git a/Demo/Sources/Demos/Modern/Modern.swift b/Demo/Sources/Demos/Modern/Modern.swift new file mode 100644 index 0000000..84af48b --- /dev/null +++ b/Demo/Sources/Demos/Modern/Modern.swift @@ -0,0 +1,10 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +// MARK: - Modern + +/** + Sample usages for `CoreStoreObject` subclasses + */ +enum Modern {} diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Geocoder.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Geocoder.swift new file mode 100644 index 0000000..a8c3934 --- /dev/null +++ b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Geocoder.swift @@ -0,0 +1,67 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import Contacts +import CoreLocation +import CoreStore + + +// MARK: - Modern.PlacemarksDemo + +extension Modern.PlacemarksDemo { + + // MARK: Geocoder + + final class Geocoder { + + // MARK: Internal + + func geocode( + place: ObjectSnapshot, + completion: @escaping (_ title: String?, _ subtitle: String?) -> Void + ) { + + self.geocoder?.cancelGeocode() + + let geocoder = CLGeocoder() + self.geocoder = geocoder + geocoder.reverseGeocodeLocation( + CLLocation(latitude: place.$latitude, longitude: place.$longitude), + completionHandler: { (placemarks, error) -> Void in + + defer { + + self.geocoder = nil + } + guard let placemark = placemarks?.first else { + + return + } + + let address = CNMutablePostalAddress() + address.street = placemark.thoroughfare ?? "" + address.subLocality = placemark.subThoroughfare ?? "" + address.city = placemark.locality ?? "" + address.subAdministrativeArea = placemark.subAdministrativeArea ?? "" + address.state = placemark.administrativeArea ?? "" + address.postalCode = placemark.postalCode ?? "" + address.country = placemark.country ?? "" + address.isoCountryCode = placemark.isoCountryCode ?? "" + + completion( + placemark.name, + CNPostalAddressFormatter.string( + from: address, + style: .mailingAddress + ) + ) + } + ) + } + + // MARK: Private + + private var geocoder: CLGeocoder? + } +} diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift new file mode 100644 index 0000000..bfa3e4f --- /dev/null +++ b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift @@ -0,0 +1,153 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreLocation +import Combine +import CoreStore +import Foundation +import MapKit +import SwiftUI + +// MARK: - Modern.PlacemarksDemo + +extension Modern.PlacemarksDemo { + + // MARK: - Modern.PlacemarksDemo.MainView + + struct MainView: View { + + /** + ⭐️ Sample 1: Asynchronous transactions + */ + private func demoAsynchronousTransaction(coordinate: CLLocationCoordinate2D) { + + Modern.PlacemarksDemo.dataStack.perform( + asynchronous: { (transaction) in + + let place = self.place.asEditable(in: transaction) + place?.annotation = .init(coordinate: coordinate) + }, + completion: { _ in } + ) + } + + /** + ⭐️ Sample 2: Synchronous transactions + + - Important: `perform(synchronous:)` was used here for illustration purposes. In practice, `perform(asynchronous:completion:)` is the preferred transaction type as synchronous transactions are very likely to cause deadlocks. + */ + private func demoSynchronousTransaction() { + + _ = try? Modern.PlacemarksDemo.dataStack.perform( + synchronous: { (transaction) in + + let place = self.place.asEditable(in: transaction) + place?.setRandomLocation() + } + ) + } + + /** + ⭐️ Sample 3: Unsafe transactions + + - Important: `beginUnsafe()` was used here for illustration purposes. In practice, `perform(asynchronous:completion:)` is the preferred transaction type. Use Unsafe Transactions only when you need to bypass CoreStore's serialized transactions. + */ + private func demoUnsafeTransaction( + title: String?, + subtitle: String?, + for snapshot: ObjectSnapshot + ) { + let transaction = Modern.PlacemarksDemo.dataStack.beginUnsafe() + let place = snapshot.asEditable(in: transaction) + place?.title = title + place?.subtitle = subtitle + + transaction.commit { (error) in + + print("Commit failed: \(error as Any)") + } + } + + // MARK: Internal + + @ObservedObject + var place: ObjectPublisher + + init() { + + self.place = Modern.PlacemarksDemo.placePublisher + self.sinkCancellable = self.place.sink( + receiveCompletion: { _ in + + // Deleted, do nothing + }, + receiveValue: { [self] (snapshot) in + + self.geocoder.geocode(place: snapshot) { (title, subtitle) in + + guard self.place.snapshot == snapshot else { + + return + } + self.demoUnsafeTransaction( + title: title, + subtitle: subtitle, + for: snapshot + ) + } + } + ) + } + + + // MARK: View + + var body: some View { + Modern.PlacemarksDemo.MapView( + place: self.place.snapshot, + onTap: { coordinate in + + self.demoAsynchronousTransaction(coordinate: coordinate) + } + ) + .overlay( + InstructionsView( + ("Random", "Sets random coordinate"), + ("Tap", "Sets to tapped coordinate") + ) + .padding(.leading, 10) + .padding(.bottom, 40), + alignment: .bottomLeading + ) + .navigationBarTitle("Placemarks") + .navigationBarItems( + trailing: Button("Random") { + + self.demoSynchronousTransaction() + } + ) + } + + + // MARK: Private + + private var sinkCancellable: AnyCancellable? = nil + private let geocoder = Modern.PlacemarksDemo.Geocoder() + } +} + + +#if DEBUG + +struct _Demo_Modern_PlacemarksDemo_MainView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + Modern.PlacemarksDemo.MainView() + } +} + +#endif diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MapView.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MapView.swift new file mode 100644 index 0000000..ed4b053 --- /dev/null +++ b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MapView.swift @@ -0,0 +1,119 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreLocation +import CoreStore +import MapKit +import UIKit +import SwiftUI + +// MARK: - Modern.PlacemarksDemo + +extension Modern.PlacemarksDemo { + + // MARK: - Modern.PlacemarksDemo.MapView + + struct MapView: UIViewRepresentable { + + // MARK: Internal + + var place: ObjectSnapshot? + + let onTap: (CLLocationCoordinate2D) -> Void + + // MARK: UIViewRepresentable + + typealias UIViewType = MKMapView + + func makeUIView(context: Context) -> UIViewType { + + let coordinator = context.coordinator + + let mapView = MKMapView() + mapView.delegate = coordinator + mapView.addGestureRecognizer( + UITapGestureRecognizer( + target: coordinator, + action: #selector(coordinator.tapGestureRecognized(_:)) + ) + ) + return mapView + } + + func updateUIView(_ view: UIViewType, context: Context) { + + let currentAnnotations = view.annotations + view.removeAnnotations(currentAnnotations) + + guard let newAnnotation = self.place?.$annotation else { + + return + } + view.addAnnotation(newAnnotation) + view.setCenter(newAnnotation.coordinate, animated: true) + view.selectAnnotation(newAnnotation, animated: true) + } + + func makeCoordinator() -> Coordinator { + + Coordinator(self) + } + + final class Coordinator: NSObject, MKMapViewDelegate { + + // MARK: Internal + + init(_ parent: MapView) { + + self.parent = parent + } + + // MARK: MKMapViewDelegate + + @objc dynamic func mapView( + _ mapView: MKMapView, + viewFor annotation: MKAnnotation + ) -> MKAnnotationView? { + + let identifier = "MKAnnotationView" + var annotationView: MKPinAnnotationView! = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView + if annotationView == nil { + + annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier) + annotationView.isEnabled = true + annotationView.canShowCallout = true + annotationView.animatesDrop = true + } + else { + + annotationView.annotation = annotation + } + return annotationView + } + + // MARK: FilePrivate + + @objc + fileprivate dynamic func tapGestureRecognized(_ gesture: UILongPressGestureRecognizer) { + + guard + case let mapView as MKMapView = gesture.view, + gesture.state == .recognized + else { + + return + } + let coordinate = mapView.convert( + gesture.location(in: mapView), + toCoordinateFrom: mapView + ) + self.parent.onTap(coordinate) + } + + // MARK: Private + + private var parent: MapView + } + } +} diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Place.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Place.swift new file mode 100644 index 0000000..7d07142 --- /dev/null +++ b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Place.swift @@ -0,0 +1,120 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import struct CoreLocation.CLLocationCoordinate2D +import protocol MapKit.MKAnnotation + +// MARK: - Modern.PlacemarksDemo + +extension Modern.PlacemarksDemo { + + // MARK: - Modern.PlacemarksDemo.Place + + final class Place: CoreStoreObject { + + // MARK: Internal + + @Field.Stored("latitude") + var latitude: Double = 0 + + @Field.Stored("longitude") + var longitude: Double = 0 + + @Field.Stored("title") + var title: String? + + @Field.Stored("subtitle") + var subtitle: String? + + @Field.Virtual( + "annotation", + customGetter: { object, field in + + Annotation( + latitude: object.$latitude.value, + longitude: object.$longitude.value, + title: object.$title.value, + subtitle: object.$subtitle.value + ) + }, + customSetter: { object, field, newValue in + + object.$latitude.value = newValue.coordinate.latitude + object.$longitude.value = newValue.coordinate.longitude + object.$title.value = "\(newValue.coordinate.latitude), \(newValue.coordinate.longitude)" + object.$subtitle.value = nil + } + ) + var annotation: Annotation + + func setRandomLocation() { + + self.latitude = Double(arc4random_uniform(180)) - 90 + self.longitude = Double(arc4random_uniform(360)) - 180 + self.title = "\(self.latitude), \(self.longitude)" + self.subtitle = nil + } + + // MARK: - Annotation + + final class Annotation: NSObject, MKAnnotation { + + // MARK: Internal + + init(coordinate: CLLocationCoordinate2D) { + + self.coordinate = coordinate + self.title = nil + self.subtitle = nil + } + + + // MARK: MKAnnotation + + let coordinate: CLLocationCoordinate2D + let title: String? + let subtitle: String? + + + // MARK: NSObjectProtocol + + override func isEqual(_ object: Any?) -> Bool { + + guard case let object as Annotation = object else { + + return false + } + return self.coordinate.latitude == object.coordinate.latitude + && self.coordinate.longitude == object.coordinate.longitude + && self.title == object.title + && self.subtitle == object.subtitle + } + + override var hash: Int { + + var hasher = Hasher() + hasher.combine(self.coordinate.latitude) + hasher.combine(self.coordinate.longitude) + hasher.combine(self.title) + hasher.combine(self.subtitle) + return hasher.finalize() + } + + + // MARK: FilePrivate + + fileprivate init( + latitude: Double, + longitude: Double, + title: String?, + subtitle: String? + ) { + self.coordinate = .init(latitude: latitude, longitude: longitude) + self.title = title + self.subtitle = subtitle + } + } + } +} diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift new file mode 100644 index 0000000..87057f7 --- /dev/null +++ b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift @@ -0,0 +1,64 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + +// MARK: - Modern + +extension Modern { + + // MARK: - Modern.PlacemarksDemo + + /** + Sample usages for `CoreStoreObject` transactions + */ + enum PlacemarksDemo { + + // MARK: Internal + + static let dataStack: DataStack = { + + let dataStack = DataStack( + CoreStoreSchema( + modelVersion: "V1", + entities: [ + Entity("Place") + ], + versionLock: [ + "Place": [0xa7eec849af5e8fcb, 0x638e69c040090319, 0x4e976d66ed400447, 0x18e96bc0438d07bb] + ] + ) + ) + + /** + - Important: `addStorageAndWait(_:)` and `perform(synchronous:)` methods were used here to simplify initializing the demo, but in practice the asynchronous function variants are recommended. + */ + try! dataStack.addStorageAndWait( + SQLiteStore( + fileName: "Modern.PlacemarksDemo.sqlite", + localStorageOptions: .recreateStoreOnModelMismatch + ) + ) + return dataStack + }() + + static let placePublisher: ObjectPublisher = { + + let dataStack = Modern.PlacemarksDemo.dataStack + if let place = try! dataStack.fetchOne(From()) { + + return dataStack.publishObject(place) + } + _ = try! dataStack.perform( + synchronous: { (transaction) in + + let place = transaction.create(Into()) + place.setRandomLocation() + } + ) + let place = try! dataStack.fetchOne(From()) + return dataStack.publishObject(place!) + }() + } +} diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Ability.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Ability.swift new file mode 100644 index 0000000..22cde5b --- /dev/null +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Ability.swift @@ -0,0 +1,33 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + +// MARK: - Modern.PokedexDemo + +extension Modern.PokedexDemo { + + // MARK: - Modern.PokedexDemo.Ability + + final class Ability: CoreStoreObject { + + // MARK: Internal + + @Field.Stored("id") + var id: Int = 0 + + @Field.Stored("name") + var name: String = "" + + @Field.Stored("text") + var text: String = "" + + @Field.Stored("isHiddenAbility") + var isHiddenAbility: Bool = false + + + @Field.Relationship("learners") + var learners: Set + } +} diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Move.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Move.swift new file mode 100644 index 0000000..13cfcf9 --- /dev/null +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Move.swift @@ -0,0 +1,48 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + +// MARK: - Modern.PokedexDemo + +extension Modern.PokedexDemo { + + // MARK: - Modern.PokedexDemo.Move + + final class Move: CoreStoreObject { + + // MARK: Internal + + @Field.Stored("id") + var id: Int = 0 + + @Field.Stored("name") + var name: String = "" + + @Field.Stored("text") + var text: String = "" + + @Field.Stored("pokemonType") + var pokemonType: Modern.PokedexDemo.PokemonType = .normal + + @Field.Stored("power") + var power: Int = 0 + + @Field.Stored("accuracy") + var accuracy: Int = 0 + + @Field.Stored("powerPoints") + var powerPoints: Int = 0 + + @Field.Stored("effectChance") + var effectChance: Int = 0 + + @Field.Stored("priority") + var priority: Int = 0 + + + @Field.Relationship("learners") + var learners: Set + } +} diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonForm.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonForm.swift new file mode 100644 index 0000000..e0bb63d --- /dev/null +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonForm.swift @@ -0,0 +1,71 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + +// MARK: - Modern.PokedexDemo + +extension Modern.PokedexDemo { + + // MARK: - Modern.PokedexDemo.PokemonForm + + final class PokemonForm: CoreStoreObject { + + // MARK: Internal + + @Field.Stored("id") + var id: Int = 0 + + @Field.Stored("name") + var name: String = "" + + @Field.Stored("pokemonType1") + var pokemonType1: Modern.PokedexDemo.PokemonType = .normal + + @Field.Stored("pokemonType2") + var pokemonType2: Modern.PokedexDemo.PokemonType? + + @Field.Relationship("species") + var species: Modern.PokedexDemo.PokemonSpecies? + + + @Field.Stored("statHitPoints") + var statHitPoints: Int = 0 + + @Field.Stored("statAttack") + var statAttack: Int = 0 + + @Field.Stored("statDefense") + var statDefense: Int = 0 + + @Field.Stored("statSpecialAttack") + var statSpecialAttack: Int = 0 + + @Field.Stored("statSpecialDefense") + var statSpecialDefense: Int = 0 + + @Field.Stored("statSpeed") + var statSpeed: Int = 0 + + + @Field.Stored("spriteFrontURL") + var spriteFrontURL: URL? + + @Field.Stored("spriteBackURL") + var spriteBackURL: URL? + + @Field.Stored("spriteShinyFrontURL") + var spriteShinyFrontURL: URL? + + @Field.Stored("spriteShinyBackURL") + var spriteShinyBackURL: URL? + + + @Field.Relationship("abilities", inverse: \.$learners) + var abilities: Set + + @Field.Relationship("moves", inverse: \.$learners) + var moves: Set + } +} diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonSpecies.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonSpecies.swift new file mode 100644 index 0000000..f8da54c --- /dev/null +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonSpecies.swift @@ -0,0 +1,29 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + +// MARK: - Modern.PokedexDemo + +extension Modern.PokedexDemo { + + // MARK: - Modern.PokedexDemo.PokemonSpecies + + final class PokemonSpecies: CoreStoreObject { + + // MARK: Internal + + @Field.Stored("id") + var id: Int = 0 + + @Field.Stored("name") + var name: String = "" + + @Field.Stored("weight") + var weight: Int = 0 + + @Field.Relationship("forms", inverse: \.$species) + var forms: Set + } +} diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonType.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonType.swift new file mode 100644 index 0000000..c3c1e6a --- /dev/null +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokemonType.swift @@ -0,0 +1,36 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + +// MARK: - Modern.PokedexDemo + +extension Modern.PokedexDemo { + + // MARK: - Modern.PokedexDemo.Move + + enum PokemonType: String, CaseIterable, FieldStorableType { + + // MARK: Internal + + case bug + case dark + case dragon + case electric + case fairy + case fighting + case fire + case flying + case ghost + case grass + case ground + case ice + case normal + case poison + case psychic + case rock + case steel + case water + } +} diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift new file mode 100644 index 0000000..95c9fab --- /dev/null +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift @@ -0,0 +1,66 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + +// MARK: - Modern + +extension Modern { + + // MARK: - Modern.PokedexDemo + + /** + Sample usages for importing external data into `CoreStoreObject` attributes + */ + enum PokedexDemo { + + // MARK: Internal + + static let dataStack: DataStack = { + + let dataStack = DataStack( + CoreStoreSchema( + modelVersion: "V1", + entities: [ + Entity("Palette") + ], + versionLock: [ + "Palette": [0xbaf4eaee9353176a, 0xdd6ca918cc2b0c38, 0xd04fad8882d7cc34, 0x3e90ca38c091503f] + ] + ) + ) + + /** + - Important: `addStorageAndWait(_:)` was used here to simplify initializing the demo, but in practice the asynchronous function variants are recommended. + */ + try! dataStack.addStorageAndWait( + SQLiteStore( + fileName: "Modern.ColorsDemo.sqlite", + localStorageOptions: .recreateStoreOnModelMismatch + ) + ) + return dataStack + }() + + static let palettesPublisher: ListPublisher = Modern.ColorsDemo.dataStack.publishList( + From() + .sectionBy(\.$colorName) + .where(Modern.ColorsDemo.filter.whereClause()) + .orderBy(.ascending(\.$hue)) + ) + + static var filter: Modern.ColorsDemo.Filter = .all { + + didSet { + + try! Modern.ColorsDemo.palettesPublisher.refetch( + From() + .sectionBy(\.$colorName) + .where(self.filter.whereClause()) + .orderBy(.ascending(\.$hue)) + ) + } + } + } +} diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift new file mode 100644 index 0000000..809a450 --- /dev/null +++ b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift @@ -0,0 +1,58 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import SwiftUI + +// MARK: - Modern.TimeZonesDemo + +extension Modern.TimeZonesDemo { + + // MARK: - Modern.TimeZonesDemo.ItemView + + struct ItemView: View { + + // MARK: Internal + + init(title: String, subtitle: String) { + self.title = title + self.subtitle = subtitle + } + + + // MARK: View + + var body: some View { + VStack(alignment: .leading) { + Text(self.title) + .font(.headline) + .foregroundColor(.primary) + Text(self.subtitle) + .font(.subheadline) + .foregroundColor(.secondary) + } + } + + + // MARK: FilePrivate + + fileprivate let title: String + fileprivate let subtitle: String + } +} + +#if DEBUG + +struct _Demo_Modern_TimeZone_ItemView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + Modern.TimeZonesDemo.ItemView( + title: "Item Title", + subtitle: "A subtitle caption for this item" + ) + } +} + +#endif diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ListView.swift b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ListView.swift new file mode 100644 index 0000000..ad8046c --- /dev/null +++ b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ListView.swift @@ -0,0 +1,94 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import SwiftUI + +// MARK: - Modern.TimeZonesDemo + +extension Modern.TimeZonesDemo { + + // MARK: - Modern.TimeZonesDemo.ListView + + struct ListView: View { + + // MARK: Internal + + init(title: String, objects: [Modern.TimeZonesDemo.TimeZone]) { + + self.title = title + self.values = objects.map { + (title: $0.name, subtitle: $0.abbreviation) + } + } + + init(title: String, value: Any?) { + + self.title = title + switch value { + + case (let array as [Any])?: + self.values = array.map { + ( + title: String(describing: $0), + dsubtitleetail: String(reflecting: type(of: $0)) + ) + } + + case let item?: + self.values = [ + ( + title: String(describing: item), + subtitle: String(reflecting: type(of: item)) + ) + ] + + case nil: + self.values = [] + } + } + + + // MARK: View + + var body: some View { + List { + ForEach(self.values, id: \.title) { item in + Modern.TimeZonesDemo.ItemView( + title: item.title, + subtitle: item.subtitle + ) + } + } + .navigationBarTitle(self.title) + } + + + // MARK: Private + + private let title: String + private let values: [(title: String, subtitle: String)] + } +} + + +#if DEBUG + +struct _Demo_Modern_TimeZonesDemo_ListView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + Modern.TimeZonesDemo.ListView( + title: "Title", + objects: try! Modern.TimeZonesDemo.dataStack.fetchAll( + From() + .orderBy(.ascending(\.$name)) + ) + ) + } +} + +#endif diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.MainView.swift b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.MainView.swift new file mode 100644 index 0000000..8f06df4 --- /dev/null +++ b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.MainView.swift @@ -0,0 +1,234 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import SwiftUI + +// MARK: - Modern.TimeZonesDemo + +extension Modern.TimeZonesDemo { + + // MARK: - Modern.TimeZonesDemo.MainView + + struct MainView: View { + + /** + ⭐️ Sample 1: Plain object fetch + */ + private func fetchAllTimeZones() -> [Modern.TimeZonesDemo.TimeZone] { + + return try! Modern.TimeZonesDemo.dataStack.fetchAll( + From() + .orderBy(.ascending(\.$secondsFromGMT)) + ) + } + + /** + ⭐️ Sample 2: Plain object fetch with simple `where` clause + */ + private func fetchTimeZonesWithDST() -> [Modern.TimeZonesDemo.TimeZone] { + + return try! Modern.TimeZonesDemo.dataStack.fetchAll( + From() + .where(\.$isDaylightSavingTime == true) + .orderBy(.ascending(\.$name)) + ) + } + + /** + ⭐️ Sample 3: Plain object fetch with custom `where` clause + */ + private func fetchTimeZonesInAsia() -> [Modern.TimeZonesDemo.TimeZone] { + + return try! Modern.TimeZonesDemo.dataStack.fetchAll( + From() + .where( + format: "%K BEGINSWITH[c] %@", + String(keyPath: \Modern.TimeZonesDemo.TimeZone.$name), + "Asia" + ) + .orderBy(.ascending(\.$secondsFromGMT)) + ) + } + + /** + ⭐️ Sample 4: Plain object fetch with complex `where` clauses + */ + private func fetchTimeZonesNearUTC() -> [Modern.TimeZonesDemo.TimeZone] { + + let secondsIn3Hours = 60 * 60 * 3 + return try! Modern.TimeZonesDemo.dataStack.fetchAll( + From() + .where((-secondsIn3Hours ... secondsIn3Hours) ~= \.$secondsFromGMT) + /// equivalent to: + /// ``` + /// .where(\.$secondsFromGMT >= -secondsIn3Hours + /// && \.$secondsFromGMT <= secondsIn3Hours) + /// ``` + .orderBy(.ascending(\.$secondsFromGMT)) + ) + } + + /** + ⭐️ Sample 5: Querying single raw value with simple `select` clause + */ + private func queryNumberOfTimeZones() -> Int? { + + return try! Modern.TimeZonesDemo.dataStack.queryValue( + From() + .select(Int.self, .count(\.$name)) + ) + } + + /** + ⭐️ Sample 6: Querying single raw values with `select` and `where` clauses + */ + private func queryTokyoTimeZoneAbbreviation() -> String? { + + return try! Modern.TimeZonesDemo.dataStack.queryValue( + From() + .select(String.self, .attribute(\.$abbreviation)) + .where( + format: "%K ENDSWITH[c] %@", + String(keyPath: \Modern.TimeZonesDemo.TimeZone.$name), + "Tokyo" + ) + ) + } + + /** + ⭐️ Sample 7: Querying a list of raw values with multiple attributes + */ + private func queryAllNamesAndAbbreviations() -> [[String: Any]]? { + + return try! Modern.TimeZonesDemo.dataStack.queryAttributes( + From() + .select( + NSDictionary.self, + .attribute(\.$name), + .attribute(\.$abbreviation) + ) + .orderBy(.ascending(\.$name)) + ) + } + + /** + ⭐️ Sample 7: Querying a list of raw values grouped by similar field + */ + private func queryNumberOfCountriesWithAndWithoutDST() -> [[String: Any]]? { + + return try! Modern.TimeZonesDemo.dataStack.queryAttributes( + From() + .select( + NSDictionary.self, + .count(\.$isDaylightSavingTime, as: "numberOfCountries"), + .attribute(\.$isDaylightSavingTime) + ) + .groupBy(\.$isDaylightSavingTime) + .orderBy( + .ascending(\.$isDaylightSavingTime), + .ascending(\.$name) + ) + ) + } + + + // MARK: View + + var body: some View { + List { + Section(header: Text("Fetching objects")) { + ForEach(self.fetchingItems, id: \.title) { item in + Menu.ItemView( + title: item.title, + destination: { + Modern.TimeZonesDemo.ListView( + title: item.title, + objects: item.objects() + ) + } + ) + } + } + Section(header: Text("Querying raw values")) { + ForEach(self.queryingItems, id: \.title) { item in + Menu.ItemView( + title: item.title, + destination: { + Modern.TimeZonesDemo.ListView( + title: item.title, + value: item.value() + ) + } + ) + } + } + } + .listStyle(GroupedListStyle()) + .navigationBarTitle("Time Zones") + } + + + // MARK: Private + + private var fetchingItems: [(title: String, objects: () -> [Modern.TimeZonesDemo.TimeZone])] { + + return [ + ( + "All Time Zones", + self.fetchAllTimeZones + ), + ( + "Time Zones with Daylight Savings", + self.fetchTimeZonesWithDST + ), + ( + "Time Zones in Asia", + self.fetchTimeZonesInAsia + ), + ( + "Time Zones at most 3 hours away from UTC", + self.fetchTimeZonesNearUTC + ) + ] + } + + private var queryingItems: [(title: String, value: () -> Any?)] { + + return [ + ( + "Number of Time Zones", + self.queryNumberOfTimeZones + ), + ( + "Abbreviation for Tokyo's Time Zone", + self.queryTokyoTimeZoneAbbreviation + ), + ( + "All Names and Abbreviations", + self.queryAllNamesAndAbbreviations + ), + ( + "Number of Countries with and without DST", + self.queryNumberOfCountriesWithAndWithoutDST + ) + ] + } + } +} + + +#if DEBUG + +struct _Demo_Modern_TimeZonesDemo_MainView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + Modern.TimeZonesDemo.MainView() + } +} + +#endif diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.TimeZone.swift b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.TimeZone.swift new file mode 100644 index 0000000..0f6854e --- /dev/null +++ b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.TimeZone.swift @@ -0,0 +1,32 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + +// MARK: - Modern.TimeZonesDemo + +extension Modern.TimeZonesDemo { + + // MARK: - Modern.TimeZonesDemo.TimeZone + + final class TimeZone: CoreStoreObject { + + // MARK: Internal + + @Field.Stored("secondsFromGMT") + var secondsFromGMT: Int = 0 + + @Field.Stored("abbreviation") + var abbreviation: String = "" + + @Field.Stored("isDaylightSavingTime") + var isDaylightSavingTime: Bool = false + + @Field.Stored("daylightSavingTimeOffset") + var daylightSavingTimeOffset: Double = 0 + + @Field.Stored("name") + var name: String = "" + } +} diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.swift b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.swift new file mode 100644 index 0000000..a58e93f --- /dev/null +++ b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.swift @@ -0,0 +1,64 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + +// MARK: - Modern + +extension Modern { + + // MARK: - Modern.TimeZonesDemo + + /** + Sample usages for creating Fetch and Query clauses for `CoreStoreObject`s + */ + enum TimeZonesDemo { + + // MARK: Internal + + static let dataStack: DataStack = { + + let dataStack = DataStack( + CoreStoreSchema( + modelVersion: "V1", + entities: [ + Entity("TimeZone") + ], + versionLock: [ + "TimeZone": [0x9b1d35108434c8fd, 0x4cb8a80903e66b64, 0x405acca3c1945fe3, 0x3b49dccaee0753d8] + ] + ) + ) + + /** + - Important: `addStorageAndWait(_:)` and `perform(synchronous:)` methods were used here to simplify initializing the demo, but in practice the asynchronous function variants are recommended. + */ + try! dataStack.addStorageAndWait( + SQLiteStore( + fileName: "Modern.TimeZonesDemo.sqlite", + localStorageOptions: .recreateStoreOnModelMismatch + ) + ) + _ = try! dataStack.perform( + synchronous: { (transaction) in + + try transaction.deleteAll(From()) + + for name in NSTimeZone.knownTimeZoneNames { + + let rawTimeZone = NSTimeZone(name: name)! + let cachedTimeZone = transaction.create(Into()) + + cachedTimeZone.name = rawTimeZone.name + cachedTimeZone.abbreviation = rawTimeZone.abbreviation ?? "" + cachedTimeZone.secondsFromGMT = rawTimeZone.secondsFromGMT + cachedTimeZone.isDaylightSavingTime = rawTimeZone.isDaylightSavingTime + cachedTimeZone.daylightSavingTimeOffset = rawTimeZone.daylightSavingTimeOffset + } + } + ) + return dataStack + }() + } +} diff --git a/Demo/Sources/Helpers/InstructionsView.swift b/Demo/Sources/Helpers/InstructionsView.swift new file mode 100644 index 0000000..c2b1f75 --- /dev/null +++ b/Demo/Sources/Helpers/InstructionsView.swift @@ -0,0 +1,58 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import SwiftUI + +// MARK: - InstructionsView + +struct InstructionsView: View { + + // MARK: Internal + + init(_ rows: (header: String, description: String)...) { + + self.rows = rows.map({ .init(header: $0, description: $1) }) + } + + + // MARK: View + + var body: some View { + ZStack(alignment: .center) { + Color.white + .cornerRadius(10) + .shadow(color: Color(.sRGB, white: 0.5, opacity: 0.3), radius: 2, x: 1, y: 1) + VStack(alignment: .leading, spacing: 3) { + ForEach(self.rows, id: \.header) { row in + HStack(alignment: .firstTextBaseline, spacing: 5) { + Text(row.header) + .font(.callout) + .fontWeight(.bold) + Text(row.description) + .font(.footnote) + } + } + } + .foregroundColor(Color(.sRGB, white: 0, opacity: 0.8)) + .padding(.horizontal, 10) + .padding(.vertical, 4) + } + .fixedSize() + } + + // MARK: Private + + private let rows: [InstructionsView.Row] + + + // MARK: - Row + + struct Row: Hashable { + + // MARK: Internal + let header: String + let description: String + } +} + diff --git a/Demo/Sources/Helpers/LazyView.swift b/Demo/Sources/Helpers/LazyView.swift new file mode 100644 index 0000000..fd74aa8 --- /dev/null +++ b/Demo/Sources/Helpers/LazyView.swift @@ -0,0 +1,29 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import SwiftUI + +// MARK: - LazyView + +struct LazyView: View { + + // MARK: Internal + + init(_ load: @escaping () -> Content) { + + self.load = load + } + + + // MARK: View + + var body: Content { + + self.load() + } + + // MARK: Private + + private let load: () -> Content +} diff --git a/Demo/Sources/Helpers/Menu/Menu.ItemView.swift b/Demo/Sources/Helpers/Menu/Menu.ItemView.swift new file mode 100644 index 0000000..4f208fc --- /dev/null +++ b/Demo/Sources/Helpers/Menu/Menu.ItemView.swift @@ -0,0 +1,71 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import SwiftUI + +// MARK: - Menu + +extension Menu { + + // MARK: - Menu.ItemView + + struct ItemView: View { + + // MARK: Internal + + init( + title: String, + subtitle: String? = nil, + destination: @escaping () -> Destination + ) { + self.title = title + self.subtitle = subtitle + self.destination = destination + } + + + // MARK: View + + var body: some View { + NavigationLink(destination: LazyView(self.destination)) { + VStack(alignment: .leading) { + Text(self.title) + .font(.headline) + .foregroundColor(.primary) + self.subtitle.map { + Text($0) + .font(.subheadline) + .foregroundColor(.secondary) + } + } + } + } + + + // MARK: FilePrivate + + fileprivate let title: String + fileprivate let subtitle: String? + fileprivate let destination: () -> Destination + } +} + +#if DEBUG + +struct _Demo_Menu_ItemView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + Menu.ItemView( + title: "Item Title", + subtitle: "A subtitle caption for this item", + destination: { + Color.blue + } + ) + } +} + +#endif diff --git a/Demo/Sources/Helpers/Menu/Menu.MainView.swift b/Demo/Sources/Helpers/Menu/Menu.MainView.swift new file mode 100644 index 0000000..d7fbf79 --- /dev/null +++ b/Demo/Sources/Helpers/Menu/Menu.MainView.swift @@ -0,0 +1,161 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import Foundation +import SwiftUI + + +// MARK: - Menu + +extension Menu { + + // MARK: - Menu.MainView + + struct MainView: View { + + // MARK: View + + var body: some View { + NavigationView { + List { + Section(header: Text("Modern (CoreStoreObject subclasses)")) { + Menu.ItemView( + title: "Placemarks", + subtitle: "Making changes using Transactions", + destination: { + Modern.PlacemarksDemo.MainView() + } + ) + Menu.ItemView( + title: "Time Zones", + subtitle: "Fetching objects and Querying raw values", + destination: { + Modern.TimeZonesDemo.MainView() + } + ) + Menu.ItemView( + title: "Colors (UIKit)", + subtitle: "Observing list changes and single-object changes using DiffableDataSources", + destination: { + Modern.ColorsDemo.MainView( + listView: { listPublisher, onPaletteTapped in + Modern.ColorsDemo.UIKit.ListView( + listPublisher: listPublisher, + onPaletteTapped: onPaletteTapped + ) + .edgesIgnoringSafeArea(.all) + }, + detailView: { objectPublisher in + Modern.ColorsDemo.UIKit.DetailView(objectPublisher) + } + ) + } + ) + Menu.ItemView( + title: "Colors (SwiftUI)", + subtitle: "Observing list changes and single-object changes using SwiftUI bindings", + destination: { + Modern.ColorsDemo.MainView( + listView: { listPublisher, onPaletteTapped in + Modern.ColorsDemo.SwiftUI.ListView( + listPublisher: listPublisher, + onPaletteTapped: onPaletteTapped + ) + }, + detailView: { objectPublisher in + Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher) + } + ) + } + ) + Menu.ItemView( + title: "Pokedex API", + subtitle: "Importing JSON data from external source", + destination: { EmptyView() } + ) + } + Section(header: Text("Classic (NSManagedObject subclasses)")) { + Menu.ItemView( + title: "Placemarks (Swift)", + subtitle: "Making changes using transactions in Swift", + destination: { EmptyView() } + ) + Menu.ItemView( + title: "Placemarks (Objective-C)", + subtitle: "Making changes using transactions in Objective-C", + destination: { EmptyView() } + ) + Menu.ItemView( + title: "Time Zones", + subtitle: "Fetching objects and Querying raw values", + destination: { EmptyView() } + ) + Menu.ItemView( + title: "Colors (Swift)", + subtitle: "Observing list changes and single-object changes in Swift", + destination: { EmptyView() } + ) + Menu.ItemView( + title: "Colors (Objective-C)", + subtitle: "Observing list changes and single-object changes in Objective-C", + destination: { EmptyView() } + ) + Menu.ItemView( + title: "Pokedex API", + subtitle: "Importing JSON data from external source", + destination: { EmptyView() } + ) + } + Section(header: Text("Advanced")) { + Menu.ItemView( + title: "Accounts", + subtitle: "Switching between multiple persistent stores", + destination: { EmptyView() } + ) + Menu.ItemView( + title: "Evolution", + subtitle: "Migrating and reverse-migrating stores", + destination: { EmptyView() } + ) + Menu.ItemView( + title: "Logger", + subtitle: "Implementing a custom logger", + destination: { EmptyView() } + ) + } + } + .listStyle(GroupedListStyle()) + .navigationBarTitle("CoreStore Demos") + Menu.DetailView() + } + .navigationViewStyle(DoubleColumnNavigationViewStyle()) + } + } + + fileprivate struct DetailView: View { + + var selectedDate: Date? + + var body: some View { + Group { + Text("Detail view content goes here") + } + .navigationBarTitle(Text("Detail")) + } + } +} + +#if DEBUG + +struct _Demo_Menu_MainView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + Menu.MainView() + } +} + +#endif diff --git a/Demo/Sources/Helpers/Menu/Menu.swift b/Demo/Sources/Helpers/Menu/Menu.swift new file mode 100644 index 0000000..34de26d --- /dev/null +++ b/Demo/Sources/Helpers/Menu/Menu.swift @@ -0,0 +1,10 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import Foundation + + +// MARK: - Menu + +enum Menu {} diff --git a/Demo/Sources/Helpers/UIImage+Extensions.swift b/Demo/Sources/Helpers/UIImage+Extensions.swift new file mode 100644 index 0000000..8dde2fe --- /dev/null +++ b/Demo/Sources/Helpers/UIImage+Extensions.swift @@ -0,0 +1,43 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import UIKit + + +// MARK: - UIImage + +extension UIImage { + + // MARK: Internal + + convenience init( + color: UIColor, + size: CGSize = CGSize(width: 1, height: 1), + cornerRadius: CGFloat = 0 + ) { + let rect = CGRect(origin: .zero, size: size) + let scale = UIScreen.main.scale + UIGraphicsBeginImageContextWithOptions(rect.size, false, scale) + defer { + UIGraphicsEndImageContext() + } + let context = UIGraphicsGetCurrentContext()! + + if cornerRadius > 0 { + UIBezierPath( + roundedRect: rect, + cornerRadius: cornerRadius + ) + .addClip() + } + color.setFill() + context.fill(rect) + + self.init( + cgImage: UIGraphicsGetImageFromCurrentImageContext()!.cgImage!, + scale: scale, + orientation: .up + ) + } +} diff --git a/Demo/Sources/SceneDelegate.swift b/Demo/Sources/SceneDelegate.swift new file mode 100644 index 0000000..afb6e32 --- /dev/null +++ b/Demo/Sources/SceneDelegate.swift @@ -0,0 +1,36 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import SwiftUI +import UIKit + +// MARK: - SceneDelegate + +@objc final class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + // MARK: UIWindowSceneDelegate + + @objc dynamic var window: UIWindow? + + + // MARK: UISceneDelegate + + @objc dynamic func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + + guard case let scene as UIWindowScene = scene else { + + return + } + let window = UIWindow(windowScene: scene) + window.rootViewController = UIHostingController( + rootView: Menu.MainView() + ) + self.window = window + window.makeKeyAndVisible() + } +}