SwiftUI: Do I need to remove UIHostingController from the controller chain when I remove the controlled view from the view hierarchy? - uiviewcontroller

I am trying to migrate UIKit Views in my app to SwiftUI. One of the central elements in my app is a UICollectionView. I am embedding the SwiftUI views using a UIHostingController - so far so good.
I am wondering, since my cells are reusable, what happens to the UIHostingController when the cell is recycled?
Do I need to take it out of the controller chain?
If I need to, what is the best way to do so? (storing the UIHostingController in the cell?)
Eg. a header view looks like this so far:
class HeaderViewCell: UICollectionReusableView {
var layoutAttributes:GroupHeaderViewLayoutAttributes = GroupHeaderViewLayoutAttributes()
public func attachContent(model:GroupHeaderModel, controller:UIViewController){
let view = GroupHeaderView(model: model, layoutAttributes: self.layoutAttributes)
let hostingController = UIHostingController(rootView: view)
if let contentView = hostingController.view {
controller.addChild(hostingController)
self.addSubviewAndConstrains(contentView)
}
}
override func prepareForReuse() {
self.subviews.forEach { $0.removeFromSuperview() }
layoutAttributes = GroupHeaderViewLayoutAttributes()
}
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
if let attributes = layoutAttributes as? TileViewLayout.HeaderViewAttributes {
self.layoutAttributes.topBarHeight = attributes.topBarHeight
self.layoutAttributes.indicatorWidth = attributes.indicatorWidth
}
}
}

Related

Can we use reflection to instantiate custom TypeScript model from a JSON string?

Is it possible to clone a JSON-generated object or string into a Typescript class which I created? We are building a model of our API using Typescript classes. There’s a base class which they all extend which has common/helper methods. When we do JSON.parse(response) to auto-generate objects it creates simple objects and not our custom objects.
Is there a way we can convert those JSON-generated objects into our custom objects, so long as the field names match up? And, to make things more robust, can this but done where our custom objects’ fields are other custom objects and/or arrays of them?
Here is our code, with comments of what we’d like to achieve.
base-model.ts
export class BaseModelObject {
uuid: string; // All of our objects in our model and JSON have this required field populated
matchUUIDs<T extends BaseModelObject>( obj: T): boolean {
return obj.uuid == this.uuid;
}
}
child-model.ts
import { BaseModelObject } from 'base-model';
export class Child extends BaseModelObject {
}
parent-model.ts
import { BaseModelObject } from 'base-model';
import { Child } from 'child-model';
export class Parent extends BaseModelObject {
children: Child[];
}
JSON payload
{
'uuid': '0632a35c-e7dd-40a8-b5f4-f571a8359c1a',
'children': [
{
'uuid': 'd738c408-4ae9-430d-a64d-ba3f085175fc'
},
{
'uuid': '44d56a0d-ad2d-4e85-b5d1-da4371fc0e5f'
}
]
}
In our components and directives and such, we hope to use the helper function in BaseModelObject:
Component code
let parent: Parent = JSON.parse(response);
console.log(parent.uuid); // Works! 0632a35c-e7dd-40a8-b5f4-f571a8359c1a
// Want this to print ‘true’, but instead we get TypeError: parebt.matchUUID is not a function
console.log(parent.matchUUID(‘0632a35c-e7dd-40a8-b5f4-f571a8359c1a’));
// Want this to print ‘true’, but instead we get TypeError: parent.children[0].matchUUID is not a function
console.log(parent.children[0].matchUUID(‘d738c408-4ae9-430d-a64d-ba3f085175fc’));
The problem is that JSON.parse() is not creating our classes, it’s creating simple objects with key/value pairs. So we’re thinking of “cloning” the JSON-generated object into an instance of our class, like this:
base-model.ts
export class BaseModelObject {
[key: string]: any;
matchUUIDs<T extends BaseModelObject>( obj: T): boolean {
return obj['uuid'] == this['uuid'];
}
cloneFields(obj: any) {
for (let prop in obj) {
this[prop] = obj[prop];
}
}
}
Component code
let parent: Parent = new Parent(); // Creates instance of our class
parent.cloneFields(JSON.parse(response)); // Copy JSON fields to our object
console.log(parent.matchUUID('0632a35c-e7dd-40a8-b5f4-f571a8359c1a')); // prints 'true'
console.log(parent.children[0].matchUUID('d738c408-4ae9-430d-a64d-ba3f085175fc')); // Still throws TypeError: parent.children[0].matchUUID is not a function
The problem now rests in the fact that the cloning of the Parent object did not recursively clone the JSON-generated Child objects into instances of our custom Child class.
Since our Parent object is typed at compile-time and it knows that the data type of the children array is Child[] (our custom class), is there a way to use reflection to instantiate the right class?
Our logic would need to say:
Create an instance of our custom class
Tell our instance to clone the fields from the JSON-generated object
Iterate over the fields in the JSON-generated object
For each field name from the JSON-generated object, find the "type definition" in our custom class
If the type definition is not a primitive or native Typescript type, then instantiate a new instance of that "type" and then clone it's fields.
(and it would need to recursively traverse the whole JSON object structure to match up all other custom classes/objects we add to our model).
So something like:
cloneFields(obj: any) {
for (let prop in obj) {
let A: any = ...find the data type of prop...
if(...A is a primitive type ...) {
this[prop] = obj[prop];
} else {
// Yes, I know this code won't compile.
// Just trying to illustrate how to instantiate
let B: <T extends BaseModelUtil> = ...instantiate an instance of A...
B.cloneFields(prop);
A[prop] = B;
}
}
}
Is it possible to reflect a data type from a class variable definition and then instantiate it at runtime?
Or if I'm going down an ugly rabbit hole to which you know a different solution, I'd love to hear it. We simply want to build our custom objects from a JSON payload without needing to hand-code the same patterns over and over since we expect our model to grow into dozens of objects and hundreds of fields.
Thanks in advance!
Michael
There are several ways to do that, but some requires more work and maintenance than others.
1. Simple, a lot of work
Make your cloneFields abstract and implement it in each class.
export abstract class BaseModelObject {
uuid: string;
matchUUIDs<T extends BaseModelObject>( obj: T): boolean {
return obj.uuid == this.uuid;
}
abstract cloneFields(obj: any);
}
class Parent extends BaseModelObject {
children: Child[];
cloneFields(obj: any) {
this.children = obj.children?.map(child => {
const c = new Children();
c.cloneFields(child);
return c;
});
}
}
2. Simple, hacky way
If there is no polymorphism like:
class Parent extends BaseModelObject {
children: Child[] = [ new Child(), new ChildOfChild(), new SomeOtherChild() ]
}
Property names mapped to types.
const Map = {
children: Child,
parent: Parent,
default: BaseModelObject
}
export class BaseModelObject {
uuid: string;
matchUUIDs<T extends BaseModelObject>( obj: T): boolean {
return obj.uuid == this.uuid;
}
cloneFields(obj: any) {
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
this[prop] = obj[prop]?.map(child => { // You have to check it is an array or not,..
const c = new (Map[prop])();
c.cloneFields(child);
return c;
});
}
}
}
}
You can serialize hints into that JSON. Eg. property type with source/target type name and use it to resolve right types.
3. Reflection
Try tst-reflect. It is pretty advanced Reflection system for TypeScript (using custom typescript transformer plugin).
I'm not going to write example, it would be too complex and it depends on your needs.
You can use tst-reflect to list type's properties and get their types. So you'll be able to validace parsed data too.
Just some showcase from its README:
import { getType } from "tst-reflect";
function printTypeProperties<TType>()
{
const type = getType<TType>(); // <<== get type of generic TType ;)
console.log(type.getProperties().map(prop => prop.name + ": " + prop.type.name).join("\n"));
}
interface SomeType {
foo: string;
bar: number;
baz: Date;
}
printTypeProperties<SomeType>();
// or direct
getType<SomeType>().getProperties();
EDIT:
I created a package ng-custom-transformers that simplifies this a lot. Follow its README.
DEMO
EDIT old:
Usage with Angular
Angular has no direct support of custom transformers/plugins. There is a feature request in the Angular Github repo.
But there is a workaround.
You have to add ngx-build-plus. Run ng add ngx-build-plus.
That package defines "plugins".
Plugins allow you to provide some custom code that modifies your webpack configuration.
So you can create plugin and extend Angular's webpack configuration. But here comes the sun problem. There is no public way to add the transformer. There were AngularCompilerPlugin in webpack configuration (#ngtools/webpack) which had private _transformers property. It was possible to add a transformer into that array property. But AngularCompilerPlugin has been replaced by AngularWebpackPlugin which has no such property. But is is possible to override method of AngularWebpackPlugin and add a transformer there. Getting an instance of the AngularWebpackPlugin is possible thanks to ngx-build-plus's plugins.
Code of the plugin
const {AngularWebpackPlugin} = require("#ngtools/webpack");
const tstReflectTransform = require("tst-reflect-transformer").default;
module.exports.default = {
pre() {},
post() {},
config(cfg) {
// Find the AngularWebpackPlugin in the webpack configuration; angular > 12
const angularWebpackPlugin = cfg.plugins.find((plugin) => plugin instanceof AngularWebpackPlugin);
if (!angularWebpackPlugin) {
console.error("Could not inject the typescript transformer: AngularWebpackPlugin not found");
return;
}
addTransformerToAngularWebpackPlugin(angularWebpackPlugin, transformer);
return cfg;
},
};
function transformer(builderProgram) {
return tstReflectTransform(builderProgram.getProgram());
}
function addTransformerToAngularWebpackPlugin(plugin, transformer) {
const originalCreateFileEmitter = plugin.createFileEmitter; // private method
plugin.createFileEmitter = function (programBuilder, transformers, getExtraDependencies, onAfterEmit, ...rest) {
if (!transformers) {
transformers = {};
}
if (!transformers.before) {
transformers = {before: []};
}
transformers.before = [transformer(programBuilder), ...transformers.before];
return originalCreateFileEmitter.apply(plugin, [programBuilder, transformers, getExtraDependencies, onAfterEmit, ...rest]);
};
}
Then it is required to execute ng commands (such as serve or build) with --plugin path/to/the/plugin.js.
I've made working StackBlitz demo.
Resources I've used while preparing the Angular demo:
https://indepth.dev/posts/1045/having-fun-with-angular-and-typescript-transformers
https://medium.com/#morrys/custom-typescript-transformers-with-angular-for-angular-11-12-and-13-40cbdc9cca7b

UIViewController lifecycle broken iOS13, makeKeyAndVisible() seems not to operate?

I have a custom UIStoryboardSegue that works as desired in iOS12.*.
One of the destination view controller is a UITabbarController: for each tab, I have a controller embedded in a navigation controller.
Unfortunately, for iOS13.*, this does not work well: the view controller lifecycle is broken, and no call the viewXXXAppear() nor the willTransition() methods are no longer issued.
It looks like makeKeyAndVisible() has no effect?!
See at the bottom how the screen UI is puzzled below without viewWillAppear() being called.
An horrible temporary workaround
I had to pull my hairs but, I have found a fix which I make public (I had to add a navigation controller on the fly).
This messes the vc hierarchy: do you have a better solution?
public class AladdinReplaceRootViewControllerSegue: UIStoryboardSegue {
override public func perform() {
guard let window = UIApplication.shared.delegate?.window as? UIWindow,
let sourceView = source.view,
let destinationView = destination.view else {
super.perform()
return
}
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
destinationView.frame = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight)
window.insertSubview(destinationView, aboveSubview: sourceView)
// **My fix**
if #available(iOS 13,*) {
// I introduced an invisible navigation controller starting in iOS13 otherwise, my controller attached to the tabbar thru a navigation, dont work correctly, no viewXAppearis called.
let navigationController = UINavigationController.init(rootViewController: self.destination)
navigationController.isNavigationBarHidden = true
window.rootViewController = navigationController
}
else {
window.rootViewController = self.destination
}
window.makeKeyAndVisible()
}
}
I found a solution thanks to Unbalanced calls to begin/end appearance transitions with custom segue
What happens here is that the creation and attaching of the destination view controller happens twice, and the first one happens too soon.
So what you need to do is:
public class AladdinReplaceRootViewControllerSegue: UIStoryboardSegue {
override public func perform() {
guard let window = UIApplication.shared.delegate?.window as? UIWindow,
let sourceView = source.view,
let destinationView = destination.view else {
super.perform()
return
}
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
let mock = createMockView(view: desination.view)
window.insertSubview(mock, aboveSubview: sourceView)
//DO SOME ANIMATION HERE< MIGHT NEED TO DO mock.alpha = 0
//after the animation is done:
window.rootViewController = self.destination
mock.removeFromSuperview()
}
func createMockView(view: UIView) -> UIImageView {
UIGraphicsBeginImageContextWithOptions(view.frame.size, true, UIScreen.main.scale)
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIImageView(image: image)
}
}
I had a similar problem on iOS 13 when performing a custom storyboard segue that replaces the rootViewController. The original code looked like this:
#interface CustomSegue : UIStoryboardSegue
#end
#implementation CustomSegue
- (void)perform {
AppDelegate* appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
UIViewController *destination = (UIViewController *) self.destinationViewController;
[destination.view removeFromSuperview];
[appDelegate.window addSubview:destination.view];
appDelegate.window.rootViewController = destination;
}
#end
Removing the line [appDelegate.window addSubview:destination]; fixed the problem to me. Apparanently, it was unnecessary to add the new VC's view as a subview to the window. It did the job correctly even after removing that line, and it also fixed the error message "unbalanced calls to begin/end appearance transitions".

What Is The Best Way to Implement Entity-Component System

Recently I am planing to implement a Entity-Component-System like Overwatch.
The major challenge(and difficulty) in my project is that, my engine allows user defined customized map, in which user was allowed to define customized unit. In another word, user can select which component they need for an entity type
For example
type Component interface {
ComponentName() string
}
type HealthComponent struct {
HP uint
}
type ManaComponent struct {
MP uint
}
type ChiComponent struct{
Chi uint
}
// assuming each component has implemented Component interface
The corresponding Entity definition is:
type Entity struct {
ID EID
EntityName string
Components map[string]Component
}
A user will have some entity definition in JSON format:
{
"EntityName": "FootMan",
"Components": {
"HealthComponent": {
"HP": 500
}
}
}
---------------------------------------------
{
"EntityName": "Warlock",
"Components": {
"HealthComponent": {
"HP": 250
},
"ManaComponent": {
"MP": 100
}
}
}
---------------------------------------------
{
"EntityName": "Monk",
"Components": {
"HealthComponent": {
"HP": 250
},
"ChiComponent": {
"Chi": 100
}
}
}
Please notice that ID is not included in JSON because we need to initialize it at run-time
So here comes the problem:
What is the most efficient way to build such an entity with a given JSON definition?
Currently my solution is using a registry to maintain a map between struct type and component name
var componentRegistry = make(map[string]reflect.Type)
func init() {
componentRegistry["ChiComponent"] = reflect.TypeOf(ChiComponent{})
componentRegistry["HealthComponent"] = reflect.TypeOf(HealthComponent{})
componentRegistry["ManaComponent"] = reflect.TypeOf(ManaComponent{})
}
The builder code is
func ComponentBuilder(name string) Component {
v := reflect.New(componentRegistry[name])
fmt.Println(v)
return v.Interface().(Component)
}
func EntityBuilder(EntityName string, RequireComponent []string) *Entity {
var entity = new(Entity)
entity.ID = getNextAvailableID()
entity.EntityName = EntityName
entity.Components = make(map[string]Component)
for _, v := range RequireComponent {
entity.Components[v] = ComponentBuilder(v)
}
return entity
}
For each system that want to access a component in this entity needs to do following:
var d = monk_entity.Components["ChiComponent"].(*ChiComponent)
d.Chi = 13
var h = monk_entity.Components["HealthComponent"].(*HealthComponent)
h.HP = 313
It works, but I am using too much reflection in this approach and I am not able to assign initial value to entity, which was store in user defined JSON file. Is there any better way to do so?
Gor one thing, you can just use functions instead of reflection:
type componentMaker func() Component // Define generator signature
var componentRegistry = make(map[string]componentMaker) // Update map type
componentRegistry["ChiComponent"] = func() Component { return new(ChiComponent) } // Define generators
entity.Components[v] = componentRegistry[v]() // Call generator
And so on. No reflection needed.

Json to typescript object . Inheritance

I have three classes
class Device{
name:string;
}
class Mobile extends Device{
number:string;
}
class Computer extends Device{
macAddress:string;
}
and json
[{
'name':'mobile1',
'number':'600 600 600',
'class':'Mobile'
},{
'name':'computer',
'macAddress:'123123123',
'class':'Computer'
}]
is it possible using some kind of decorators/or anything else to get List of devices with correct object types.
I'm producting Json at my site so i can also add another fields, change structure to make typescript object list generate corectly
I was searching for any solution without success.
Regards,
Adrian
I would suggest the following implementation. Please note the comments inside. It might contain some errors because I cannot actually check the code, so maybe some more work is necessary.
Basic idea: To make code simple in your components, you should wrap your array into an object, here called JsonDevices. Then you can write a custom converter and let the magic happen inside the converter.
Classes
// Custom serializer/deserializer.
// You must implement serialize and deserialize methods
#JsonConverter
class DeviceConverter implements JsonCustomConvert<Device> {
// We receive the instance and just serialize it with the standard json2typescript method.
serialize(device: Device): any {
const jsonConvert: JsonConvert = new JsonConvert();
return jsonConvert.serialize(device);
}
// We receive a json object (not string) and decide
// based on the given properties whether we want to
// create an instance of Computer or Mobile.
deserialize(device: any): Device {
const jsonConvert: JsonConvert = new JsonConvert();
// We need the try/catch because of deserialize inside
try {
if (device.name && device.macAddress) {
const computer: Computer = new Computer();
computer.name = device.name;
computer.macAddress = device.macAddress;
return jsonConvert.deserialize(computer, Computer);
} else if (device.name && device.number)
const mobile: Mobile = new Mobile();
mobile.name = device.name;
mobile.number = device.number;
return jsonConvert.deserialize(mobile, Mobile);
}
} catch(e) {}
throw new TypeError();
}
}
#JsonObject
class JsonDevices {
#JsonProperty("devices", DeviceConverter)
devices: Device[] = [];
}
#JsonObject
class Device {
#JsonProperty("name", String)
name: string = undefined;
}
#JsonObject
class Mobile extends Device {
#JsonProperty("number", String)
number: string = undefined;
}
#JsonObject
class Computer extends Device {
#JsonProperty("macAddress", String)
macAddress: string = undefined;
}
Usage
// Assume this is your incoming json
const jsonString: string = ("
[{
'name':'mobile1',
'number':'600 600 600',
'class':'Mobile'
},{
'name':'computer',
'macAddress:'123123123',
'class':'Computer'
}]
");
// Convert it to an JSON/JavaScript object
// In the current Angular 4 HttpClientModule API,
// you would get an object directly and you wouldn't
// bother with it anyway.
const jsonArray: any[] = JSON.parse(jsonString);
// Make sure the given array is added to an object
// having a device array
const jsonDevicesObject: any = {
devices: jsonArray;
}
// Now deserialize the whole thing with json2typescript:
const jsonConvert: JsonConvert = new JsonConvert();
const jsonDevices: JsonDevices = jsonConvert.deserialize(jsonDevicesObject, JsonDevices);
// Now all elements of jsonDevices.devices are of instance Mobile or Computer
I've based my solution on what andreas wrote thank You.
To achieve proper solution i've used aswesome library
json2typescript
. My Solution:
import {JsonObject, JsonProperty, JsonConverter, JsonCustomConvert, JsonConvert} from "json2typescript";
#JsonConverter
class DeviceConverter implements JsonCustomConvert<DeviceDto[]> {
// We receive the instance and just serialize it with the standard json2typescript method.
serialize(device: DeviceDto[]): any {
const jsonConvert: JsonConvert = new JsonConvert();
return jsonConvert.serialize(device);
}
// We receive a json object (not string) and decide
// based on the given properties whether we want to
// create an instance of Computer or Mobile.
deserialize(devicesInput: any): DeviceDto[] {
const jsonConvert: JsonConvert = new JsonConvert();
let devices: Array<DeviceDto> = new Array<DeviceDto>();
for (let device of devicesInput) {
if (device['type'] == 'mobile') {
let temp:MobileDeviceDto=jsonConvert.deserialize(device, MobileDeviceDto)
devices.push(temp);
} else if (device['type'] == 'rpi') {
devices.push(jsonConvert.deserialize(device, RaspberryPiDeviceDto));
}
}
return devices;
}
}
#JsonObject
export class DevicesDto {
#JsonProperty("devices", DeviceConverter)
devices: DeviceDto[] = [];
}
#JsonObject
export class DeviceDto {
#JsonProperty("name", String)
name: string= undefined;
#JsonProperty("description", String)
description: string= undefined;
#JsonProperty("type", String)
type: string= undefined;
}
#JsonObject
export class MobileDeviceDto extends DeviceDto {
#JsonProperty("number", String)
number: string = undefined;
}
#JsonObject
export class RaspberryPiDeviceDto extends DeviceDto {
#JsonProperty("version", String)
version: string = undefined;
}
Usage:
let jsonConvert: JsonConvert = new JsonConvert();
let devices: DeviceDto[] = jsonConvert.deserialize(data, DevicesDto).devices;
this.subjectDeviceList.next(data);
Thank You very much :)

Perfect JSON structure

I've been looking around and learning JSON a little bit. I thought it would be good to start learning with something easy but it seems it is not. I am trying to do JSON database. For example it has brand names and every brand has its own products with some info. I've done that like this which is actually much longer:
{
"Snuses": {
"Brands": {
"CATCH": [
{
"Products": "CATCH EUCALYPTUS WHITE LARGE",
"nicotine": "8.0"
}
]
}
Now I am using Firebase to parse the "Brands" like "CATCH" etc.. But I can't.
In swift I am trying to do it like this:
override func viewDidLoad() {
super.viewDidLoad()
ref = FIRDatabase.database().reference()
ref.observeSingleEventOfType(.Value, withBlock: { snapshot in
self.ref = FIRDatabase.database().reference().child("Snuses").child("Brands")
self.ref.observeEventType(.Value, withBlock: { snapshot -> Void in
for brands in snapshot.children {
print(brands)
}
})
})
}
How to get reference to the Brands first? And how to store list of brands separately?
Some smart guys told me that it is not correct to do but I don't know what is wrong with the JSON structure. How can I flatten it?
I red the docs also that says how it is best to do it but it is a little to complicaetd. Can you point me to the right direction?
You just need to do allKeys to get allKeys from snap
let ref = FIRDatabase.database().reference().child("Snuses").child("Brands")
ref.observeSingleEventOfType(.Value, withBlock: { (snapshot) in
if snapshot.exists() {
if let allProducts = (snapshot.value?.allKeys)! as? [String]{
self.snusBrandsArray = allProducts
self.productstable.reloadData()
}
}
})