How do I execute a SectionedFetchRequest in a SwiftUI Preview? - core-data

Previously, I was using FetchRequest and passing the results to my view. I had this working in my preview using the following code:
struct SchoolsGrid_Previews: PreviewProvider {
static var viewContext = PersistenceController.preview.container.viewContext
static var fetchRequest = School.fetchRequest()
static func getSchools() -> [School] {
do {
return try viewContext.fetch(fetchRequest)
} catch {
print(error.localizedDescription)
return []
}
}
static var previews: some View {
SchoolsGrid(sectionedSchools: Dictionary(
grouping: getSchools(),
by: {
$0.name!.first!.isLetter ? String($0.name!.first!) : "#"
}
))
.environment(\.managedObjectContext, viewContext)
.environmentObject(SettingsData())
}
}
After learning about SectionedFetchRequest, I updated my code and my view to accept this instead, but I am unable to find the "sectioned" alternative to the code above. I tried the following without any luck - it returns no data:
struct SchoolsGrid_Previews: PreviewProvider {
static var viewContext = PersistenceController.preview.container.viewContext
static var fetchRequest = SectionedFetchRequest(fetchRequest: School.fetchRequest(), sectionIdentifier: \School.firstLetterOfName)
static var previews: some View {
SchoolsGrid(sectionedSchools: fetchRequest.wrappedValue)
.environment(\.managedObjectContext, viewContext)
.environmentObject(SettingsData())
}
}
Edit to add:
I have a wrapper view which fetches my data, because the same data can be presented in two different subviews - SchoolsGrid and SchoolsList:
struct Schools: View {
#State var isGrid = false
#State var searchTerm: String = ""
#SectionedFetchRequest<String, School>(
entity: School.entity(),
sectionIdentifier: \.firstLetterOfName,
sortDescriptors: [
NSSortDescriptor(keyPath: \School.name, ascending: true),
]
) var sectionedSchools: SectionedFetchResults<String, School>
var query: Binding<String> {
Binding {
searchTerm
} set: { newValue in
searchTerm = newValue
let namePredicate = NSPredicate(format: "%K CONTAINS[c] %#", #keyPath(School.name), newValue)
sectionedSchools.nsPredicate = newValue.isEmpty
? nil
: namePredicate
}
}
var body: some View {
NavigationView {
ScrollViewReader { _ in
VStack {
ZStack {
if isGrid {
SchoolsGrid(sectionedSchools: sectionedSchools, searchTerm: searchTerm)
} else {
SchoolsList(sectionedSchools: sectionedSchools, searchTerm: searchTerm)
}
}
}
.searchable(text: query)
}
.navigationTitle("Schools")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button(action: { isGrid.toggle() }) {
Image(systemName: isGrid ? "list.bullet" : "square.grid.2x2")
.accessibilityLabel("Toggle View")
}
}
}
}
}
}
My SchoolsGrid looks like:
struct SchoolsGrid: View {
var sectionedSchools: SectionedFetchResults<String, School>
var searchTerm: String
var gridItems: [GridItem] {
Array(repeating: .init(.adaptive(minimum: 120)), count: 2)
}
var body: some View {
ScrollView {
ForEach(sectionedSchools) { section in
Section(header: SchoolsGridSectionHeader(key: section.id)) {
LazyVGrid(columns: gridItems) {
ForEach(section) { school in
NavigationLink(destination: SchoolDetail(school: school)) {
SchoolsGridItem(school: school, searchTerm: searchTerm)
}
}
}
}
}
}
}
}
And my SchoolsList:
struct SchoolsList: View {
var sectionedSchools: SectionedFetchResults<String, School>
var searchTerm: String
var body: some View {
List {
ForEach(sectionedSchools) { section in
Section(header: Text(section.id).id(section.id)) {
ForEach(section) { school in
NavigationLink(destination: SchoolDetail(school: school)) {
SchoolsListItem(school: school, searchTerm: searchTerm)
}
}
}
}
}
.listStyle(PlainListStyle())
}
}
As with the first part of my question, I only recently changed the fetch request to a sectioned fetch request, so previously both SchoolsGrid and SchoolsList accepted schools: FetchResults< School> and I was able to emulate this in the Preview using the first code block, where I used viewContext.fetch. After updating to accept sectionedSchools: SectionedFetchResults<String, School>, there is no viewContext.sectionedFetch that I can use, so passing it the way I tried using:
var fetchRequest = SectionedFetchRequest(fetchRequest: School.fetchRequest(), sectionIdentifier: \School.firstLetterOfName)
returns no data because it isn't being called on the preview view context.

Related

Custom Card rendering when no longer presented?

I'm new to building custom cards for HASS so this might be obvious.
I have a basic clock card and I have put a console message on the render method - it seems to be writing to the log even when the card is no longer being presented? (i.e. you've moved to another lovelace view).
I'm using a setTimeout to trigger a property change - am I meant to stop the timeout at some point of the lifecycle, or is there some teardown in the lifecycle, etc?
Here's my code:
/* eslint-disable #typescript-eslint/no-explicit-any */
import {
LitElement,
html,
customElement,
property,
CSSResult,
TemplateResult,
css,
PropertyValues,
internalProperty,
} from 'lit-element';
import {
HomeAssistant,
hasConfigOrEntityChanged,
hasAction,
ActionHandlerEvent,
handleAction,
LovelaceCardEditor,
getLovelace,
LovelaceCard,
} from 'custom-card-helpers'; // This is a community maintained npm module with common helper functions/types
import { hass, provideHass } from "card-tools/src/hass";
import './editor';
import type { BoilerplateCardConfig } from './types';
import { actionHandler } from './action-handler-directive';
import { CARD_VERSION } from './const';
import { localize } from './localize/localize';
/* eslint no-console: 0 */
console.info(
`%c BOILERPLATE-CARD \n%c ${localize('common.version')} ${CARD_VERSION} `,
'color: orange; font-weight: bold; background: black',
'color: white; font-weight: bold; background: dimgray',
);
// This puts your card into the UI card picker dialog
(window as any).customCards = (window as any).customCards || [];
(window as any).customCards.push({
type: 'boilerplate-card',
name: 'Boilerplate Card',
description: 'A template custom card for you to create something awesome',
});
// TODO Name your custom element
#customElement('boilerplate-card')
export class BoilerplateCard extends LitElement {
CUSTOM_TYPE_PREFIX = "custom:";
constructor() {
super();
this.date = new Date();
setInterval(() => {
this.date = new Date();
}, 1000);
}
public static async getConfigElement(): Promise<LovelaceCardEditor> {
return document.createElement('boilerplate-card-editor');
}
public static getStubConfig(): object {
return {};
}
// TODO Add any properities that should cause your element to re-render here
// https://lit-element.polymer-project.org/guide/properties
#property({ attribute: false }) public hass!: HomeAssistant;
#internalProperty() private date: Date;
#internalProperty() private config!: BoilerplateCardConfig;
// https://lit-element.polymer-project.org/guide/properties#accessors-custom
public setConfig(config: BoilerplateCardConfig): void {
// TODO Check for required fields and that they are of the proper format
if (!config) {
throw new Error(localize('common.invalid_configuration'));
}
if (config.test_gui) {
getLovelace().setEditMode(true);
}
this.config = {
name: 'Boilerplate',
...config,
};
}
// https://lit-element.polymer-project.org/guide/lifecycle#shouldupdate
protected shouldUpdate(changedProps: PropertyValues): boolean {
return hasConfigOrEntityChanged(this, changedProps, true);
}
// https://lit-element.polymer-project.org/guide/templates
protected render(): TemplateResult | void {
const timeFormatter: Intl.DateTimeFormatOptions = {
year: undefined,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
}
console.info("Draw")
return html`
<ha-card
.header=${this.config.name}
.actionHandler=${actionHandler({
hasHold: hasAction(this.config.hold_action),
hasDoubleClick: hasAction(this.config.double_tap_action),
})}
tabindex="0"
.label=${`Boilerplate: ${this.config.entity || 'No Entity Defined'}`}
>
<h1>${new Intl.DateTimeFormat(undefined, timeFormatter).format(this.date)}</h1>
${this.config.cards.map((card) => {
let tag = card.type;
if (tag.startsWith(this.CUSTOM_TYPE_PREFIX)) {
tag = tag.substr(this.CUSTOM_TYPE_PREFIX.length);
} else {
tag = `hui-${tag}-card`;
}
const cardElement = document.createElement(tag) as LovelaceCard;
cardElement.setConfig(card);
cardElement.hass = hass();
return cardElement
})}
</ha-card>
`;
}
// https://lit-element.polymer-project.org/guide/styles
static get styles(): CSSResult {
return css``;
}
}
Use connectedCallback and disconnectedCallback to start and stop your timer:
#customElement('boilerplate-card')
export class BoilerplateCard extends LitElement {
connectedCallback() {
super.connectedCallback();
this.date = new Date();
this.interval = setInterval(() => {
this.date = new Date();
}, 1000);
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.interval);
}
...
}

Fetch core data object into a SwiftUI picker

Goal: fetch Fetch core data object into a SwiftUI picker
Problem: haven't figured out how to pass data into a Picker
import SwiftUI
struct DetailView: View {
#ObservedObject var order: Order
#State var showOrderEdit = false
var body: some View {
Form{
Text(order.flavor)
Text(order.tableNumber)
}
.navigationTitle(order.pizzaType)
.toolbar {
ToolbarItem(placement: .primaryAction) {
//edit button
Button(action: {
showOrderEdit = true
}, label: {
Text("Edit")
})
.sheet(isPresented: $showOrderEdit) {
DetailEdit(order: order)
}
}
}
}
}
import SwiftUI
struct DetailEdit: View {
#State var tableNumber = ""
#ObservedObject var order: Order
#Environment(\.managedObjectContext) private var viewContext
#Environment (\.presentationMode) var presentationMode
#State var selectedFlavor = Flavor.chocolate
enum Flavor: String, CaseIterable, Identifiable {
case chocolate
case vanilla
case strawberry
var id: String { self.rawValue }
}
var body: some View {
NavigationView {
Form {
//1- this work fine
TextField("table number", text: $tableNumber)
//2- this doesnt get updated
Picker("Flavor", selection: $selectedFlavor) {
Text("Chocolate").tag(Flavor.chocolate)
Text("Vanilla").tag(Flavor.vanilla)
Text("Strawberry").tag(Flavor.strawberry)
}
Text("Selected flavor: \(selectedFlavor.rawValue)")
}
//fetch data from core data -> item edit
.onAppear {
//1- this work fine
self.tableNumber = self.order.tableNumber
//2- this doesnt get updated
self.selectedFlavor.rawValue = self.order.flavor
}
.navigationTitle("Edit Order")
}
}
Try this:
.onChange(of: selectedFlavor) {
self.order.flavor = $0.rawValue
}
.onAppear {
self.tableNumber = self.order.tableNumber
self.selectedFlavor = Flavor(rawValue: self.order.flavor)
}

DispatchQueue.main.async does not return to the main thread - SwiftUI

struct ContentView: View {
#State var manager = HtpAuth()
var body: some View {
if manager.authenticated {
Text("Login Successful!!")
}
// 2 textfields for username and password and
// a login button to call checkForAuth function
//...
}
}
class HttpAuth: ObservableObject {
var didChange = PassthroughSubject<HttpAuth, Never>()
var authenticated = false {
didSet {
didChange.send(self)
}
}
func checkForAuth(username: String, password: String) {
//REST API call
URLSession.shared.data.task(with: loginRequest) { data, response, error in
guard let data = data else { return }
let finalData = try! JSONDecoder().decode(ServerMessage.self, from: data)
DispatchQueue.main.async {
self.authenticated = true
}
}.resume()
}
}
I am getting response from the server. But after recieving the resposne, I want to return to the main thread to display some other view. I am using DispatchQueue.main.async for this purpose, but it does not work. The label "Login Successfull!!" never appears, after successful login.
Use instead standard Published and ObservedObject wrappers as below
struct ContentView: View {
#ObservedObject var manager = HtpAuth() // use #StateObject in SwiftUI 2
...
class HttpAuth: ObservableObject {
#Published var authenticated = false
...

How to return a PartialView from Core 2 RazorPage ViewModel Handler

In Asp.Net MVC, you can easily return a partial view by doing the following:
return PartialView("ModelName", Model);
How is this done on a RazorPage ViewModel Handler?
I figured this out. It is not nearly as straight forward as it is in MVC. You have to create an empty ViewDataDictionary() and then set its Model property to the partial's populated model.
View Model / Handler
public async Task<IActionResult> OnGetAsyncUpdateSearchResults(DateTime startDate, DateTime endDate, string selectedTypes)
{
int[] types = selectedTypes.Split(",").Select(x => int.Parse(x)).ToArray();
var inventory = await _itemService.GetFiltered(types, null, null, null, null, null, null, startDate, endDate.ToUniversalTime(), null, null, null, null, null, null, null);
if (inventory != null)
{
SearchResultsGridPartialModel = new SearchResultsGridPartialModel();
SearchResultsGridPartialModel.TotalCount = inventory.TotalCount;
SearchResultsGridPartialModel.TotalPages = inventory.TotalPages;
SearchResultsGridPartialModel.PageNumber = inventory.PageNumber;
SearchResultsGridPartialModel.Items = inventory.Items;
}
var myViewData = new ViewDataDictionary(new Microsoft.AspNetCore.Mvc.ModelBinding.EmptyModelMetadataProvider(), new Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary()) { { "SearchResultsGridPartialModel", SearchResultsGridPartialModel } };
myViewData.Model = SearchResultsGridPartialModel;
PartialViewResult result = new PartialViewResult()
{
ViewName = "SearchResultsGridPartial",
ViewData = myViewData,
};
return result;
}
I can now call this handler via ajax GET and have it return the partial's HTML. I can then set the partial's div and the partial refreshes as expected.
Here is the AJAX call I'm making:
var jsonData = { "startDate": startDate, "endDate": endDate, "selectedTypes": selectedTypesAsString };
$.ajax({
type: 'GET',
url: "searchresults/?handler=AsyncUpdateSearchResults",
beforeSend: function (xhr) {
xhr.setRequestHeader("XSRF-TOKEN", $('input:hidden[name="__RequestVerificationToken"]').val());
},
contentType: 'application/json; charset=utf-8"',
data: jsonData,
success: function (result) {
$("#searchResultsGrid").html(result);
},
error: function (error) {
console.log(error);
}
});
Thanks alot to TechFisher for figuring it out, here is a bit cleaner example.
public IActionResult OnGetTestPartial()
{
return new PartialViewResult()
{
ViewName = "Test",
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = new TestPartialData { Data = "inputhere" },
}
};
}
Partial view in a file name "Test.cshtml" in the same folder as the above class.
#using YourNamespace
#model TestPartialData
<div>Hello, model value: #Model.Data</div>
Load it async with jquery
$("#someHtmlElementId").load("Your/Path/TestPartial");

Merging json text into single dto

is there a mechanism in servicestack.text to merge two json strings into a single dto?
The use case is merging complex settings from multiple sources into a single settings file
i.e.
{ "blah": { "params": { "default": "bar", "misc": "0", } } }
and
{ "blah": { "params": { "value": "val", "misc": "1", } } }
becomes
{ "blah": { "params": { "default": "bar", "value": "val", "misc": "1", } } }
Thanks
Be careful of the trailing comma's as it's not valid JSON. But you can use the dynamic API of ServiceStack's JSON Serializer to do this:
var json1 = "{\"blah\":{\"params\":{\"default\":\"bar\", \"misc\": \"0\" } } }";
var json2 = "{\"blah\":{\"params\":{\"value\":\"val\", \"misc\": \"1\" } } }";
var jsonObj = JsonObject.Parse(json1);
var jsonParams =jsonObj.Object("blah").Object("params");
foreach (var entry in JsonObject.Parse(json2).Object("blah").Object("params"))
{
jsonParams[entry.Key] = entry.Value;
}
var to = new { blah = new { #params = jsonParams } };
to.ToJson().Print();
Which will output:
{"blah":{"params":{"default":"bar","misc":"1","value":"val"}}}
Well, if you don't ever going to use JsonArrays, solution above could be written in recursive way:
public static JsonObject Merge(JsonObject #this, JsonObject that) {
foreach (var entry in that) {
var exists = #this.ContainsKey (entry.Key);
if (exists) {
var otherThis = JsonObject.Parse(#this.GetUnescaped (entry.Key));
var otherThat = JsonObject.Parse(that.GetUnescaped (entry.Key));
#this [entry.Key] = Merge (otherThis, otherThat).ToJson ();
} else {
#this [entry.Key] = entry.Value;
}
}
return #this;
}

Resources