0

I have converted an Angular project into a hybrid app following this guide: https://medium.com/@christof.thalmann/convert-angular-project-to-android-apk-in-10-steps-c49e2fddd29

For Android I did not run into many issues and the app is working as expected on that platform.

On IOS I ran into multiple difficulties. First of all in order contents display I needed to change the Angular LocationStrategy to HashLocation as describe in this SO topic:

Why Angular Router 8 router ...

Although I now do get content to render I still have trouble getting the dynamic content (i.e. the content requiring a call to a web server before rendering) to render properly.

My app has got a classic NavBar to switch from one component to another. If I activate one component by clicking on a Nav Button the static content gets rendered ok. However, the dynamic content is not there. I can verify this by looking at the source code: The holding the dynamic content is empty. If I click on the same Nav Button again the dynamic content is added. I get the same effect on an iPhone simulator and on a real device.

This is the html of one of the components

<div class="card bg-opaque border-light">
    <div class="card-body">
        <div>
            <h2 class="text-light text-opaque">Einsparungen von {{ user }}</h2>
        </div>

        <div *ngIf="monthScore" class="card border-primary bg-opaque-light"> <!-- THIS BLOCK NOT VISIBLE -->
            <div class="card-body text-center">
                <h3 class="card-title">Current Month</h3>
                <ul class="list-unstyled">
                    <li>
                        <strong>Savings:</strong> {{ monthScore.savings | number: '1.1-2' }} kg
                    </li>
                    <li>
                        <strong>Position:</strong> {{ monthScore.rank }}
                        <span *ngIf="!monthScore.rank">???</span>
                        <span *ngIf="monthScore.rank == 1">
                            <mat-icon class="text-warning">emoji_events</mat-icon>
                        </span>
                    </li>
                    <li>
                        <strong>Captured:</strong> {{ monthScore.captured | number: '1.1-2' }} kg
                    </li>
                </ul>
                <div *ngIf="showMonthButton" >
                    <button class="btn btn-outline-primary" (click)="toggleMonthGraph()">
                        <mat-icon>bar_chart</mat-icon>
                    </button>
                </div>
                <div *ngIf="!showMonthButton" (click)="toggleMonthGraph()">
                    <canvas
                        baseChart
                        [chartType]="'bar'"
                        [datasets]="monthChartData"
                        [labels]="monthChartLabels"
                        [options]="chartOptions"
                        [legend]="false">
                    </canvas>
                </div>
            </div>
        </div>

        <div *ngIf="yearScore" class="card border-primary bg-opaque-light"> <!-- THIS BLOCK NOT VISIBLE -->                <div class="card-body text-center">
                <h3 class="card-title">Current year</h3>
                <ul class="list-unstyled">
                    <li>
                        <strong>Savings:</strong> {{ yearScore.savings | number: '1.1-2' }} kg
                    </li> 
                    <li>
                        <strong>Position:</strong> {{ yearScore.rank }}
                        <span *ngIf="!yearScore.rank">???</span>
                        <span *ngIf="yearScore.rank == 1">
                            <mat-icon class="text-warning">emoji_events</mat-icon>
                        </span>
                    </li>
                    <li>
                        <strong>Captured:</strong> {{ yearScore.captured | number: '1.1-2' }} kg
                    </li>
                </ul>
                <div *ngIf="showYearButton" >
                    <button class="btn btn-outline-primary" (click)="toggleYearGraph()">
                        <mat-icon>bar_chart</mat-icon>
                    </button>
                </div>
                <div *ngIf="!showYearButton" (click)="toggleYearGraph()">
                    <canvas
                        baseChart
                        [chartType]="'bar'"
                        [datasets]="yearChartData"
                        [labels]="yearChartLabels"
                        [options]="chartOptions"
                        [legend]="false">
                    </canvas>
                </div>
            </div>
        </div>
    </div>
</div>

<app-inpage></app-inpage>

The .ts file:

import { Component, OnInit } from '@angular/core';

import { SummaryService} from '../summary.service';
import { AuthService } from '../../auth/auth.service';
import { Score } from '../summary';
import { MAT_RIPPLE_GLOBAL_OPTIONS } from '@angular/material/core';

@Component({
  selector: 'app-score',
  templateUrl: './score.component.html',
  styleUrls: ['./score.component.scss']
})
export class ScoreComponent implements OnInit {

  monthScore: Score;
  yearScore: Score;
  user: string;

  // Histogramm per Consumer
  chartOptions = {
    responsive: true,
    scales: {
      xAxes: [{
          gridLines: {
              drawOnChartArea: false
          }
      }],
      yAxes: [{
          gridLines: {
              drawOnChartArea: false
          }
      }]
  }
  };

  yearChartData = [];
  yearChartLabels = [];
  yearChartTitle: string;
  showYearChart: boolean = false;
  showYearButton: boolean = true;

  monthChartData = [];
  monthChartLabels = [];
  monthChartTitle: string;
  showMonthChart: boolean = false;
  showMonthButton: boolean = true;

  constructor(private service: SummaryService, private authService: AuthService) { }

  ngOnInit(): void {

    this.user = this.authService.user

    this.getMonthScore();
    this.getYearScore();
  }

  getMonthScore(): void {
    this.service.getScore('month').subscribe(score => {
      this.monthScore = score;
      this.createMonthGraph();
    })
  }

  getYearScore(): void {
    console.log('GETTING SCORE')
    this.service.getScore('year').subscribe(score => {
      this.yearScore = score;
      this.createYearGraph();
    })
  }

  private createYearGraph(): void {
    this.service.getTimeline('year').subscribe(timelines => {
      let data: number[] = [];
      let label: string[] = [];
      for (let i = 0; i < timelines.length; i++){
        data.push(timelines[i].user_savings);
        label.push(timelines[i].period.toString());
      }

      this.yearChartData = [{data: data, label: 'Savings', barThickness: 2, backgroundColor: 'rgba(0, 0, 0, 0.5' }]
      this.yearChartLabels = label

    })
  }

  private createMonthGraph(): void {
    this.service.getTimeline('month').subscribe(timelines => {
      let data: number[] = [];
      let label: string[] = [];
      for (let i = 0; i < timelines.length; i++){
        data.push(timelines[i].user_savings);
        label.push(timelines[i].period.toString());
      }

      this.monthChartData = [{data: data, label: 'Savings', barThickness: 2, backgroundColor: 'rgba(0, 0, 0, 0.5' }]
      this.monthChartLabels = label

    })
  }

  toggleYearGraph(): void {
    this.showYearChart = !this.showYearChart;
    this.showYearButton = !this.showYearButton;
  }

  toggleMonthGraph(): void {
    this.showMonthButton = !this.showMonthButton;
  }
}

My config.xml

<?xml version='1.0' encoding='utf-8'?>
<widget id="com.ticumwelt.co2" version="0.2.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
    <name>Tracker</name>
    <description>
        An app to track your savings.
    </description>
    <author email="mymail@example.com" href="https://example.com">
        Developer Team
    </author>
    <content src="index.html" />
    <access origin="*" />
    <allow-intent href="http://*/*" />
    <allow-intent href="https://*/*" />
    <allow-intent href="tel:*" />
    <allow-intent href="sms:*" />
    <allow-intent href="mailto:*" />
    <allow-intent href="geo:*" />
    <platform name="android">
        <allow-intent href="market:*" />
    </platform>
    <platform name="ios">
        <allow-intent href="itms:*" />
        <allow-intent href="itms-apps:*" />
        <!-- iOS 8.0+ -->
        <!-- iPhone 6 Plus  -->
        <icon src="res/ios/icons/icon-60@3x.png" width="180" height="180" />
        <!-- iOS 7.0+ -->
        <!-- iPhone / iPod Touch  -->
        <icon src="res/ios/icons/icon-60.png" width="60" height="60" />
        <icon src="res/ios/icons/icon-60@2x.png" width="120" height="120" />
        <!-- iPad -->
        <icon src="res/ios/icons/icon-76.png" width="76" height="76" />
        <icon src="res/ios/icons/icon-76@2x.png" width="152" height="152" />
        <!-- Spotlight Icon -->
        <icon src="res/ios/icons/icon-40.png" width="40" height="40" />
        <icon src="res/ios/icons/icon-40@2x.png" width="80" height="80" />
        <!-- iOS 6.1 -->
        <!-- iPhone / iPod Touch -->
        <icon src="res/ios/icons/icon.png" width="57" height="57" />
        <icon src="res/ios/icons/icon@2x.png" width="114" height="114" />
        <!-- iPad -->
        <icon src="res/ios/icons/icon-72.png" width="72" height="72" />
        <icon src="res/ios/icons/icon-72@2x.png" width="144" height="144" />
        <!-- iPad Pro -->
        <icon src="res/ios/icons/icon-167.png" width="167" height="167" />
        <!-- iPhone Spotlight and Settings Icon -->
        <icon src="res/ios/icons/icon-small.png" width="29" height="29" />
        <icon src="res/ios/icons/icon-small@2x.png" width="58" height="58" />
        <icon src="res/ios/icons/icon-small@3x.png" width="87" height="87" />
        <!-- iPad Spotlight and Settings Icon -->
        <icon src="res/ios/icons/icon-50.png" width="50" height="50" />
        <icon src="res/ios/icons/icon-50@2x.png" width="100" height="100" />
        <!-- iPad Pro -->
        <icon src="res/ios/icons/icon-83.5@2x.png" width="167" height="167" />
        
        <splash src="res/ios/screen/default@2x~universal~anyany.png" />

        <preference name="WKWebViewOnly" value="true" />

        <feature name="CDVWKWebViewEngine">
            <param name="ios-package" value="CDVWKWebViewEngine" />
        </feature>

        <preference name="CordovaWebViewEngine" value="CDVWKWebViewEngine" />
        <preference name="WKSuspendInBackground" value="false" />
    </platform>
    <edit-config target="NSLocationWhenInUseUsageDescription" file="*-Info.plist" mode="merge">
        <string>need location access to find things nearby</string>
    </edit-config>
</widget>

And my package.json

{
  "name": "com.example.tracker",
  "displayName": "Tracker",
  "version": "0.2.1",
  "description": "An app to track your savings",
  "main": "index.js",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "keywords": [
    "ecosystem:cordova"
  ],
  "author": "My Name",
  "license": "Apache-2.0",
  "private": true,
  "dependencies": {
    "@angular-material-components/datetime-picker": "^2.0.4",
    "@angular/animations": "~9.0.3",
    "@angular/cdk": "^9.2.4",
    "@angular/common": "~9.0.3",
    "@angular/compiler": "~9.0.3",
    "@angular/core": "~9.0.3",
    "@angular/forms": "~9.0.3",
    "@angular/localize": "~9.0.3",
    "@angular/material": "^9.2.4",
    "@angular/platform-browser": "~9.0.3",
    "@angular/platform-browser-dynamic": "~9.0.3",
    "@angular/router": "~9.0.3",
    "@ionic-native/background-geolocation": "^5.29.0",
    "@ionic-native/core": "^5.29.0",
    "@ionic/angular": "^5.4.1",
    "@ng-bootstrap/ng-bootstrap": "^6.2.0",
    "bootstrap": "^4.4.0",
    "chart.js": "^2.9.4",
    "cordova-plugin-splashscreen": "6.0.0",
    "material-design-icons-iconfont": "^6.1.0",
    "ng-connection-service": "^1.0.4",
    "ng2-charts": "^2.4.2",
    "ngx-cookie-service": "^10.1.1",
    "rxjs": "~6.5.4",
    "tslib": "^1.10.0",
    "zone.js": "~0.10.2"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^0.1002.0",
    "@angular/cli": "~9.0.4",
    "@angular/compiler-cli": "~9.0.3",
    "@angular/language-service": "~9.0.3",
    "@globules-io/cordova-plugin-ios-xhr": "^1.2.0",
    "@mauron85/cordova-plugin-background-geolocation": "^3.1.0",
    "@types/jasmine": "~3.5.0",
    "@types/jasminewd2": "~2.0.3",
    "@types/node": "^12.11.1",
    "codelyzer": "^5.1.2",
    "cordova-ios": "^6.1.1",
    "cordova-plugin-geolocation": "^4.1.0",
    "cordova-plugin-whitelist": "^1.3.4",
    "jasmine-core": "~3.5.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~4.3.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage-istanbul-reporter": "~2.1.0",
    "karma-jasmine": "~2.0.1",
    "karma-jasmine-html-reporter": "^1.4.2",
    "protractor": "~5.4.3",
    "ts-node": "~8.3.0",
    "tslint": "~5.18.0",
    "typescript": "~3.7.5"
  },
  "cordova": {
    "plugins": {
      "cordova-plugin-whitelist": {},
      "cordova-plugin-geolocation": {
        "GPS_REQUIRED": "true"
      },
      "cordova-plugin-background-geolocation": {
        "ALWAYS_USAGE_DESCRIPTION": "This app always requires location tracking",
        "MOTION_USAGE_DESCRIPTION": "This app requires motion detection"
      },
      "@globules-io/cordova-plugin-ios-xhr": {}
    },
    "platforms": [
      "ios"
    ]
  }
}

My suspicion is that it has something to do with the WKWebView of Apple. As it is the first time I am actually developing something in the Apple World I have the feeling that some Apple security feature is blocking something.

Update:

I did 2 additional checks:

  1. In order to check if any styling stuff was causing the issue I removed all the stylings. However, same problem.

  2. In order to check if the dynamic data is actually fetched from the server when initiating the component I added a console.log() to print the data after it is fetched. It is fetched correctly but the screen does not update to display the data.

Update 2:

Updating from Angular 9 to Angular 10 also did not solve the problem.

Chris
  • 2,162
  • 1
  • 6
  • 17
  • Could you be running into a cors issue? If you connect the remote debugging tools do you get any network or console errors? –  Jan 05 '21 at 22:18
  • No, no CORS issues anymore. I had them but they were resolved by the ios-xhr plugin. – Chris Jan 06 '21 at 08:01

1 Answers1

1

After a lot of trial and error and searching I found the solution.

I found a hint here:

https://github.com/angular/angular/issues/7381[1]

For a reason I fo not fully understand yet the app seems to switch zones during the async call to the server. Therefore the UI change mechanism is not triggered and the screen is not updated.

By wrapping the changes of variables into NgZone.run()the screen is updated correctly.

The updated .ts file

import { Component, OnInit, NgZone } from '@angular/core';

// ...

constructor(private service: SummaryService, private authService: AuthService, private zone: NgZone) { }

// ...

getMonthScore(): void {
    this.service.getScore('month').subscribe(score => {
      this.zone.run(() => {
        this.monthScore = score;
        this.createMonthGraph();
        console.log('GOT MONTH SCORE');
        console.log(score);
      });
      
    })
  }

This is required only when building an iOS app with Cordova. When building an Android app or using the browser does not seem necessary.

Chris
  • 2,162
  • 1
  • 6
  • 17