diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 9bca030..6041e77 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -25,7 +25,7 @@ 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 */; }; + 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 */; }; @@ -66,6 +66,16 @@ B5A391B424E96C0A00E7E8BD /* ⭐️Modern.PokedexDemo.Form.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391B324E96C0A00E7E8BD /* ⭐️Modern.PokedexDemo.Form.swift */; }; B5A391B924E96F8500E7E8BD /* ⭐️Modern.PokedexDemo.Species.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391B824E96F8500E7E8BD /* ⭐️Modern.PokedexDemo.Species.swift */; }; B5A391BB24E970A400E7E8BD /* ⭐️Modern.PokedexDemo.PokemonType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A391BA24E970A400E7E8BD /* ⭐️Modern.PokedexDemo.PokemonType.swift */; }; + B5A543D724FB7478000DC5E3 /* Classic.ColorsDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A543D624FB7478000DC5E3 /* Classic.ColorsDemo.swift */; }; + B5A543DB24FB7513000DC5E3 /* Classic.ColorsDemo.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B5A543D924FB7513000DC5E3 /* Classic.ColorsDemo.xcdatamodeld */; }; + B5A543DD24FB78F9000DC5E3 /* Classic.ColorsDemo.Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A543DC24FB78F9000DC5E3 /* Classic.ColorsDemo.Palette.swift */; }; + B5A543E724FB82BB000DC5E3 /* Classic.ColorsDemo.Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A543E624FB82BB000DC5E3 /* Classic.ColorsDemo.Filter.swift */; }; + B5A543E924FB84A1000DC5E3 /* Classic.ColorsDemo.DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A543E824FB84A1000DC5E3 /* Classic.ColorsDemo.DetailView.swift */; }; + B5A543EB24FB84AF000DC5E3 /* ⭐️Classic.ColorsDemo.DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A543EA24FB84AF000DC5E3 /* ⭐️Classic.ColorsDemo.DetailViewController.swift */; }; + B5A543ED24FB84BE000DC5E3 /* Classic.ColorsDemo.MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A543EC24FB84BE000DC5E3 /* Classic.ColorsDemo.MainView.swift */; }; + B5A543EF24FB84D1000DC5E3 /* Classic.ColorsDemo.LIstView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A543EE24FB84D1000DC5E3 /* Classic.ColorsDemo.LIstView.swift */; }; + B5A543F124FB84DD000DC5E3 /* ⭐️Classic.ColorsDemo.ListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A543F024FB84DD000DC5E3 /* ⭐️Classic.ColorsDemo.ListViewController.swift */; }; + B5A543F324FB84EC000DC5E3 /* Classic.ColorsDemo.ItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A543F224FB84EC000DC5E3 /* Classic.ColorsDemo.ItemCell.swift */; }; B5E32C9024FA41F9003F46AD /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E32C8F24FA41F9003F46AD /* ImageDownloader.swift */; }; /* End PBXBuildFile section */ @@ -108,7 +118,7 @@ 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 = ""; }; + 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 = ""; }; @@ -145,6 +155,16 @@ B5A391B324E96C0A00E7E8BD /* ⭐️Modern.PokedexDemo.Form.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "⭐️Modern.PokedexDemo.Form.swift"; sourceTree = ""; }; B5A391B824E96F8500E7E8BD /* ⭐️Modern.PokedexDemo.Species.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "⭐️Modern.PokedexDemo.Species.swift"; sourceTree = ""; }; B5A391BA24E970A400E7E8BD /* ⭐️Modern.PokedexDemo.PokemonType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "⭐️Modern.PokedexDemo.PokemonType.swift"; sourceTree = ""; }; + B5A543D624FB7478000DC5E3 /* Classic.ColorsDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Classic.ColorsDemo.swift; sourceTree = ""; }; + B5A543DA24FB7513000DC5E3 /* ColorsDemo.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = ColorsDemo.xcdatamodel; sourceTree = ""; }; + B5A543DC24FB78F9000DC5E3 /* Classic.ColorsDemo.Palette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Classic.ColorsDemo.Palette.swift; sourceTree = ""; }; + B5A543E624FB82BB000DC5E3 /* Classic.ColorsDemo.Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Classic.ColorsDemo.Filter.swift; sourceTree = ""; }; + B5A543E824FB84A1000DC5E3 /* Classic.ColorsDemo.DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Classic.ColorsDemo.DetailView.swift; sourceTree = ""; }; + B5A543EA24FB84AF000DC5E3 /* ⭐️Classic.ColorsDemo.DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "⭐️Classic.ColorsDemo.DetailViewController.swift"; sourceTree = ""; }; + B5A543EC24FB84BE000DC5E3 /* Classic.ColorsDemo.MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Classic.ColorsDemo.MainView.swift; sourceTree = ""; }; + B5A543EE24FB84D1000DC5E3 /* Classic.ColorsDemo.LIstView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Classic.ColorsDemo.LIstView.swift; sourceTree = ""; }; + B5A543F024FB84DD000DC5E3 /* ⭐️Classic.ColorsDemo.ListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "⭐️Classic.ColorsDemo.ListViewController.swift"; sourceTree = ""; }; + B5A543F224FB84EC000DC5E3 /* Classic.ColorsDemo.ItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Classic.ColorsDemo.ItemCell.swift; sourceTree = ""; }; B5E32C8F24FA41F9003F46AD /* ImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -170,7 +190,7 @@ isa = PBXGroup; children = ( B5A3913D24E62C6C00E7E8BD /* Metadata */, - B5A3911B24E5429200E7E8BD /* Sources */, + B5A3911B24E5429200E7E8BD /* ⭐️Sources */, B5A3913E24E62CB200E7E8BD /* Resources */, B5A3911A24E5429200E7E8BD /* Products */, B5A3916624E698F900E7E8BD /* Frameworks */, @@ -185,15 +205,15 @@ name = Products; sourceTree = ""; }; - B5A3911B24E5429200E7E8BD /* Sources */ = { + B5A3911B24E5429200E7E8BD /* ⭐️Sources */ = { isa = PBXGroup; children = ( B5A3911C24E5429200E7E8BD /* AppDelegate.swift */, B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */, - B5A3915524E6858A00E7E8BD /* Demos */, + B5A3915524E6858A00E7E8BD /* ⭐️Demos */, B5A3917A24E6A75F00E7E8BD /* Helpers */, ); - path = Sources; + path = "⭐️Sources"; sourceTree = ""; }; B5A3912424E5429300E7E8BD /* Preview Content */ = { @@ -234,45 +254,46 @@ path = Menu; sourceTree = ""; }; - B5A3915524E6858A00E7E8BD /* Demos */ = { + B5A3915524E6858A00E7E8BD /* ⭐️Demos */ = { isa = PBXGroup; children = ( - B5A3915624E685B700E7E8BD /* Modern */, - B5A3915724E685D300E7E8BD /* Classic */, + B5A3915624E685B700E7E8BD /* ⭐️Modern */, + B5A3915724E685D300E7E8BD /* ⭐️Classic */, ); - path = Demos; + path = "⭐️Demos"; sourceTree = ""; }; - B5A3915624E685B700E7E8BD /* Modern */ = { + B5A3915624E685B700E7E8BD /* ⭐️Modern */ = { isa = PBXGroup; children = ( B5A3915A24E685FE00E7E8BD /* Modern.swift */, - B5A3915C24E6921E00E7E8BD /* PlacemarksDemo */, - B5A3918124E7A1EF00E7E8BD /* TimeZonesDemo */, - B5A3918D24E7DE7A00E7E8BD /* ColorsDemo */, - B5A391AF24E96AD600E7E8BD /* PokedexDemo */, + B5A3915C24E6921E00E7E8BD /* ⭐️PlacemarksDemo */, + B5A3918124E7A1EF00E7E8BD /* ⭐️TimeZonesDemo */, + B5A3918D24E7DE7A00E7E8BD /* ⭐️ColorsDemo */, + B5A391AF24E96AD600E7E8BD /* ⭐️PokedexDemo */, ); - path = Modern; + path = "⭐️Modern"; sourceTree = ""; }; - B5A3915724E685D300E7E8BD /* Classic */ = { + B5A3915724E685D300E7E8BD /* ⭐️Classic */ = { isa = PBXGroup; children = ( B5A3915824E685EC00E7E8BD /* Classic.swift */, + B5A543D524FB73B2000DC5E3 /* ⭐️ColorsDemo */, ); - path = Classic; + path = "⭐️Classic"; sourceTree = ""; }; - B5A3915C24E6921E00E7E8BD /* PlacemarksDemo */ = { + B5A3915C24E6921E00E7E8BD /* ⭐️PlacemarksDemo */ = { isa = PBXGroup; children = ( - B5A3915D24E6922E00E7E8BD /* Modern.PlacemarksDemo.swift */, + B5A3915D24E6922E00E7E8BD /* ⭐️Modern.PlacemarksDemo.swift */, B5A3916124E697BA00E7E8BD /* ⭐️Modern.PlacemarksDemo.MainView.swift */, B5A3915F24E6925900E7E8BD /* Modern.PlacemarksDemo.MapView.swift */, B5A3917D24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift */, B5A3916324E698B300E7E8BD /* Models */, ); - path = PlacemarksDemo; + path = "⭐️PlacemarksDemo"; sourceTree = ""; }; B5A3916324E698B300E7E8BD /* Models */ = { @@ -308,7 +329,7 @@ path = Helpers; sourceTree = ""; }; - B5A3918124E7A1EF00E7E8BD /* TimeZonesDemo */ = { + B5A3918124E7A1EF00E7E8BD /* ⭐️TimeZonesDemo */ = { isa = PBXGroup; children = ( B5A3918224E7A21800E7E8BD /* Modern.TimeZonesDemo.swift */, @@ -317,7 +338,7 @@ B5A3918B24E7B44B00E7E8BD /* Modern.TimeZonesDemo.ItemView.swift */, B5A3918424E7A53300E7E8BD /* Models */, ); - path = TimeZonesDemo; + path = "⭐️TimeZonesDemo"; sourceTree = ""; }; B5A3918424E7A53300E7E8BD /* Models */ = { @@ -328,17 +349,17 @@ name = Models; sourceTree = ""; }; - B5A3918D24E7DE7A00E7E8BD /* ColorsDemo */ = { + B5A3918D24E7DE7A00E7E8BD /* ⭐️ColorsDemo */ = { isa = PBXGroup; children = ( B5A3918E24E7E06500E7E8BD /* Modern.ColorsDemo.swift */, B5A3919724E7E67000E7E8BD /* Modern.ColorsDemo.Filter.swift */, B5A391A524E8F4EA00E7E8BD /* ⭐️Modern.ColorsDemo.MainView.swift */, - B5A3919C24E8EE9000E7E8BD /* UIKit */, - B5A3919B24E8EE8100E7E8BD /* SwiftUI */, + B5A3919C24E8EE9000E7E8BD /* ⭐️UIKit */, + B5A3919B24E8EE8100E7E8BD /* ⭐️SwiftUI */, B5A3919024E7E0B000E7E8BD /* Models */, ); - path = ColorsDemo; + path = "⭐️ColorsDemo"; sourceTree = ""; }; B5A3919024E7E0B000E7E8BD /* Models */ = { @@ -349,7 +370,7 @@ name = Models; sourceTree = ""; }; - B5A3919B24E8EE8100E7E8BD /* SwiftUI */ = { + B5A3919B24E8EE8100E7E8BD /* ⭐️SwiftUI */ = { isa = PBXGroup; children = ( B5A3919D24E8EEB600E7E8BD /* Modern.ColorsDemo.SwiftUI.swift */, @@ -357,10 +378,10 @@ B5A3919524E7E4AC00E7E8BD /* ⭐️Modern.ColorsDemo.SwiftUI.ItemView.swift */, B5A3919924E8207A00E7E8BD /* ⭐️Modern.ColorsDemo.SwiftUI.DetailView.swift */, ); - name = SwiftUI; + name = "⭐️SwiftUI"; sourceTree = ""; }; - B5A3919C24E8EE9000E7E8BD /* UIKit */ = { + B5A3919C24E8EE9000E7E8BD /* ⭐️UIKit */ = { isa = PBXGroup; children = ( B5A3919F24E8F00A00E7E8BD /* Modern.ColorsDemo.UIKit.swift */, @@ -370,10 +391,10 @@ B5A391AB24E9143B00E7E8BD /* Modern.ColorsDemo.UIKit.DetailView.swift */, B5A391AD24E9150F00E7E8BD /* ⭐️Modern.ColorsDemo.UIKit.DetailViewController.swift */, ); - name = UIKit; + name = "⭐️UIKit"; sourceTree = ""; }; - B5A391AF24E96AD600E7E8BD /* PokedexDemo */ = { + B5A391AF24E96AD600E7E8BD /* ⭐️PokedexDemo */ = { isa = PBXGroup; children = ( B5A391B024E96AF600E7E8BD /* Modern.PokedexDemo.swift */, @@ -382,29 +403,54 @@ B566C8E724F9D406001134A1 /* Modern.PokedexDemo.ListView.swift */, B566C8E924F9D412001134A1 /* Modern.PokedexDemo.ListViewController.swift */, B566C8EB24F9D694001134A1 /* Modern.PokedexDemo.ItemCell.swift */, - B5A391B224E96B7400E7E8BD /* Models */, + B5A391B224E96B7400E7E8BD /* ⭐️Models */, ); - path = PokedexDemo; + path = "⭐️PokedexDemo"; sourceTree = ""; }; - B5A391B224E96B7400E7E8BD /* Models */ = { + B5A391B224E96B7400E7E8BD /* ⭐️Models */ = { isa = PBXGroup; children = ( B531EFE824EB5A52005F247D /* ⭐️Modern.PokedexDemo.PokedexEntry.swift */, B566C8ED24FA1EA3001134A1 /* Modern.PokedexDemo.Details.swift */, B5A391B824E96F8500E7E8BD /* ⭐️Modern.PokedexDemo.Species.swift */, B5A391B324E96C0A00E7E8BD /* ⭐️Modern.PokedexDemo.Form.swift */, - B5A391B724E96E8600E7E8BD /* Attributes */, + B5A391B724E96E8600E7E8BD /* ⭐️Attributes */, ); - name = Models; + name = "⭐️Models"; sourceTree = ""; }; - B5A391B724E96E8600E7E8BD /* Attributes */ = { + B5A391B724E96E8600E7E8BD /* ⭐️Attributes */ = { isa = PBXGroup; children = ( B5A391BA24E970A400E7E8BD /* ⭐️Modern.PokedexDemo.PokemonType.swift */, ); - name = Attributes; + name = "⭐️Attributes"; + sourceTree = ""; + }; + B5A543D524FB73B2000DC5E3 /* ⭐️ColorsDemo */ = { + isa = PBXGroup; + children = ( + B5A543D624FB7478000DC5E3 /* Classic.ColorsDemo.swift */, + B5A543E624FB82BB000DC5E3 /* Classic.ColorsDemo.Filter.swift */, + B5A543EC24FB84BE000DC5E3 /* Classic.ColorsDemo.MainView.swift */, + B5A543EE24FB84D1000DC5E3 /* Classic.ColorsDemo.LIstView.swift */, + B5A543F024FB84DD000DC5E3 /* ⭐️Classic.ColorsDemo.ListViewController.swift */, + B5A543F224FB84EC000DC5E3 /* Classic.ColorsDemo.ItemCell.swift */, + B5A543E824FB84A1000DC5E3 /* Classic.ColorsDemo.DetailView.swift */, + B5A543EA24FB84AF000DC5E3 /* ⭐️Classic.ColorsDemo.DetailViewController.swift */, + B5A543D824FB7483000DC5E3 /* Models */, + ); + path = "⭐️ColorsDemo"; + sourceTree = ""; + }; + B5A543D824FB7483000DC5E3 /* Models */ = { + isa = PBXGroup; + children = ( + B5A543D924FB7513000DC5E3 /* Classic.ColorsDemo.xcdatamodeld */, + B5A543DC24FB78F9000DC5E3 /* Classic.ColorsDemo.Palette.swift */, + ); + name = Models; sourceTree = ""; }; /* End PBXGroup section */ @@ -484,19 +530,25 @@ B5A3918024E787D900E7E8BD /* InstructionsView.swift in Sources */, B5A3917C24E6A76C00E7E8BD /* LazyView.swift in Sources */, B5A3913424E6170500E7E8BD /* Menu.swift in Sources */, + B5A543DB24FB7513000DC5E3 /* Classic.ColorsDemo.xcdatamodeld in Sources */, B5A3915B24E685FE00E7E8BD /* Modern.swift in Sources */, + B5A543D724FB7478000DC5E3 /* Classic.ColorsDemo.swift in Sources */, B5A3911F24E5429200E7E8BD /* SceneDelegate.swift in Sources */, B5A3915324E6537F00E7E8BD /* Menu.ItemView.swift in Sources */, B5A3912124E5429200E7E8BD /* Menu.MainView.swift in Sources */, B531EFE724EA762D005F247D /* Menu.PlaceholderView.swift in Sources */, B5A3918F24E7E06500E7E8BD /* Modern.ColorsDemo.swift in Sources */, - B5A3915E24E6922E00E7E8BD /* Modern.PlacemarksDemo.swift in Sources */, + B5A543EF24FB84D1000DC5E3 /* Classic.ColorsDemo.LIstView.swift in Sources */, + B5A3915E24E6922E00E7E8BD /* ⭐️Modern.PlacemarksDemo.swift in Sources */, + B5A543F324FB84EC000DC5E3 /* Classic.ColorsDemo.ItemCell.swift in Sources */, B5A391B124E96AF600E7E8BD /* Modern.PokedexDemo.swift in Sources */, B5A3918324E7A21800E7E8BD /* Modern.TimeZonesDemo.swift in Sources */, B5A3919824E7E67000E7E8BD /* Modern.ColorsDemo.Filter.swift in Sources */, B5A3919224E7E0C600E7E8BD /* Modern.ColorsDemo.Palette.swift in Sources */, B5A3919E24E8EEB600E7E8BD /* Modern.ColorsDemo.SwiftUI.swift in Sources */, B5A391A024E8F00A00E7E8BD /* Modern.ColorsDemo.UIKit.swift in Sources */, + B5A543E724FB82BB000DC5E3 /* Classic.ColorsDemo.Filter.swift in Sources */, + B5A543ED24FB84BE000DC5E3 /* Classic.ColorsDemo.MainView.swift in Sources */, B5A3917E24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift in Sources */, B5A3916024E6925900E7E8BD /* Modern.PlacemarksDemo.MapView.swift in Sources */, B5A3916524E698C700E7E8BD /* Modern.PlacemarksDemo.Place.swift in Sources */, @@ -510,6 +562,7 @@ B5A3918624E7A54A00E7E8BD /* Modern.TimeZonesDemo.TimeZone.swift in Sources */, B5A391A624E8F4EA00E7E8BD /* ⭐️Modern.ColorsDemo.MainView.swift in Sources */, B5A3916224E697BA00E7E8BD /* ⭐️Modern.PlacemarksDemo.MainView.swift in Sources */, + B5A543EB24FB84AF000DC5E3 /* ⭐️Classic.ColorsDemo.DetailViewController.swift in Sources */, B5A391B424E96C0A00E7E8BD /* ⭐️Modern.PokedexDemo.Form.swift in Sources */, B531EFE924EB5A53005F247D /* ⭐️Modern.PokedexDemo.PokedexEntry.swift in Sources */, B5A391BB24E970A400E7E8BD /* ⭐️Modern.PokedexDemo.PokemonType.swift in Sources */, @@ -519,11 +572,14 @@ B5A391AC24E9143B00E7E8BD /* Modern.ColorsDemo.UIKit.DetailView.swift in Sources */, B5A391AA24E9104300E7E8BD /* Modern.ColorsDemo.UIKit.ItemCell.swift in Sources */, B5A391A424E8F04300E7E8BD /* Modern.ColorsDemo.UIKit.ListView.swift in Sources */, + B5A543E924FB84A1000DC5E3 /* Classic.ColorsDemo.DetailView.swift in Sources */, B5A3919A24E8207A00E7E8BD /* ⭐️Modern.ColorsDemo.SwiftUI.DetailView.swift in Sources */, + B5A543F124FB84DD000DC5E3 /* ⭐️Classic.ColorsDemo.ListViewController.swift in Sources */, B5A3919624E7E4AC00E7E8BD /* ⭐️Modern.ColorsDemo.SwiftUI.ItemView.swift in Sources */, B5A3919424E7E36700E7E8BD /* ⭐️Modern.ColorsDemo.SwiftUI.ListView.swift in Sources */, B5A391AE24E9150F00E7E8BD /* ⭐️Modern.ColorsDemo.UIKit.DetailViewController.swift in Sources */, B5A391A224E8F01F00E7E8BD /* ⭐️Modern.ColorsDemo.UIKit.ListViewController.swift in Sources */, + B5A543DD24FB78F9000DC5E3 /* Classic.ColorsDemo.Palette.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -717,6 +773,19 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + B5A543D924FB7513000DC5E3 /* Classic.ColorsDemo.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + B5A543DA24FB7513000DC5E3 /* ColorsDemo.xcdatamodel */, + ); + currentVersion = B5A543DA24FB7513000DC5E3 /* ColorsDemo.xcdatamodel */; + path = Classic.ColorsDemo.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = B5A3910F24E5424E00E7E8BD /* Project object */; } diff --git a/Demo/Sources/AppDelegate.swift b/Demo/⭐️Sources/AppDelegate.swift similarity index 100% rename from Demo/Sources/AppDelegate.swift rename to Demo/⭐️Sources/AppDelegate.swift diff --git a/Demo/Sources/Helpers/ImageDownloader.swift b/Demo/⭐️Sources/Helpers/ImageDownloader.swift similarity index 94% rename from Demo/Sources/Helpers/ImageDownloader.swift rename to Demo/⭐️Sources/Helpers/ImageDownloader.swift index 7e80e3e..bdd3ae6 100644 --- a/Demo/Sources/Helpers/ImageDownloader.swift +++ b/Demo/⭐️Sources/Helpers/ImageDownloader.swift @@ -1,9 +1,6 @@ // -// ImageDownloader.swift -// Demo -// -// Created by John Rommel Estropia on 2020/08/29. -// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. import Foundation import UIKit diff --git a/Demo/Sources/Helpers/InstructionsView.swift b/Demo/⭐️Sources/Helpers/InstructionsView.swift similarity index 100% rename from Demo/Sources/Helpers/InstructionsView.swift rename to Demo/⭐️Sources/Helpers/InstructionsView.swift diff --git a/Demo/Sources/Helpers/LazyView.swift b/Demo/⭐️Sources/Helpers/LazyView.swift similarity index 100% rename from Demo/Sources/Helpers/LazyView.swift rename to Demo/⭐️Sources/Helpers/LazyView.swift diff --git a/Demo/Sources/Helpers/Menu/Menu.ItemView.swift b/Demo/⭐️Sources/Helpers/Menu/Menu.ItemView.swift similarity index 100% rename from Demo/Sources/Helpers/Menu/Menu.ItemView.swift rename to Demo/⭐️Sources/Helpers/Menu/Menu.ItemView.swift diff --git a/Demo/Sources/Helpers/Menu/Menu.MainView.swift b/Demo/⭐️Sources/Helpers/Menu/Menu.MainView.swift similarity index 84% rename from Demo/Sources/Helpers/Menu/Menu.MainView.swift rename to Demo/⭐️Sources/Helpers/Menu/Menu.MainView.swift index a16436a..3d97bb9 100644 --- a/Demo/Sources/Helpers/Menu/Menu.MainView.swift +++ b/Demo/⭐️Sources/Helpers/Menu/Menu.MainView.swift @@ -79,34 +79,23 @@ extension Menu { } Section(header: Text("Classic (NSManagedObject subclasses)")) { Menu.ItemView( - title: "Placemarks (Swift)", + title: "Placemarks", 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() } - ) + .disabled(true) Menu.ItemView( title: "Time Zones", subtitle: "Fetching objects and Querying raw values", destination: { EmptyView() } ) + .disabled(true) 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() } + title: "Colors", + subtitle: "Observing list changes and single-object changes using ListMonitor", + destination: { + Classic.ColorsDemo.MainView() + } ) } Section(header: Text("Advanced")) { diff --git a/Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift b/Demo/⭐️Sources/Helpers/Menu/Menu.PlaceholderView.swift similarity index 100% rename from Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift rename to Demo/⭐️Sources/Helpers/Menu/Menu.PlaceholderView.swift diff --git a/Demo/Sources/Helpers/Menu/Menu.swift b/Demo/⭐️Sources/Helpers/Menu/Menu.swift similarity index 100% rename from Demo/Sources/Helpers/Menu/Menu.swift rename to Demo/⭐️Sources/Helpers/Menu/Menu.swift diff --git a/Demo/Sources/SceneDelegate.swift b/Demo/⭐️Sources/SceneDelegate.swift similarity index 100% rename from Demo/Sources/SceneDelegate.swift rename to Demo/⭐️Sources/SceneDelegate.swift diff --git a/Demo/Sources/Demos/Classic/Classic.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/Classic.swift similarity index 100% rename from Demo/Sources/Demos/Classic/Classic.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Classic/Classic.swift diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.DetailView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.DetailView.swift new file mode 100644 index 0000000..729e4ad --- /dev/null +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.DetailView.swift @@ -0,0 +1,79 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import Combine +import CoreStore +import SwiftUI + +// MARK: - Classic.ColorsDemo + +extension Classic.ColorsDemo { + + // MARK: - Classic.ColorsDemo.DetailView + + struct DetailView: UIViewControllerRepresentable { + + // MARK: Internal + + init(_ palette: ObjectMonitor) { + + self.palette = palette + } + + // MARK: UIViewControllerRepresentable + + typealias UIViewControllerType = Classic.ColorsDemo.DetailViewController + + func makeUIViewController(context: Self.Context) -> UIViewControllerType { + + return UIViewControllerType(self.palette) + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) { + + uiViewController.palette = self.palette + } + + static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {} + + func makeCoordinator() -> ObjectMonitor { + + return self.palette + } + + + // MARK: Private + + private let palette: ObjectMonitor + } +} + +#if DEBUG + +struct _Demo_Classic_ColorsDemo_DetailView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + try! Classic.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + guard (try transaction.fetchCount(From())) <= 0 else { + return + } + let palette = transaction.create(Into()) + palette.setRandomHue() + } + ) + + return Classic.ColorsDemo.DetailView( + Classic.ColorsDemo.dataStack.monitorObject( + Classic.ColorsDemo.palettesMonitor[0, 0] + ) + ) + } +} + +#endif diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.Filter.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.Filter.swift new file mode 100644 index 0000000..53ea8b0 --- /dev/null +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.Filter.swift @@ -0,0 +1,36 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + + +// MARK: - Classic.ColorsDemo + +extension Classic.ColorsDemo { + + // MARK: - Classic.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 (\.brightness >= 0.9) + case .dark: return (\.brightness <= 0.4) + } + } + } +} diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.ItemCell.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.ItemCell.swift new file mode 100644 index 0000000..e1c6655 --- /dev/null +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.ItemCell.swift @@ -0,0 +1,28 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import UIKit + + +// MARK: - Classic.ColorsDemo + +extension Classic.ColorsDemo { + + // MARK: - Classic.ColorsDemo.ItemCell + + final class ItemCell: UITableViewCell { + + // MARK: Internal + + static let reuseIdentifier: String = NSStringFromClass(Classic.ColorsDemo.ItemCell.self) + + func setPalette(_ palette: Classic.ColorsDemo.Palette) { + + self.contentView.backgroundColor = palette.color + self.textLabel?.text = palette.colorText + self.textLabel?.textColor = palette.brightness > 0.6 ? .black : .white + } + } +} diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.LIstView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.LIstView.swift new file mode 100644 index 0000000..b958685 --- /dev/null +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.LIstView.swift @@ -0,0 +1,89 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import SwiftUI + +// MARK: - Classic.ColorsDemo + +extension Classic.ColorsDemo { + + // MARK: - Classic.ColorsDemo.ListView + + struct ListView: UIViewControllerRepresentable { + + // MARK: Internal + + init( + listMonitor: ListMonitor, + onPaletteTapped: @escaping (Classic.ColorsDemo.Palette) -> Void + ) { + + self.listMonitor = listMonitor + self.onPaletteTapped = onPaletteTapped + } + + + // MARK: UIViewControllerRepresentable + + typealias UIViewControllerType = Classic.ColorsDemo.ListViewController + + func makeUIViewController(context: Self.Context) -> UIViewControllerType { + + return UIViewControllerType( + listMonitor: self.listMonitor, + 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 listMonitor: ListMonitor + private let onPaletteTapped: (Classic.ColorsDemo.Palette) -> Void + } +} + +#if DEBUG + +struct _Demo_Classic_ColorsDemo_ListView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + let minimumSamples = 10 + try! Classic.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + let missing = minimumSamples + - (try transaction.fetchCount(From())) + guard missing > 0 else { + return + } + for _ in 0..()) + palette.setRandomHue() + } + } + ) + return Classic.ColorsDemo.ListView( + listMonitor: Classic.ColorsDemo.palettesMonitor, + onPaletteTapped: { _ in } + ) + } +} + +#endif diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.MainView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.MainView.swift new file mode 100644 index 0000000..a40a803 --- /dev/null +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.MainView.swift @@ -0,0 +1,247 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import SwiftUI + +// MARK: - Classic.ColorsDemo + +extension Classic.ColorsDemo { + + // MARK: - Classic.ColorsDemo.MainView + + struct MainView: View { + + // MARK: Internal + + init() { + + let listMonitor = Classic.ColorsDemo.palettesMonitor + self.listMonitor = listMonitor + self.listHelper = .init(listMonitor: listMonitor) + self._filter = Binding( + get: { Classic.ColorsDemo.filter }, + set: { Classic.ColorsDemo.filter = $0 } + ) + } + + + // MARK: View + + var body: some View { + let detailView: AnyView + if let selectedObject = self.listHelper.selectedObject() { + + detailView = AnyView( + Classic.ColorsDemo.DetailView(selectedObject) + ) + } + else { + + detailView = AnyView(EmptyView()) + } + let listMonitor = self.listMonitor + return VStack(spacing: 0) { + Classic.ColorsDemo.ListView + .init( + listMonitor: listMonitor, + onPaletteTapped: { + + self.listHelper.setSelectedPalette($0) + } + ) + .navigationBarTitle( + Text("Colors (\(self.listHelper.count) objects)") + ) + .frame(minHeight: 0, maxHeight: .infinity) + .edgesIgnoringSafeArea(.vertical) + 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 listMonitor: ListMonitor + + @ObservedObject + private var listHelper: ListHelper + + @Binding + private var filter: Classic.ColorsDemo.Filter + + private func changeFilter() { + + Classic.ColorsDemo.filter = Classic.ColorsDemo.filter.next() + } + + private func clearColors() { + + Classic.ColorsDemo.dataStack.perform( + asynchronous: { transaction in + + try transaction.deleteAll(From()) + }, + completion: { _ in } + ) + } + + private func addColor() { + + Classic.ColorsDemo.dataStack.perform( + asynchronous: { transaction in + + _ = transaction.create(Into()) + }, + completion: { _ in } + ) + } + + private func shuffleColors() { + + Classic.ColorsDemo.dataStack.perform( + asynchronous: { transaction in + + for palette in try transaction.fetchAll(From()) { + + palette.setRandomHue() + } + }, + completion: { _ in } + ) + } + + + // MARK: - Classic.ColorsDemo.MainView.ListHelper + + fileprivate final class ListHelper: ObservableObject, ListObjectObserver { + + // MARK: FilePrivate + + fileprivate private(set) var count: Int = 0 + + fileprivate init(listMonitor: ListMonitor) { + + listMonitor.addObserver(self) + self.count = listMonitor.numberOfObjects() + } + + fileprivate func selectedObject() -> ObjectMonitor? { + + return self.selectedPalette.flatMap { + + guard !$0.isDeleted else { + + return nil + } + return Classic.ColorsDemo.dataStack.monitorObject($0) + } + } + + fileprivate func setSelectedPalette(_ palette: Classic.ColorsDemo.Palette?) { + + guard self.selectedPalette != palette else { + + return + } + self.objectWillChange.send() + if let palette = palette, !palette.isDeleted { + + self.selectedPalette = palette + } + else { + + self.selectedPalette = nil + } + } + + + // MARK: ListObserver + + typealias ListEntityType = Classic.ColorsDemo.Palette + + func listMonitorDidChange(_ monitor: ListMonitor) { + + self.objectWillChange.send() + self.count = monitor.numberOfObjects() + } + + func listMonitorDidRefetch(_ monitor: ListMonitor) { + + self.objectWillChange.send() + self.count = monitor.numberOfObjects() + } + + // MARK: ListObjectObserver + + func listMonitor(_ monitor: ListMonitor, didDeleteObject object: Classic.ColorsDemo.Palette, fromIndexPath indexPath: IndexPath) { + + if self.selectedPalette == object { + + self.setSelectedPalette(nil) + } + } + + + // MARK: Private + + private var selectedPalette: Classic.ColorsDemo.Palette? + } + } +} + +#if DEBUG + +struct _Demo_Classic_ColorsDemo_MainView_Preview: PreviewProvider { + + // MARK: PreviewProvider + + static var previews: some View { + + let minimumSamples = 10 + try! Classic.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + let missing = minimumSamples + - (try transaction.fetchCount(From())) + guard missing > 0 else { + return + } + for _ in 0..()) + palette.setRandomHue() + } + } + ) + return Classic.ColorsDemo.MainView() + } +} + +#endif diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.Palette.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.Palette.swift new file mode 100644 index 0000000..1c16082 --- /dev/null +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.Palette.swift @@ -0,0 +1,101 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreData +import UIKit + + +// MARK: - Classic.ColorsDemo.Palette + +@objc(Classic_ColorsDemo_Palette) +final class Classic_ColorsDemo_Palette: NSManagedObject { + + // MARK: Internal + + @NSManaged + dynamic var hue: Float + + @NSManaged + dynamic var saturation: Float + + @NSManaged + dynamic var brightness: Float + + @objc + dynamic var colorGroup: String! { + + let key = #keyPath(colorGroup) + if case let value as String = self.getValue(forKvcKey: key) { + + return value + } + let newValue: String + switch self.hue * 359 { + + case 0 ..< 20: newValue = "Lower Reds" + case 20 ..< 57: newValue = "Oranges and Browns" + case 57 ..< 90: newValue = "Yellow-Greens" + case 90 ..< 159: newValue = "Greens" + case 159 ..< 197: newValue = "Blue-Greens" + case 197 ..< 241: newValue = "Blues" + case 241 ..< 297: newValue = "Violets" + case 297 ..< 331: newValue = "Magentas" + default: newValue = "Upper Reds" + } + self.setPrimitiveValue(newValue, forKey: key) + return newValue + } + + var color: UIColor { + + let newValue = UIColor( + hue: CGFloat(self.hue), + saturation: CGFloat(self.saturation), + brightness: CGFloat(self.brightness), + alpha: 1.0 + ) + return newValue + } + + var colorText: String { + + let newValue: String = "H: \(self.hue * 359)˚, S: \(round(self.saturation * 100.0))%, B: \(round(self.brightness * 100.0))%" + return newValue + } + + func setRandomHue() { + + self.hue = Self.randomHue() + } + + + // MARK: NSManagedObject + + public override func awakeFromInsert() { + + super.awakeFromInsert() + + self.hue = Self.randomHue() + self.saturation = Self.randomSaturation() + self.brightness = Self.randomBrightness() + } + + + // MARK: Private + + private static func randomHue() -> Float { + + return Float.random(in: 0.0 ... 1.0) + } + + private static func randomSaturation() -> Float { + + return Float.random(in: 0.4 ... 1.0) + } + + private static func randomBrightness() -> Float { + + return Float.random(in: 0.0 ... 1.0) + } +} diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.swift new file mode 100644 index 0000000..a0122a0 --- /dev/null +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.swift @@ -0,0 +1,59 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore + +// MARK: - Classic + +extension Classic { + + // MARK: - Classic.ColorsDemo + + /** + Sample usages for observing lists or single instances of `NSManagedObject`s + */ + enum ColorsDemo { + + // MARK: Internal + + typealias Palette = Classic_ColorsDemo_Palette + + static let dataStack: DataStack = { + + let dataStack = DataStack( + xcodeModelName: "Classic.ColorsDemo", + bundle: Bundle(for: Palette.self) + ) + + /** + - Important: `addStorageAndWait(_:)` was used here to simplify initializing the demo, but in practice the asynchronous function variants are recommended. + */ + try! dataStack.addStorageAndWait( + SQLiteStore( + fileName: "Classic.ColorsDemo.sqlite", + localStorageOptions: .recreateStoreOnModelMismatch + ) + ) + return dataStack + }() + + static let palettesMonitor: ListMonitor = Classic.ColorsDemo.dataStack.monitorSectionedList( + From() + .sectionBy(\.colorGroup) + .where(Classic.ColorsDemo.filter.whereClause()) + .orderBy(.ascending(\.hue)) + ) + + static var filter: Classic.ColorsDemo.Filter = .all { + + didSet { + + Classic.ColorsDemo.palettesMonitor.refetch( + self.filter.whereClause(), + OrderBy(.ascending(\.hue)) + ) + } + } + } +} diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.xcdatamodeld/ColorsDemo.xcdatamodel/contents b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.xcdatamodeld/ColorsDemo.xcdatamodel/contents new file mode 100644 index 0000000..e3e0f30 --- /dev/null +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/Classic.ColorsDemo.xcdatamodeld/ColorsDemo.xcdatamodel/contents @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/⭐️Classic.ColorsDemo.DetailViewController.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/⭐️Classic.ColorsDemo.DetailViewController.swift new file mode 100644 index 0000000..e97f740 --- /dev/null +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/⭐️Classic.ColorsDemo.DetailViewController.swift @@ -0,0 +1,291 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import UIKit + + +// MARK: - Classic.ColorsDemo + +extension Classic.ColorsDemo { + + // MARK: - Classic.ColorsDemo.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. + */ + var palette: ObjectMonitor { + + didSet { + + oldValue.removeObserver(self) + + self.startMonitoringObject() + } + } + + init(_ palette: ObjectMonitor) { + + self.palette = palette + 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: Classic.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: \Classic.ColorsDemo.Palette.hue)) == true { + + self.hueSlider.value = Float(palette.hue) + } + if changedKeys == nil + || changedKeys?.contains(String(keyPath: \Classic.ColorsDemo.Palette.saturation)) == true { + + self.saturationSlider.value = palette.saturation + } + if changedKeys == nil + || changedKeys?.contains(String(keyPath: \Classic.ColorsDemo.Palette.brightness)) == true { + + self.brightnessSlider.value = palette.brightness + } + } + + + // MARK: ObjectObserver + + func objectMonitor( + _ monitor: ObjectMonitor, + didUpdateObject object: Classic.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 + + @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 + Classic.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 + Classic.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 + Classic.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/⭐️Classic/⭐️ColorsDemo/⭐️Classic.ColorsDemo.ListViewController.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/⭐️Classic.ColorsDemo.ListViewController.swift new file mode 100644 index 0000000..940c160 --- /dev/null +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Classic/⭐️ColorsDemo/⭐️Classic.ColorsDemo.ListViewController.swift @@ -0,0 +1,199 @@ +// +// Demo +// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. + +import CoreStore +import UIKit + + +// MARK: - Classic.ColorsDemo + +extension Classic.ColorsDemo { + + // MARK: - Classic.ColorsDemo.ListViewController + + final class ListViewController: UITableViewController, ListSectionObserver { + + /** + ⭐️ Sample 1: Once the views are created, we can start observing `ListMonitor` updates. 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 `tableView.reloadData()` once. + */ + private func startObservingList() { + + self.listMonitor.addObserver(self) + self.tableView.reloadData() + } + + /** + ⭐️ Sample 2: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ListMonitor`s safely remove deallocated observers automatically. + */ + deinit { + + self.listMonitor.removeObserver(self) + } + + + /** + ⭐️ Sample 3: `ListSectionObserver` (and inherently, `ListObjectObserver` and `ListObserver`) conformance + */ + + // MARK: ListObserver + + typealias ListEntityType = Classic.ColorsDemo.Palette + + func listMonitorWillChange(_ monitor: ListMonitor) { + + self.tableView.beginUpdates() + } + + func listMonitorDidChange(_ monitor: ListMonitor) { + + self.tableView.endUpdates() + } + + func listMonitorDidRefetch(_ monitor: ListMonitor) { + + self.tableView.reloadData() + } + + + // MARK: ListObjectObserver + + func listMonitor(_ monitor: ListMonitor, didInsertObject object: ListEntityType, toIndexPath indexPath: IndexPath) { + + self.tableView.insertRows(at: [indexPath], with: .automatic) + } + + func listMonitor(_ monitor: ListMonitor, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath) { + + self.tableView.deleteRows(at: [indexPath], with: .automatic) + } + + func listMonitor(_ monitor: ListMonitor, didUpdateObject object: ListEntityType, atIndexPath indexPath: IndexPath) { + + if case let cell as Classic.ColorsDemo.ItemCell = self.tableView.cellForRow(at: indexPath) { + + cell.setPalette(object) + } + } + + func listMonitor(_ monitor: ListMonitor, didMoveObject object: ListEntityType, fromIndexPath: IndexPath, toIndexPath: IndexPath) { + + self.tableView.deleteRows(at: [fromIndexPath], with: .automatic) + self.tableView.insertRows(at: [toIndexPath], with: .automatic) + } + + + // MARK: ListSectionObserver + + func listMonitor(_ monitor: ListMonitor, didInsertSection sectionInfo: NSFetchedResultsSectionInfo, toSectionIndex sectionIndex: Int) { + + self.tableView.insertSections(IndexSet(integer: sectionIndex), with: .automatic) + } + + func listMonitor(_ monitor: ListMonitor, didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, fromSectionIndex sectionIndex: Int) { + + self.tableView.deleteSections(IndexSet(integer: sectionIndex), with: .automatic) + } + + + // MARK: UITableViewDataSource + + override func numberOfSections(in tableView: UITableView) -> Int { + + return self.listMonitor.numberOfSections() + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + + return self.listMonitor.numberOfObjects(in: section) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let cell = tableView.dequeueReusableCell( + withIdentifier: Classic.ColorsDemo.ItemCell.reuseIdentifier, + for: indexPath + ) as! Classic.ColorsDemo.ItemCell + cell.setPalette(self.listMonitor[indexPath]) + return cell + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + + return self.listMonitor.sectionInfo(at: section).name + } + + + // MARK: UITableViewDelegate + + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + + switch editingStyle { + + case .delete: + let object = self.listMonitor[indexPath] + Classic.ColorsDemo.dataStack.perform( + asynchronous: { (transaction) in + + transaction.delete(object) + }, + completion: { _ in } + ) + + default: + break + } + } + + + // MARK: Internal + + init( + listMonitor: ListMonitor, + onPaletteTapped: @escaping (Classic.ColorsDemo.Palette) -> Void + ) { + + self.listMonitor = listMonitor + self.onPaletteTapped = onPaletteTapped + + super.init(style: .plain) + } + + + // MARK: UIViewController + + override func viewDidLoad() { + + super.viewDidLoad() + + self.tableView.register( + Classic.ColorsDemo.ItemCell.self, + forCellReuseIdentifier: Classic.ColorsDemo.ItemCell.reuseIdentifier + ) + + self.startObservingList() + } + + + // MARK: UITableViewDelegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + self.onPaletteTapped( + self.listMonitor[indexPath] + ) + } + + + // MARK: Private + + private let listMonitor: ListMonitor + private let onPaletteTapped: (Classic.ColorsDemo.Palette) -> Void + + @available(*, unavailable) + required init?(coder: NSCoder) { + + fatalError() + } + } +} diff --git a/Demo/Sources/Demos/Modern/Modern.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/Modern.swift similarity index 100% rename from Demo/Sources/Demos/Modern/Modern.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/Modern.swift diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Filter.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.Filter.swift similarity index 82% rename from Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Filter.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.Filter.swift index 3e5747e..e9a77bb 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Filter.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.Filter.swift @@ -28,8 +28,8 @@ extension Modern.ColorsDemo { switch self { case .all: return .init() - case .light: return (\Modern.ColorsDemo.Palette.$brightness >= 0.9) - case .dark: return (\Modern.ColorsDemo.Palette.$brightness <= 0.4) + case .light: return (\.$brightness >= 0.9) + case .dark: return (\.$brightness <= 0.4) } } } diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Palette.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.Palette.swift similarity index 80% rename from Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Palette.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.Palette.swift index 734a404..150b89b 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.Palette.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.Palette.swift @@ -49,31 +49,31 @@ extension Modern.ColorsDemo { var brightness: Float @Field.Virtual( - "colorName", + "colorGroup", customGetter: { object, field in - if let colorName = field.primitiveValue { + if let colorGroup = field.primitiveValue { - return colorName + return colorGroup } - let colorName: String + let colorGroup: 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" + case 0 ..< 20: colorGroup = "Lower Reds" + case 20 ..< 57: colorGroup = "Oranges and Browns" + case 57 ..< 90: colorGroup = "Yellow-Greens" + case 90 ..< 159: colorGroup = "Greens" + case 159 ..< 197: colorGroup = "Blue-Greens" + case 197 ..< 241: colorGroup = "Blues" + case 241 ..< 297: colorGroup = "Violets" + case 297 ..< 331: colorGroup = "Magentas" + default: colorGroup = "Upper Reds" } - field.primitiveValue = colorName - return colorName + field.primitiveValue = colorGroup + return colorGroup } ) - var colorName: String + var colorGroup: String @Field.Virtual( "color", @@ -120,7 +120,7 @@ extension Modern.ColorsDemo { private static func resetVirtualProperties(_ object: ObjectProxy) { - object.$colorName.primitiveValue = nil + object.$colorGroup.primitiveValue = nil object.$color.primitiveValue = nil object.$colorText.primitiveValue = nil } diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.SwiftUI.swift similarity index 100% rename from Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.SwiftUI.swift diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.UIKit.DetailView.swift similarity index 100% rename from Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailView.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.UIKit.DetailView.swift diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ItemCell.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.UIKit.ItemCell.swift similarity index 100% rename from Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ItemCell.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.UIKit.ItemCell.swift diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.UIKit.ListView.swift similarity index 100% rename from Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListView.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.UIKit.ListView.swift diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.UIKit.swift similarity index 100% rename from Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.UIKit.swift diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.swift similarity index 95% rename from Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.swift index d8b7ed0..03b0e8c 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.swift @@ -45,7 +45,7 @@ extension Modern { static let palettesPublisher: ListPublisher = Modern.ColorsDemo.dataStack.publishList( From() - .sectionBy(\.$colorName) + .sectionBy(\.$colorGroup) .where(Modern.ColorsDemo.filter.whereClause()) .orderBy(.ascending(\.$hue)) ) @@ -56,7 +56,7 @@ extension Modern { try! Modern.ColorsDemo.palettesPublisher.refetch( From() - .sectionBy(\.$colorName) + .sectionBy(\.$colorGroup) .where(self.filter.whereClause()) .orderBy(.ascending(\.$hue)) ) diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/⭐️Modern.ColorsDemo.MainView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.MainView.swift similarity index 100% rename from Demo/Sources/Demos/Modern/ColorsDemo/⭐️Modern.ColorsDemo.MainView.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.MainView.swift diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.DetailView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.DetailView.swift similarity index 100% rename from Demo/Sources/Demos/Modern/ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.DetailView.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.DetailView.swift diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ItemView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ItemView.swift similarity index 100% rename from Demo/Sources/Demos/Modern/ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ItemView.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ItemView.swift diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ListView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ListView.swift similarity index 100% rename from Demo/Sources/Demos/Modern/ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ListView.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ListView.swift diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/⭐️Modern.ColorsDemo.UIKit.DetailViewController.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.UIKit.DetailViewController.swift similarity index 100% rename from Demo/Sources/Demos/Modern/ColorsDemo/⭐️Modern.ColorsDemo.UIKit.DetailViewController.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.UIKit.DetailViewController.swift diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/⭐️Modern.ColorsDemo.UIKit.ListViewController.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.UIKit.ListViewController.swift similarity index 100% rename from Demo/Sources/Demos/Modern/ColorsDemo/⭐️Modern.ColorsDemo.UIKit.ListViewController.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.UIKit.ListViewController.swift diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Geocoder.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/Modern.PlacemarksDemo.Geocoder.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Geocoder.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/Modern.PlacemarksDemo.Geocoder.swift diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MapView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/Modern.PlacemarksDemo.MapView.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MapView.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/Modern.PlacemarksDemo.MapView.swift diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Place.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/Modern.PlacemarksDemo.Place.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Place.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/Modern.PlacemarksDemo.Place.swift diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/⭐️Modern.PlacemarksDemo.MainView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/⭐️Modern.PlacemarksDemo.MainView.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PlacemarksDemo/⭐️Modern.PlacemarksDemo.MainView.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/⭐️Modern.PlacemarksDemo.MainView.swift diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/⭐️Modern.PlacemarksDemo.swift similarity index 95% rename from Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/⭐️Modern.PlacemarksDemo.swift index 87057f7..86764c1 100644 --- a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/⭐️Modern.PlacemarksDemo.swift @@ -17,6 +17,9 @@ extension Modern { // MARK: Internal + /** + ⭐️ Sample 1: Setting up the `DataStack` and storage + */ static let dataStack: DataStack = { let dataStack = DataStack( diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Details.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.Details.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Details.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.Details.swift diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ItemCell.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.ItemCell.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ItemCell.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.ItemCell.swift diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ListView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.ListView.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ListView.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.ListView.swift diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ListViewController.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.ListViewController.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.ListViewController.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.ListViewController.swift diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.MainView.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.MainView.swift diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.swift diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/⭐️Modern.PokedexDemo.Form.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/⭐️Modern.PokedexDemo.Form.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PokedexDemo/⭐️Modern.PokedexDemo.Form.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/⭐️Modern.PokedexDemo.Form.swift diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/⭐️Modern.PokedexDemo.PokedexEntry.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/⭐️Modern.PokedexDemo.PokedexEntry.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PokedexDemo/⭐️Modern.PokedexDemo.PokedexEntry.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/⭐️Modern.PokedexDemo.PokedexEntry.swift diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/⭐️Modern.PokedexDemo.PokemonType.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/⭐️Modern.PokedexDemo.PokemonType.swift similarity index 98% rename from Demo/Sources/Demos/Modern/PokedexDemo/⭐️Modern.PokedexDemo.PokemonType.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/⭐️Modern.PokedexDemo.PokemonType.swift index 408d9b6..b05c057 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/⭐️Modern.PokedexDemo.PokemonType.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/⭐️Modern.PokedexDemo.PokemonType.swift @@ -9,7 +9,7 @@ import UIKit extension Modern.PokedexDemo { - // MARK: - Modern.PokedexDemo.Move + // MARK: - Modern.PokedexDemo.PokemonType /** ⭐️ Sample 1: Types that will be used with `@Field.Stored` need to implement both `ImportableAttributeType` and `FieldStorableType`. In this case, `RawRepresentable` types with primitive `RawValue`s have built-in implementations so we only have to declare conformance to `ImportableAttributeType` and `FieldStorableType`. diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/⭐️Modern.PokedexDemo.Service.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/⭐️Modern.PokedexDemo.Service.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PokedexDemo/⭐️Modern.PokedexDemo.Service.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/⭐️Modern.PokedexDemo.Service.swift diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/⭐️Modern.PokedexDemo.Species.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/⭐️Modern.PokedexDemo.Species.swift similarity index 100% rename from Demo/Sources/Demos/Modern/PokedexDemo/⭐️Modern.PokedexDemo.Species.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/⭐️Modern.PokedexDemo.Species.swift diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift similarity index 100% rename from Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ListView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️TimeZonesDemo/Modern.TimeZonesDemo.ListView.swift similarity index 100% rename from Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ListView.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️TimeZonesDemo/Modern.TimeZonesDemo.ListView.swift diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.TimeZone.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️TimeZonesDemo/Modern.TimeZonesDemo.TimeZone.swift similarity index 100% rename from Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.TimeZone.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️TimeZonesDemo/Modern.TimeZonesDemo.TimeZone.swift diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️TimeZonesDemo/Modern.TimeZonesDemo.swift similarity index 100% rename from Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️TimeZonesDemo/Modern.TimeZonesDemo.swift diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/⭐️Modern.TimeZonesDemo.MainView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️TimeZonesDemo/⭐️Modern.TimeZonesDemo.MainView.swift similarity index 100% rename from Demo/Sources/Demos/Modern/TimeZonesDemo/⭐️Modern.TimeZonesDemo.MainView.swift rename to Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️TimeZonesDemo/⭐️Modern.TimeZonesDemo.MainView.swift diff --git a/README.md b/README.md index af133f0..950f56d 100644 --- a/README.md +++ b/README.md @@ -1914,14 +1914,14 @@ class Vehicle: CoreStoreObject { Built-in encoders such as `FieldCoders.NSCoding`, `FieldCoders.Json`, and `FieldCoders.Plist` are available, and custom encoding/decoding is also supported: ```swift class Person: CoreStoreObject { -
+ struct CustomInfo: Codable { // ... } -
+ @Field.Coded("otherInfo", coder: FieldCoders.Json.self) var otherInfo: CustomInfo? -
+ @Field.Coded( "photo", coder: { diff --git a/Sources/Field.Coded.swift b/Sources/Field.Coded.swift index dcadfa5..4f8df54 100644 --- a/Sources/Field.Coded.swift +++ b/Sources/Field.Coded.swift @@ -65,7 +65,8 @@ extension FieldContainer { var eyeColor: UIColor = .black } ``` - - parameter initial: the initial value for the property when the object is first created. + - Important: Any changes in the `coder` are not reflected in the VersionLock, so make sure that the encoder and decoder logic is compatible for all versions of your persistent store. + - parameter initial: the initial value for the property that is shared for all instances of this object. Note that this is evaluated during `DataStack` setup, not during object creation. To assign a value during object creation, use the `dynamicInitialValue` argument instead. - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. @@ -99,6 +100,25 @@ extension FieldContainer { ) } + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + + @Field.Coded("eyeColor", coder: FieldCoders.NSCoding.self, dynamicInitialValue: { UIColor.random() }) + var eyeColor: UIColor + } + ``` + - Important: Any changes in the `coder` are not reflected in the VersionLock, so make sure that the encoder and decoder logic is compatible for all versions of your persistent store. + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter coder: The `FieldCoderType` to be used for encoding and decoding the value + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + - parameter dynamicInitialValue: the initial value for the property when the object is first created. + */ public init( _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, @@ -139,7 +159,8 @@ extension FieldContainer { var bloodType: BloodType = .unknown } ``` - - parameter initial: the initial value for the property when the object is first created. + - Important: Any changes in the encoder/decoder are not reflected in the VersionLock, so make sure that the encoder and decoder logic is compatible for all versions of your persistent store. + - parameter initial: the initial value for the property that is shared for all instances of this object. Note that this is evaluated during `DataStack` setup, not during object creation. To assign a value during object creation, use the `dynamicInitialValue` argument instead. - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. @@ -172,7 +193,33 @@ extension FieldContainer { affectedByKeyPaths: affectedByKeyPaths ) } - + + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + + @Field.Coded( + "bloodType", + coder: { + encode: { $0.toData() }, + decode: { BloodType(fromData: $0) } + }, + dynamicInitialValue: { BloodType.random() } + ) + var bloodType: BloodType + } + ``` + - Important: Any changes in the encoder/decoder are not reflected in the VersionLock, so make sure that the encoder and decoder logic is compatible for all versions of your persistent store. + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter coder: The closures to be used for encoding and decoding the value + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + - parameter dynamicInitialValue: the initial value for the property when the object is first created. + */ public init( _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, @@ -467,7 +514,8 @@ extension FieldContainer.Coded where V: FieldOptionalType { var eyeColor: UIColor? = nil } ``` - - parameter initial: the initial value for the property when the object is first created. + - Important: Any changes in the `coder` are not reflected in the VersionLock, so make sure that the encoder and decoder logic is compatible for all versions of your persistent store. + - parameter initial: the initial value for the property that is shared for all instances of this object. Note that this is evaluated during `DataStack` setup, not during object creation. To assign a value during object creation, use the `dynamicInitialValue` argument instead. - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. @@ -501,6 +549,25 @@ extension FieldContainer.Coded where V: FieldOptionalType { ) } + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + + @Field.Coded("eyeColor", coder: FieldCoders.NSCoding.self, dynamicInitialValue: { UIColor.random() }) + var eyeColor: UIColor? + } + ``` + - Important: Any changes in the `coder` are not reflected in the VersionLock, so make sure that the encoder and decoder logic is compatible for all versions of your persistent store. + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter coder: The `FieldCoderType` to be used for encoding and decoding the value + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + - parameter dynamicInitialValue: the initial value for the property when the object is first created. + */ public init( _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, @@ -541,7 +608,8 @@ extension FieldContainer.Coded where V: FieldOptionalType { var bloodType: BloodType? } ``` - - parameter initial: the initial value for the property when the object is first created. + - Important: Any changes in the encoder/decoder are not reflected in the VersionLock, so make sure that the encoder and decoder logic is compatible for all versions of your persistent store. + - parameter initial: the initial value for the property that is shared for all instances of this object. Note that this is evaluated during `DataStack` setup, not during object creation. To assign a value during object creation, use the `dynamicInitialValue` argument instead. - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. @@ -574,7 +642,33 @@ extension FieldContainer.Coded where V: FieldOptionalType { affectedByKeyPaths: affectedByKeyPaths ) } - + + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + + @Field.Coded( + "bloodType", + coder: { + encode: { $0.toData() }, + decode: { BloodType(fromData: $0) } + }, + dynamicInitialValue: { BloodType.random() } + ) + var bloodType: BloodType? + } + ``` + - Important: Any changes in the encoder/decoder are not reflected in the VersionLock, so make sure that the encoder and decoder logic is compatible for all versions of your persistent store. + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter coder: The closures to be used for encoding and decoding the value + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + - parameter dynamicInitialValue: the initial value for the property when the object is first created. + */ public init( _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, @@ -615,7 +709,7 @@ extension FieldContainer.Coded where V: DefaultNSSecureCodable { var customInfo: NSDictionary = [:] } ``` - - parameter initial: the initial value for the property when the object is first created. + - parameter initial: the initial value for the property that is shared for all instances of this object. Note that this is evaluated during `DataStack` setup, not during object creation. To assign a value during object creation, use the `dynamicInitialValue` argument instead. - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. @@ -647,6 +741,23 @@ extension FieldContainer.Coded where V: DefaultNSSecureCodable { ) } + /** + Initializes the metadata for the property. This overload is for types supported by Core Data's default NSSecureCodable implementation: `NSArray`, `NSDictionary`, `NSSet`, `NSString`, `NSNumber`, `NSDate`, `NSData`, `NSURL`, `NSUUID`, and `NSNull`. + ``` + class Person: CoreStoreObject { + + @Field.Coded("customInfo", dynamicInitialValue: { ["id": UUID()] }) + var customInfo: NSDictionary + } + ``` + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + - parameter dynamicInitialValue: the initial value for the property when the object is first created. + */ public init( _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, @@ -686,7 +797,7 @@ extension FieldContainer.Coded where V: FieldOptionalType, V.Wrapped: DefaultNSS var customInfo: NSDictionary? = nil } ``` - - parameter initial: the initial value for the property when the object is first created. + - parameter initial: the initial value for the property that is shared for all instances of this object. Note that this is evaluated during `DataStack` setup, not during object creation. To assign a value during object creation, use the `dynamicInitialValue` argument instead. - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. @@ -718,6 +829,23 @@ extension FieldContainer.Coded where V: FieldOptionalType, V.Wrapped: DefaultNSS ) } + /** + Initializes the metadata for the property. This overload is for types supported by Core Data's default NSSecureCodable implementation: `NSArray`, `NSDictionary`, `NSSet`, `NSString`, `NSNumber`, `NSDate`, `NSData`, `NSURL`, `NSUUID`, and `NSNull`. + ``` + class Person: CoreStoreObject { + + @Field.Coded("customInfo", dynamicInitialValue: { ["id": UUID()] }) + var customInfo: NSDictionary? + } + ``` + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + - parameter dynamicInitialValue: the initial value for the property when the object is first created. + */ public init( _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, diff --git a/Sources/Field.Stored.swift b/Sources/Field.Stored.swift index 69f771d..070f57b 100644 --- a/Sources/Field.Stored.swift +++ b/Sources/Field.Stored.swift @@ -59,7 +59,7 @@ extension FieldContainer { var title: String = "Mr." } ``` - - parameter initial: the initial value for the property when the object is first created. + - parameter initial: the initial value for the property that is shared for all instances of this object. Note that this is evaluated during `DataStack` setup, not during object creation. To assign a value during object creation, use the `dynamicInitialValue` argument instead. - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. @@ -90,6 +90,23 @@ extension FieldContainer { ) } + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + + @Field.Stored("title", dynamicInitialValue: { Person.randomTitle() }) + var title: String + } + ``` + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + - parameter dynamicInitialValue: the initial value for the property when the object is first created. + */ public init( _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, @@ -360,7 +377,7 @@ extension FieldContainer.Stored where V: FieldOptionalType { var nickname: String? } ``` - - parameter initial: the initial value for the property when the object is first created. + - parameter initial: the initial value for the property that is shared for all instances of this object. Note that this is evaluated during `DataStack` setup, not during object creation. To assign a value during object creation, use the `dynamicInitialValue` argument instead. - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. @@ -391,6 +408,23 @@ extension FieldContainer.Stored where V: FieldOptionalType { ) } + /** + Initializes the metadata for the property. + ``` + class Person: CoreStoreObject { + + @Field.Stored("nickname", dynamicInitialValue: { Person.randomNickname() }) + var nickname: String? + } + ``` + - parameter keyPath: the permanent attribute name for this property. + - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) + - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. + - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. + - parameter dynamicInitialValue: the initial value for the property when the object is first created. + */ public init( _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, diff --git a/Sources/Transformable.Optional.swift b/Sources/Transformable.Optional.swift index fa647cb..500800b 100644 --- a/Sources/Transformable.Optional.swift +++ b/Sources/Transformable.Optional.swift @@ -74,7 +74,7 @@ extension TransformableContainer { } ``` - parameter keyPath: the permanent attribute name for this property. - - parameter initial: the initial value for the property when the object is first created. Defaults to the `ImportableAttributeType`'s empty value if not specified. + - parameter initial: the initial value for the property that is shared for all instances of this object. Note that this is evaluated during `DataStack` setup, not during object creation. Defaults to the `ImportableAttributeType`'s empty value if not specified. - parameter isTransient: `true` if the property is transient, otherwise `false`. Defaults to `false` if not specified. The transient flag specifies whether or not a property's value is ignored when an object is saved to a persistent store. Transient properties are not saved to the persistent store, but are still managed for undo, redo, validation, and so on. - parameter allowsExternalBinaryDataStorage: `true` if the attribute allows external binary storage, otherwise `false`. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) diff --git a/Sources/Transformable.Required.swift b/Sources/Transformable.Required.swift index fdf4e5c..0b85d95 100644 --- a/Sources/Transformable.Required.swift +++ b/Sources/Transformable.Required.swift @@ -77,7 +77,7 @@ extension TransformableContainer { } ``` - parameter keyPath: the permanent attribute name for this property. - - parameter initial: the initial value for the property when the object is first created. Defaults to the `ImportableAttributeType`'s empty value if not specified. + - parameter initial: the initial value for the property that is shared for all instances of this object. Note that this is evaluated during `DataStack` setup, not during object creation. Defaults to the `ImportableAttributeType`'s empty value if not specified. - parameter isTransient: `true` if the property is transient, otherwise `false`. Defaults to `false` if not specified. The transient flag specifies whether or not a property's value is ignored when an object is saved to a persistent store. Transient properties are not saved to the persistent store, but are still managed for undo, redo, validation, and so on. - parameter allowsExternalBinaryDataStorage: `true` if the attribute allows external binary storage, otherwise `false`. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) diff --git a/Sources/Value.Optional.swift b/Sources/Value.Optional.swift index ec83d40..a348254 100644 --- a/Sources/Value.Optional.swift +++ b/Sources/Value.Optional.swift @@ -71,7 +71,7 @@ extension ValueContainer { } ``` - parameter keyPath: the permanent attribute name for this property. - - parameter initial: the initial value for the property when the object is first created. Defaults to `nil` if not specified. + - parameter initial: the initial value for the property that is shared for all instances of this object. Note that this is evaluated during `DataStack` setup, not during object creation. Defaults to `nil` if not specified. - parameter isTransient: `true` if the property is transient, otherwise `false`. Defaults to `false` if not specified. The transient flag specifies whether or not a property's value is ignored when an object is saved to a persistent store. Transient properties are not saved to the persistent store, but are still managed for undo, redo, validation, and so on. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter renamingIdentifier: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property and a destination entity property that share the same identifier indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's name.