As a quickie demo, I’ve added this project to by GitHub – Ionic and Swift Hybrid Demo iOS Xcode Project

I’ve published this becuase it is a concise demo of the following scenario:

  • You have an iOS app written in Swift
  • You want to specify one view in the otherwise vanilla Swift + Xcode storyboard app to be controlled by an Ionic TypeScript + Angular app
  • You want to be able to freely navigate between the swift view(s) and the Ionic v4-based view (becuse of couse you’d want navigation to work within the app and appear seamless)

I couldn’t find any existing guides covering this particular scenario but its a pretty compelling use-case for teams wanting to create mobile apps that leverage a hybrid webview in an existing native codebase.

It uses Ionic Capacitor to leverage some UINavigation native API hooks to be called within the Ionic Angular App

Codable screenshot

I’ve shipped my first major-ish update to Codable – now at 1.1.X

Improvements:

  • Minor UI cleanup with header texts looking cleaner, and support for dynamic type where appropriate
  • Added “home” button on browser to more easily return back to your default page
  • Updated the available list of viewport device-size presets in settings
  • Improved the CSS editing view

Codable I feel is a good app, albeit utilitarian. Despite me basically never promoting it outside my own website, it has thousands of users, and I hope its been a help to those trying to do some debugging / QA on small screens.

Update: Ignore the below; you’d be crazy to not just use React.Suspense

Recently I added code splitting and dynamic imports to a React + TypeScript + WebPack project using React Loadable. However, it took a few hours to work though getting the JS chunks to properly split rather than get bundled together into a single chunk.

React Loadable looks like it requires minimal configuration to work, and TypeScript claims to have out-of-the-box support for dynamic imports. Perhaps with Create React App this is the case, but in existing larger codebases that use WebPack, I had to make some changes, which I’ll highlight below the pasted code.

Stack used here:

  • React 16
  • TypeScript 3
  • WebPack 4

React TSX Class:

import * as React from 'react';
import * as Loadable from 'react-loadable';
import { Icon, message, Spin } from 'antd';
...

interface HasDynamicImportProps {
    propThing: type
}

interface HasDynamicImportState {
  loadableComponent: type
}

export class HasDynamicImport extends React.Component<HasDynamicImportProps, HasDynamicImportState> {

  constructor(props) {
    super(props);
    this.state = {
      loadableComponent: null,
    };
  }
  
  private readonly ANT_ICON = <Icon type='loading' style= spin/>;

  public async componentDidUpdate(previousProps, previousState) {
    /* this component responds to inputs but that is omitted for simplicity */
    this.setAsyncComponent();
  }

  public setAsyncComponent() {
    const loadableInstanceComponent = Loadable.Map({
      loader: {
        AsyncComponentImport: () => {
          return import( /* webpackChunkName: "ComponentWrapper" */ '../components/AsyncComponent');
        },
      },
      loading: () => {
        return null;
      },
      render(loaded, props: any) {
        const Component = loaded.AsyncComponentImport.AsyncComponent;
        const propThing = props.propThing;
        return <Component
          propThing={propThing}
        />;
      },
    });
    this.setState({ loadableComponent: loadableInstanceComponent });
  }

  public render() {
    if (!this.state.loadableComponent) {
      return (
        <div className='flex h-full w-full justify-center'>
          <Spin indicator={this.ANT_ICON} className='mt-8 mb-8'/>
        </div>);
    }
    return (
      <section id='container'>
        <article className='w-screen'>
          <this.state.loadableComponent
            propThing={this.props.propThing}
          />
        </article>
      </section>
    );
  }
}

Highlights from above:

  • Note how this.state.loadableComponent is used to house the loadable component, rather than declaring it as a const outside the class. If declared outside the class as shown in other docs, it exists in memory when you don’t want it to.
  • My example above intentionally does not load a Loading...component but you can do that. The Map is also being used to transfer props. See React Loadable docs.
  • Note how I am importing things, by referencing the exports. TypeScript imports modules a little differently than your typical CRA JS.
  • This handles the passing of internal class values via props, where the props are passed in first through the render. This guards against passing in undefined props on load
  • The inline comment /* webpackChunkName: "ComponentWrapper" */ has a purpose: It tells webpack to code-split this under this chunk name

But what if its compiling fine, but your return import( /* webpackChunkName: "ComponentWrapper" */ '../components/AsyncComponent'); is getting compiled right into the main bundle, and isn’t being split out as it should be?

You should see something like

                                main.0cf7b02de965f7714959.package.js   X KiB                    main  [emitted]  main
              vendor~ComponentWrapper.0cf7b02de965f7714959.package.js  X MiB  vendor~ComponentWrapper  [emitted]  vendor~ComponentWrapper
                         vendor~main.0cf7b02de965f7714959.package.js  X MiB             vendor~main  [emitted]  vendor~main
                                                          index.html  1.81 KiB                          [emitted]  

What if you don’t? What if you’re only seeing main or a single vendor with main? The webpack comment should show up in the list here as a chunk, as it does above.

If you aren’t seeing the above, keep reading…

First, verify your webpack config:

`webpack.web.config.js’

const baseConfig = {/* your other bits */
const plugins = [
  new HtmlWebpackPlugin({
    template: projectRoot + '/src/index.html',
  }),
];
const babelLoader = {
  loader: 'babel-loader',
  options: {
    cacheDirectory: true,
    presets: [['@babel/preset-env']],
    comments: true,
    compact: false,
    plugins: ['@babel/plugin-syntax-dynamic-import']
  },
};
module.exports = merge.smart(baseConfig, {
  entry: {
    main: './src/main.tsx'
  },
  module: {
      rules: [
        {
          test: /\.tsx?$/,
          exclude: /node_modules/,
          use: [babelLoader, { loader: 'ts-loader' }],
        },
  },
  output: {
    path: projectRoot + '/dist',
    filename: '[name].[hash].package.js',
    chunkFilename: "[name].[hash].package.js",
    publicPath: '/',
  },
    optimization: {
        splitChunks: {
            cacheGroups: {
                default: false,
                vendors: false,
                vendor: {
                    chunks: 'all',
                    test: /node_modules/
                }
            }
        }
    },
  plugins,
});

Notes on above:

  • babelLoader has the @babel/plugin-syntax-dynamic-import plugin
  • Some of the optimizations may not be needed in your case, but this webpack config enables the WebPack 4 almost-automatic code splitting to take place. For me, this also splits out fonts automatically.
  • HtmlWebpackPlugin is set to automatically append the relevant JS chunks onto index.html with this config

Second, is your tsconfig correct? It should be using esnext

tsconfig.json

{
  "compileOnSave": false,
  "compilerOptions": {
    "jsx": "react",
    "sourceMap": true,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "target": "es5",
    "module" : "esnext",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "ES2017",
      "DOM",
      "DOM.Iterable"
    ]
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
      "**/*.history"
  ]
}

If this is set to commonjs instead of esnext, dynamic imports will not work and will just get compiled into one of the other chunks. Be warned that esnext is a little experiemental at the time of this writing, and it may interfere with some of your other imports but I was able to work through those just by toggling my import or require syntax as needed in other files.

I want to highlight my terribly-named recent GitHub project “TypeScript-Docker-Node-DI-Starter”, which is meant to combine a stack I’ve become familiar with, but where each component requires some labor to start fresh with.

These are:

  • Docker
  • MySQL in a docker container
  • Node/express, with a persisted connection to be above
  • TypeScript as the languge for the node project, compiling via npm scripts
  • An ORM for MySQL entities
  • OAuth tables, a user table, and fully functional Oauth login flow
  • Dependency Injection
  • A suite of npm scripts and other minor depedenices in the package.json, for convenience

The OAuth implementation is clean, and relies on no existing “All in one” dependencies (just bcrypt and token generators)

I hope this can help someone, just from a standpoint of delivering a boilerplate that no single tutorial online assembles.

View on GitHub

After a few months of evening work and a lot of experimentation, I’ve released an iOS app.

Codable screenshot

Codable provides three primary features:

  • resizable viewport with scaling, allowing you to view any URL as if you were on a smaller or larger iOS screen size
  • JavaScript console support
  • Rendered DOM node navigation and HTML/CSS manipulation

By my knowledge, there are two other functional apps on the iOS app store that do these same functions. It’s a pretty niche use-case to want to do front-end web debugging and editing from mobile I know, but it’s something I’ve wanted to do many times.

Codable leverages one API endpoint quite heavily and uniquely: WKWebView.evaluateJavaScipt()

If you click through on that link, you’ll see that it isn’t too helpful. There is no APIs for grabbing browser errors, the console, the pre-rendered source, or the rendered DOM. So, how do you grab it? Via JavaScript, from the above API. There is pretty much a constant stream of JS dom-query scripts being applied to the browser, and a JSON-ified node list script response is then captured coming out.

Go to Codable on the App Store