Ionic(Angular)でAdmobを導入する方法(iOS編)

Ionic(Angular)でAdmobを導入する方法です。こちらはiOSでの実装となります。Androidでの実装方法はこちら。

ポイント

  • capacitor-admobプラグインを使用します
  • 公式の方法+Plugin.swiftの編集が必要です(この記事に方法あります)

流れ

  1. npmでcapacitor-admobをインストールし、info.plistを編集します
  2. そのままだとエラーが起こるので、Plugin.swiftを修正します
  3. app.component.tsでアプリ情報を初期化してから読み込みたいモジュールで広告呼び出します

動作環境

Ionic4, 5, 6でiOS13から14.3で確認しています。

参考文献

https://github.com/rahadur/capacitor-admob

https://developers.google.com/admob/android/test-ads?hl=ja

初期状態

IonicのTabsのデフォルトテンプレートから始めます。

作成方法はこちら。


見た目はこんな感じ。

appフォルダ以下の構成は下記のようになっています。

.
├── app-routing.module.ts
├── app.component.html
├── app.component.scss
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── explore-container
│   ├── explore-container.component.html
│   ├── explore-container.component.scss
│   ├── explore-container.component.spec.ts
│   ├── explore-container.component.ts
│   └── explore-container.module.ts
├── tab1
│   ├── tab1-routing.module.ts
│   ├── tab1.module.ts
│   ├── tab1.page.html
│   ├── tab1.page.scss
│   ├── tab1.page.spec.ts
│   └── tab1.page.ts
├── tab2
│   ├── tab2-routing.module.ts
│   ├── tab2.module.ts
│   ├── tab2.page.html
│   ├── tab2.page.scss
│   ├── tab2.page.spec.ts
│   └── tab2.page.ts
├── tab3
│   ├── tab3-routing.module.ts
│   ├── tab3.module.ts
│   ├── tab3.page.html
│   ├── tab3.page.scss
│   ├── tab3.page.spec.ts
│   └── tab3.page.ts
└── tabs
├── tabs-routing.module.ts
├── tabs.module.ts
├── tabs.page.html
├── tabs.page.scss
├── tabs.page.spec.ts
└── tabs.page.ts

実装する

1. ライブラリをインストールする

npmでcapacitor-admobをインストールします。

 npm install --save capacitor-admob

2. iOSのプロジェクトを生成する

iOSのプロジェクトを生成するために下記コマンドを打ちます。capacitorを入れていない方はcapacitorを入れてください。

ionic build
npx cap add ios
npx cap sync
npx cap open ios

3. info.plistを編集する

info.plistとは、iOSアプリの設定情報などを記入するものです。インフォプロパティリストと読みます。下記コマンドでXcodeを開きます。

npx cap open ios

info.plistを選択します。

このファイルを編集するのですが、編集するにはコツが入ります、info.plistの上で右クリックをして、open as > source codeを選択して編集できるようにします。

そして、下記のスニペットを下の方に貼り付けます。自分のアプリIDに変更するのを忘れないでください。

    <key>GADIsAdManagerApp</key>
    <true/>
    <key>GADApplicationIdentifier</key>
    <!-- replace this value with your App ID key-->
    <string>ca-app-pub-7171076285090910~XXXXXXXX</string>

もし場所がわからない方がいたらinfo.plistを丸ごと貼り付けるので参考にしてください。下から3行目から8行目までが貼り付けたものです。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>en</string>
	<key>CFBundleDisplayName</key>
        <string>admob-master</string>
	<key>CFBundleExecutable</key>
	<string>$(EXECUTABLE_NAME)</string>
	<key>CFBundleIdentifier</key>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>$(PRODUCT_NAME)</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>1.0</string>
	<key>CFBundleURLTypes</key>
	<array>
		<dict>
			<key>CFBundleURLName</key>
			<string>com.getcapacitor.capacitor</string>
			<key>CFBundleURLSchemes</key>
			<array>
				<string>capacitor</string>
			</array>
		</dict>
	</array>
	<key>CFBundleVersion</key>
	<string>1</string>
	<key>LSRequiresIPhoneOS</key>
	<true/>
	<key>NSAppTransportSecurity</key>
	<dict>
		<key>NSAllowsArbitraryLoads</key>
		<true/>
	</dict>
	<key>NSCameraUsageDescription</key>
	<string>To Take Photos and Video</string>
	<key>NSLocationAlwaysUsageDescription</key>
	<string>Always allow Geolocation?</string>
	<key>NSLocationWhenInUseUsageDescription</key>
	<string>Allow Geolocation?</string>
	<key>NSMicrophoneUsageDescription</key>
	<string>To Record Audio With Video</string>
	<key>NSPhotoLibraryAddUsageDescription</key>
	<string>Store camera photos to camera</string>
	<key>NSPhotoLibraryUsageDescription</key>
	<string>To Pick Photos from Library</string>
	<key>UILaunchStoryboardName</key>
	<string>LaunchScreen</string>
	<key>UIMainStoryboardFile</key>
	<string>Main</string>
	<key>UIRequiredDeviceCapabilities</key>
	<array>
		<string>armv7</string>
	</array>
	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UISupportedInterfaceOrientations~ipad</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationPortraitUpsideDown</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UIViewControllerBasedStatusBarAppearance</key>
	<true/>
    
    <key>GADIsAdManagerApp</key>
    <true/>
    <key>GADApplicationIdentifier</key>
    <!-- replace this value with your App ID key-->
    <string>ca-app-pub-7171076285090910~3362455272</string>
    
</dict>
</plist>

5. 公式にはないエラー処理のコードを書く

公式ではこれで完了ということですが、これだとエラーが起きます。これでエミュレータを起動すると下記のエラーが出ます。

右上を見ると赤い背景で9というエラーが表示されています。その右下の別の赤いボタンからエラーが起きている「Plugin.swift」へ移動します。

そうするとそれぞれのエラーの詳細が確認できます。

よく見るとどれも同じメッセージのようです。こう書いてあります、

Method 'interstitialDidReceiveAd' must be declared public because it matches a requirement in public protocol 'GADInterstitialDelegate'

publicで宣言せよと書いてあるのでそうします。

修正前

    private func interstitialDidReceiveAd(_ ad: GADInterstitial) {
        print("interstitialDidReceiveAd")
        self.notifyListeners("onAdLoaded", data: ["value": true])
    }

修正後

    public func interstitialDidReceiveAd(_ ad: GADInterstitial) {
        print("interstitialDidReceiveAd")
        self.notifyListeners("onAdLoaded", data: ["value": true])
    }

合計9箇所同じように変えると、ビルドできるようになるはずです。

広告を表示する

以上で広告を表示するためのプラグインの設定が終わったので、いよいよ広告をアプリで表示していきます。

まず、app.component.tsでアプリケーションを初期化します。

import { Plugins } from "@capacitor/core";
// import { initialize } from 'capacitor-admob'; No longar required

const { AdMob } = Plugins;

@Component({
  selector: "app-root",
  templateUrl: "app.component.html",
  styleUrls: ["app.component.scss"]
})
export class AppComponent {
  constructor() {
    // Initialize AdMob for your Application
    AdMob.initialize("YOUR APPID");
  }
}

次に、読み込みたいページで任意の広告を呼び出します。ここでは、tab1でバナー広告、tab2でインタースティシャル広告を呼び出します。

テストIDを使ってください。

https://developers.google.com/admob/android/test-ads?hl=ja

tab1でバナー広告を呼び出す

import { Component } from '@angular/core';
import { Plugins } from "@capacitor/core";
import { AdOptions, AdSize, AdPosition } from "capacitor-admob";

const { AdMob } = Plugins;
@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
  options: AdOptions = {
    adId: "ca-app-pub-3940256099942544/6300978111",
    adSize: AdSize.BANNER,
    position: AdPosition.BOTTOM_CENTER
  };

  constructor() {
    // Show Banner Ad
    AdMob.showBanner(this.options).then(
      value => {
        console.log(value); // true
      },
      error => {
        console.error(error); // show error
      }
    );

    // Subscibe Banner Event Listener
    AdMob.addListener("onAdLoaded", (info: boolean) => {
      console.log("Banner Ad Loaded");
    });
  }

}

tab2でインタースティシャルを表示する

import { Component } from '@angular/core';
import { Plugins } from "@capacitor/core";
import { AdOptions } from "capacitor-admob";
const { AdMob } = Plugins;

@Component({
  selector: 'app-tab2',
  templateUrl: 'tab2.page.html',
  styleUrls: ['tab2.page.scss']
})
export class Tab2Page {
  options: AdOptions = {
    adId: "ca-app-pub-3940256099942544/1033173712",
    hasTabBar: false, // make it true if you have TabBar Layout.
    tabBarHeight: 56 // you can assign custom margin in pixel default is 56
  };

  constructor() {
    // Prepare interstitial banner
    AdMob.prepareInterstitial(this.options).then(
      value => {
        console.log(value); // true
      },
      error => {
        console.error(error); // show error
      }
    );

    // Subscibe Banner Event Listener
    AdMob.addListener("onAdLoaded", (info: boolean) => {
      // You can call showInterstitial() here or anytime you want.
      console.log("Interstitial Ad Loaded");
      // Show interstitial ad when it’s ready

      AdMob.showInterstitial().then(
        value => {
          console.log(value); // true
        },
        error => {
          console.error(error); // show error
        }
      );

    });
  }
}

時間を空ける

Admobの仕様で広告が表示されるまでに時間が必要です。

設定が正しくされていても最初は

  • Ad failed to load : 2
  • Ad failed to load : 0

などが表示されて広告が表示されません。これは時間をおけば解決なので少し忍耐が必要です。

その他何かうまくいかないときは下記を実行することをお勧めします。

  • ionic build –prodでプロダクションモードでビルドする
  • npx cap sync ios
  • npx cap copy

終わりに

あー記事まとめるのめっちゃ大変だった!お疲れ様でした。

Ionic(Angular)でAdmobを導入する方法(Android編)

Ionic(Angular)でAdmobを導入する方法です。こちらはAndroidでの実装となります。iOS版はこちら。

ポイント

  • capacitor-admobプラグインを使用します
  • 公式の方法+依存関係の解決(この記事に方法あります)が必要です

流れ

  1. npmでcapacitor-admobをインストールします
  2. そのままだとエラーが起こるので、依存関係を解決します
  3. エミュレータで正しくアプリが立ち上がり依存関係が解決されていることを確認したのち、app.component.tsでアプリ情報を初期化してから読み込みたいモジュールで広告呼び出します

動作環境

Ionic4, 5, 6でAndroid SDKはAPIレベル29(Android X)で検証。28以下でもいけました。

参考文献

https://github.com/rahadur/capacitor-admob

https://developers.google.com/admob/android/test-ads?hl=ja

初期状態

IonicのTabsのデフォルトテンプレートから始めます。

appフォルダ以下の構成は下記のようになっています。

.
├── app-routing.module.ts
├── app.component.html
├── app.component.scss
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── explore-container
│   ├── explore-container.component.html
│   ├── explore-container.component.scss
│   ├── explore-container.component.spec.ts
│   ├── explore-container.component.ts
│   └── explore-container.module.ts
├── tab1
│   ├── tab1-routing.module.ts
│   ├── tab1.module.ts
│   ├── tab1.page.html
│   ├── tab1.page.scss
│   ├── tab1.page.spec.ts
│   └── tab1.page.ts
├── tab2
│   ├── tab2-routing.module.ts
│   ├── tab2.module.ts
│   ├── tab2.page.html
│   ├── tab2.page.scss
│   ├── tab2.page.spec.ts
│   └── tab2.page.ts
├── tab3
│   ├── tab3-routing.module.ts
│   ├── tab3.module.ts
│   ├── tab3.page.html
│   ├── tab3.page.scss
│   ├── tab3.page.spec.ts
│   └── tab3.page.ts
└── tabs
├── tabs-routing.module.ts
├── tabs.module.ts
├── tabs.page.html
├── tabs.page.scss
├── tabs.page.spec.ts
└── tabs.page.ts

実装する

1. ライブラリをインストールする

npmでcapacitor-admobをインストールします。

 npm install --save capacitor-admob

2. Androidのプロジェクトを生成する

Androidのプロジェクトを生成するために下記コマンドを打ちます。capacitorを入れていない方はcapacitorを入れてください。

ionic build
npx cap add android
npx cap sync
npx cap open android

3. AndroidManifest.xmlを編集する

Androidのマニフェストファイルはアプリに機能を追加するときに編集するものです。Android Studioではデフォルトで作成されるものとなります。

ここからどのファイルを編集するか分かりにくいので詳細に記載します。非常にここは苦労しました。

デフォルトでは下記のようにAndroidが選択されていると思います。

ドリルダウンを選択してProjectを選択します

この状態にまずしてください。

マニフェストファイルはProjectのandroid/app/src/main/AndroidManifest.xmlにあります。

自分のAdmobのアプリIDに編集する必要がありますが、下記のように変更します。

<application>
  <!-- this line needs to be added (replace the value!) -->
         <meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-7171076285090910~XXXXXXXX" />
  <activity></activity>
</application>

つまり、編集した後のファイルは下記のようになります。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="io.ionic.starter">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-7171076285090910~XXXXXXXXX" />

        <activity
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
            android:name="io.ionic.starter.MainActivity"
            android:label="@string/title_activity_main"
            android:theme="@style/AppTheme.NoActionBarLaunch"
            android:launchMode="singleTask">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="@string/custom_url_scheme" />
            </intent-filter>

        </activity>

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"></meta-data>
        </provider>
    </application>

    <!-- Permissions -->

    <uses-permission android:name="android.permission.INTERNET" />
    <!-- Camera, Photos, input file -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!-- Geolocation API -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-feature android:name="android.hardware.location.gps" />
    <!-- Network API -->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <!-- Navigator.getUserMedia -->
    <!-- Video -->
    <uses-permission android:name="android.permission.CAMERA" />
    <!-- Audio -->
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
</manifest>

4. MainActivity.javaを編集する

admobをandroidに登録します。

Projectを選択した状態で、Android>app>src>main>java><your original name>>MainActivityを選択します。

そして、このように変更します。

// Other imports...
import app.xplatform.capacitor.plugins.AdMob;

public class MainActivity extends BridgeActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    this.init(savedInstanceState, new ArrayList<Class<? extends Plugin>>() {{

      add(AdMob.class);  // Add AdMob as a Capacitor Plugin

    }});
  }
}

つまり、このようにします。

package io.ionic.starter;

import android.os.Bundle;

import com.getcapacitor.BridgeActivity;
import com.getcapacitor.Plugin;
import java.util.ArrayList;

import app.xplatform.capacitor.plugins.AdMob;

public class MainActivity extends BridgeActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Initializes the Bridge
    this.init(savedInstanceState, new ArrayList<Class<? extends Plugin>>() {{
      // Additional plugins you've installed go here
      // Ex: add(TotallyAwesomePlugin.class);
      add(AdMob.class);  // Add AdMob as a Capacitor Plugin

    }});
  }
}

5. 公式にはないエラー処理のコードを書く

公式ではこれで完了ということですが、これだとエラーが起きます。これでエミュレータを起動すると下記のエラーが出ます。

/Users/masaya/Desktop/MyGoodness/admob-master/admob-master/node_modules/capacitor-admob/android/src/main/java/app/xplatform/capacitor/plugins/AdMob.java:4: エラー: パッケージandroid.support.design.widgetは存在しません
import android.support.design.widget.CoordinatorLayout;

そこで、焦らずに下記の対応を行います。

https://github.com/rahadur/capacitor-admob/issues/35

Admob.javaの編集

まず、エラーが出ているAdmob.javaのファイルのエラー発生している行を下記に変更します。

import androidx.coordinatorlayout.widget.CoordinatorLayout;

前のものはコメントアウトでもしておいてこのようにします。

そして、capacito-admobのbuild.gradleのdependenciesの中にimplementation ‘androidx.coordinatorlayout:coordinatorlayout:1.1.0’を追加します。

implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'

編集するファイルを間違えるとドツボにハマるので注意。

これでサイドエミュレータを立ち上げると下記のエラーが出ます。

2020-12-21 15:44:03.866 9236-9236/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: io.ionic.starter, PID: 9236
    java.lang.RuntimeException: Unable to start activity ComponentInfo{io.ionic.starter/io.ionic.starter.MainActivity}: android.view.InflateException: Binary XML file line #2 in io.ionic.starter:layout/bridge_layout_main: Binary XML file line #2 in io.ionic.starter:layout/bridge_layout_main: Error inflating class android.support.design.widget.CoordinatorLayout
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3270)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
     Caused by: android.view.InflateException: Binary XML file line #2 in io.ionic.starter:layout/bridge_layout_main: Binary XML file line #2 in io.ionic.starter:layout/bridge_layout_main: Error inflating class android.support.design.widget.CoordinatorLayout
     Caused by: android.view.InflateException: Binary XML file line #2 in io.ionic.starter:layout/bridge_layout_main: Error inflating class android.support.design.widget.CoordinatorLayout
     Caused by: java.lang.ClassNotFoundException: android.support.design.widget.CoordinatorLayout
        at java.lang.Class.classForName(Native Method)
        at java.lang.Class.forName(Class.java:454)
        at android.view.LayoutInflater.createView(LayoutInflater.java:815)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1006)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:961)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:659)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:534)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:481)
        at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:555)
        at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:161)
        at com.getcapacitor.BridgeActivity.init(BridgeActivity.java:60)
        at com.getcapacitor.BridgeActivity.init(BridgeActivity.java:48)
        at io.ionic.starter.MainActivity.onCreate(MainActivity.java:17)
        at android.app.Activity.performCreate(Activity.java:7802)
        at android.app.Activity.performCreate(Activity.java:7791)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1299)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3245)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
2020-12-21 15:44:03.866 9236-9236/? E/AndroidRuntime: Caused by: java.lang.ClassNotFoundException: Didn't find class "android.support.design.widget.CoordinatorLayout" on path: DexPathList[[zip file "/data/app/io.ionic.starter-wIOzWpenwsyjIPlSvEtd-g==/base.apk"],nativeLibraryDirectories=[/data/app/io.ionic.starter-wIOzWpenwsyjIPlSvEtd-g==/lib/x86, /system/lib, /system/product/lib]]
        at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:196)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
        	... 28 more

そこで、この辺を解決してくれるように下記のライブラリを導入します。

terminalで下記を打ちます。

npm install jetifier
npx jetify
npx cap sync android

これで再度エミュレータを起動すると、ようやく無事に立ち上がるようになります。これで依存関係が無事に解決されました。

広告を表示する

以上で広告を表示するためのプラグインの設定が終わったので、いよいよ広告をアプリで表示していきます。

まず、app.component.tsでアプリケーションを初期化します。

import { Plugins } from "@capacitor/core";
// import { initialize } from 'capacitor-admob'; No longar required

const { AdMob } = Plugins;

@Component({
  selector: "app-root",
  templateUrl: "app.component.html",
  styleUrls: ["app.component.scss"]
})
export class AppComponent {
  constructor() {
    // Initialize AdMob for your Application
    AdMob.initialize("YOUR APPID");
  }
}

次に、読み込みたいページで任意の広告を呼び出します。ここでは、tab1でバナー広告、tab2でインタースティシャル広告を呼び出します。

テストIDを使ってください。

https://developers.google.com/admob/android/test-ads?hl=ja

tab1でバナー広告を呼び出す

import { Component } from '@angular/core';
import { Plugins } from "@capacitor/core";
import { AdOptions, AdSize, AdPosition } from "capacitor-admob";

const { AdMob } = Plugins;
@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
  options: AdOptions = {
    adId: "ca-app-pub-3940256099942544/6300978111",
    adSize: AdSize.BANNER,
    position: AdPosition.BOTTOM_CENTER
  };

  constructor() {
    // Show Banner Ad
    AdMob.showBanner(this.options).then(
      value => {
        console.log(value); // true
      },
      error => {
        console.error(error); // show error
      }
    );

    // Subscibe Banner Event Listener
    AdMob.addListener("onAdLoaded", (info: boolean) => {
      console.log("Banner Ad Loaded");
    });
  }

}

tab2でインタースティシャルを表示する

import { Component } from '@angular/core';
import { Plugins } from "@capacitor/core";
import { AdOptions } from "capacitor-admob";
const { AdMob } = Plugins;

@Component({
  selector: 'app-tab2',
  templateUrl: 'tab2.page.html',
  styleUrls: ['tab2.page.scss']
})
export class Tab2Page {
  options: AdOptions = {
    adId: "ca-app-pub-3940256099942544/1033173712",
    hasTabBar: false, // make it true if you have TabBar Layout.
    tabBarHeight: 56 // you can assign custom margin in pixel default is 56
  };

  constructor() {
    // Prepare interstitial banner
    AdMob.prepareInterstitial(this.options).then(
      value => {
        console.log(value); // true
      },
      error => {
        console.error(error); // show error
      }
    );

    // Subscibe Banner Event Listener
    AdMob.addListener("onAdLoaded", (info: boolean) => {
      // You can call showInterstitial() here or anytime you want.
      console.log("Interstitial Ad Loaded");
      // Show interstitial ad when it’s ready

      AdMob.showInterstitial().then(
        value => {
          console.log(value); // true
        },
        error => {
          console.error(error); // show error
        }
      );

    });
  }
}

時間を空ける

Admobの仕様で広告が表示されるまでに時間が必要です。

設定が正しくされていても最初は

  • Ad failed to load : 2
  • Ad failed to load : 0

などが表示されて広告が表示されません。これは時間をおけば解決なので少し忍耐が必要です。

その他何かうまくいかないときは下記を実行することをお勧めします。

  • ionic build –prodでプロダクションモードでビルドする
  • npx cap sync android
  • npx cap copy

終わりに

あー記事まとめるのめっちゃ大変だった。お疲れ様でした。

Angularで画像をトリミングする方法

Angularで読み込んだ画像をトリミングできるようにする方法です。

ポイント

  • ngx-image-cropperを使います

流れ

  1. ngx-image-cropperをnpmでinstallします
  2. 読み込みたい場所でngx-image-cropperをmoduleに登録します
  3. その場所のHTMLとTSファイルを編集します

環境

Ionic 4, 5, 6で動作検証しています。

ソースコード

GitHubに上げました。

https://github.com/NP-Systems/Ionic_tips/tree/id1137-imageCropper

参考文献

https://www.npmjs.com/package/ngx-image-cropper

初期状態

IonicのTabsのデフォルトテンプレートから始めます。

appフォルダ以下の構成は下記のようになっています。

.
├── app-routing.module.ts
├── app.component.html
├── app.component.scss
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── explore-container
│   ├── explore-container.component.html
│   ├── explore-container.component.scss
│   ├── explore-container.component.spec.ts
│   ├── explore-container.component.ts
│   └── explore-container.module.ts
├── tab1
│   ├── tab1-routing.module.ts
│   ├── tab1.module.ts
│   ├── tab1.page.html
│   ├── tab1.page.scss
│   ├── tab1.page.spec.ts
│   └── tab1.page.ts
├── tab2
│   ├── tab2-routing.module.ts
│   ├── tab2.module.ts
│   ├── tab2.page.html
│   ├── tab2.page.scss
│   ├── tab2.page.spec.ts
│   └── tab2.page.ts
├── tab3
│   ├── tab3-routing.module.ts
│   ├── tab3.module.ts
│   ├── tab3.page.html
│   ├── tab3.page.scss
│   ├── tab3.page.spec.ts
│   └── tab3.page.ts
└── tabs
├── tabs-routing.module.ts
├── tabs.module.ts
├── tabs.page.html
├── tabs.page.scss
├── tabs.page.spec.ts
└── tabs.page.ts

実装方法

それではTab1ページに実装していきます。

インストール

プロジェクトのフォルダに移動して下記コマンドでインストールします。

npm install ngx-image-cropper --save

モジュールへの登録

今回はtab1に実装することにするので、tab1.module.tsに下記のようにします。

import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Tab1Page } from './tab1.page';
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
import { ImageCropperModule } from 'ngx-image-cropper';

import { Tab1PageRoutingModule } from './tab1-routing.module';

@NgModule({
  imports: [
    IonicModule,
    CommonModule,
    FormsModule,
    ExploreContainerComponentModule,
    Tab1PageRoutingModule,
    ImageCropperModule
  ],
  declarations: [Tab1Page]
})
export class Tab1PageModule {}

import { ImageCropperModule } from ‘ngx-image-cropper’;を追加して、

@NgModuleのimportsにImageCropperModuleを追加します。

HTMLとTSファイルの編集

上記で準備は完了なので、HTMLとTSファイルの編集を行います。

tab1.page.htmlに下記のように追加します、

<input type="file" (change)="fileChangeEvent($event)" />

<image-cropper
    [imageChangedEvent]="imageChangedEvent"
    [maintainAspectRatio]="true"
    [aspectRatio]="4 / 3"
    format="png"
    (imageCropped)="imageCropped($event)"
    (imageLoaded)="imageLoaded()"
    (cropperReady)="cropperReady()"
    (loadImageFailed)="loadImageFailed()"
></image-cropper>

<img [src]="croppedImage" />

tab1.page.tsを下記のようにします。

import { Component } from '@angular/core';
import { ImageCroppedEvent } from 'ngx-image-cropper';

@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
    imageChangedEvent: any = '';
    croppedImage: any = '';

    constructor(
    ) {}

    fileChangeEvent(event: any): void {
        this.imageChangedEvent = event;
    }
    imageCropped(event: ImageCroppedEvent) {
        this.croppedImage = event.base64;
    }
    imageLoaded(image: HTMLImageElement) {
        // show cropper
    }
    cropperReady() {
        // cropper ready
    }
    loadImageFailed() {
        // show message
    }

}

応用編

以上が基礎ですが、ここからは切り取った画像を新たに別の画像をとして読み込む方法について記載します。

まず、HTMLに切り取った後に押下するためのボタンを配置します。

tab1.page.htmlの当該部分を下記のように変更します。

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Tab 1</ion-title>
    </ion-toolbar>
  </ion-header>

  <ion-item>
    <input type="file" accept='image/*'  (change)="fileChangeEvent($event)" />
  </ion-item>

  <ion-progress-bar type="indeterminate" *ngIf="show" reversed="true"></ion-progress-bar>
  <!--プログレスバーの追加です-->

  <image-cropper
  [imageChangedEvent]="imageChangedEvent"
  [maintainAspectRatio]="false"
  [aspectRatio]="5/ 3"
  format="jpeg"
  (imageCropped)="imageCropped($event)"
  (imageLoaded)="imageLoaded()"
  (cropperReady)="cropperReady()"
  (loadImageFailed)="loadImageFailed()"
  imageQuality=25
></image-cropper>

  <img [src]="croppedImage" />

  <ion-button expand='block' (click)="onButtonClicked()" shape='round'>Get cropped image</ion-button>

</ion-content>

そして、tsファイルで追加で下記2つのモジュールを読み込みます。

import { LoadingController } from '@ionic/angular';
import { Observable } from 'rxjs';

'
'
  constructor(
    public loadingController: LoadingController,
  ) {}

そして、tsファイルの変数として下記を追加します。

export class Tab1Page {
  imageChangedEvent: any = '';
  originalImgName:string;
  buffCroppedImageBase64:string;
  croppedImageBase64: any = '';
  croppedFileObject:any;
  show:any=false;
  • imageChangedEventは、画像の選択範囲が変わったら都度呼び出されるイベントに関する情報を保持するものです。
  • originalImgNameは、inputタグで読み込んだ画像から元のファイル名を取得できるのでそれを格納する変数です
  • buffCroppedImageBase64は、切り取った画像(base64です)を格納する一次ファイルです。HTML側で<img [src]=”croppedImageBase64″ />と定義していますが、ユーザーが選んでいる時に都度croppedImageBase64″に入れると切り取った画像がチラチラしてしまうので、一時的に格納するものとしてbuffCroppedImageBase64を用意しました。
  • CroppedImageBase64は最終的な結果を入れるものです。ここにBase64の画像を入れると、HTML側で表示されます
  • croppedFileObjectは、切り取った画像を別のファイルとして読み込むのに使用します。
  • showはProgressバーを制御するものです

fileChangeEventの部分を下記のように拡張します。

  fileChangeEvent(event: any): void {
    this.show = true;
    this.read(event).subscribe(()=>{
      console.log('done');
      this.show = false;
    });
  }

  read(event: any):Observable<any> {
    return new Observable(observer => {
      setTimeout(() => {
        this.imageChangedEvent = event;
        this.originalImgName = event.target.files[0].name;
        observer.next('e');
      }, 500);
    })
  }

重い画像だと読み込むのに数秒かかってしまいます。そこで、読み込み開始したらprogressバーで動作していることを知らせています、500は使った感触で決めました。

imageCroppedの関数を下記のように変えます。

  imageCropped(event: ImageCroppedEvent) {
      this.buffCroppedImageBase64 = event.base64;
  }

先ほど行ったように一度Bafferの方に切り取った画像を入れています。

最後にonButtonClickedの関数を下記のように変更します。


  onButtonClicked(){
    this.croppedImageBase64 = this.buffCroppedImageBase64;
    console.log(this.croppedImageBase64,'this.croppedImageBase64');

    var bin = atob(this.buffCroppedImageBase64.replace(/^.*,/, ''));
    // バイナリデータ化
    var buffer = new Uint8Array(bin.length);
    for (var i = 0; i < bin.length; i++) {
        buffer[i] = bin.charCodeAt(i);
    }
    this.croppedFileObject = new File([buffer.buffer], "heihei.jpg",{type: "image/jpeg"});
    console.log(this.croppedFileObject,'this.croppedFileObject');
  }

最終的に下記にようにします。

import { Component } from '@angular/core';
import { ImageCroppedEvent } from 'ngx-image-cropper';
import { LoadingController } from '@ionic/angular';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
  imageChangedEvent: any = '';
  originalImgName:string;
  buffCroppedImageBase64:string;
  croppedImageBase64: any = '';
  show:any=false;

  constructor(
    public loadingController: LoadingController,
  ) {}

  fileChangeEvent(event: any): void {
    console.log(event,'abcde');
    this.show = true;
    this.read(event).subscribe(()=>{
      console.log('done');
      this.show = false;
    });
  }

  read(event: any):Observable<any> {
    return new Observable(observer => {
      setTimeout(() => {
        this.imageChangedEvent = event;
        this.originalImgName = event.target.files[0].name;
        observer.next('e');
      }, 500);
    })
  }


  imageCropped(event: ImageCroppedEvent) {
      this.buffCroppedImageBase64 = event.base64;
  }
  imageLoaded(image: HTMLImageElement) {
      // show cropper
  }
  cropperReady() {
      // cropper ready
  }
  loadImageFailed() {
      // show message
  }

  onButtonClicked(){
    this.croppedImageBase64 = this.buffCroppedImageBase64;
    console.log(this.croppedImageBase64,'this.croppedImageBase64');

    var bin = atob(this.buffCroppedImageBase64.replace(/^.*,/, ''));
    // バイナリデータ化
    var buffer = new Uint8Array(bin.length);
    for (var i = 0; i < bin.length; i++) {
        buffer[i] = bin.charCodeAt(i);
    }
    this.croppedFileObject = new File([buffer.buffer], "heihei.jpg",{type: "image/jpeg"});
    console.log(this.croppedFileObject,'this.croppedFileObject');
  }


}

Ionic(Angular)でアコーディオンを実装する方法

Ionic(Angular)でアコーディオンを実装する方法です。

ポイント

  • Componentを自作して、それを読み込むようにします
  • これによりいろいろな場所で使い回しができるようになります

結果

こんな感じでクリックするとカードが広がるようになります。

環境

Ionic 4, 5, 6で動作検証しています。

流れ

  1. コンポーネントを作成し、HTMLとSCSSを記載します
  2. 読み込みたい場所で作成したコンポーネントをmoduleに登録します
  3. 使いたい場所のHTMLでapp-expandableタグで呼び出します

ソースコード

GItHubに上げました。

https://github.com/NP-Systems/Ionic_tips/tree/id1131-createAccordion/frontend

参考文献

https://www.joshmorony.com/creating-an-accordion-list-in-ionic/

初期状態

IonicのTabsのデフォルトテンプレートから始めます。

appフォルダ以下の構成は下記のようになっています。

.
├── app-routing.module.ts
├── app.component.html
├── app.component.scss
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── explore-container
│   ├── explore-container.component.html
│   ├── explore-container.component.scss
│   ├── explore-container.component.spec.ts
│   ├── explore-container.component.ts
│   └── explore-container.module.ts
├── tab1
│   ├── tab1-routing.module.ts
│   ├── tab1.module.ts
│   ├── tab1.page.html
│   ├── tab1.page.scss
│   ├── tab1.page.spec.ts
│   └── tab1.page.ts
├── tab2
│   ├── tab2-routing.module.ts
│   ├── tab2.module.ts
│   ├── tab2.page.html
│   ├── tab2.page.scss
│   ├── tab2.page.spec.ts
│   └── tab2.page.ts
├── tab3
│   ├── tab3-routing.module.ts
│   ├── tab3.module.ts
│   ├── tab3.page.html
│   ├── tab3.page.scss
│   ├── tab3.page.spec.ts
│   └── tab3.page.ts
└── tabs
    ├── tabs-routing.module.ts
    ├── tabs.module.ts
    ├── tabs.page.html
    ├── tabs.page.scss
    ├── tabs.page.spec.ts
    └── tabs.page.ts

自作コンポーネントの作成

下記のコマンドで自作コンポーネントを作成します。

% ionic g component components/Expandable

app.module.tsと同じ階層にcomponentsフォルダができて新しいコンポーネントが生成されます。

.
├── app-routing.module.ts
├── app.component.html
├── app.component.scss
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── components
│   └── expandable
│       ├── expandable.component.html
│       ├── expandable.component.scss
│       ├── expandable.component.spec.ts
│       └── expandable.component.ts
├── explore-container
│   ├── explore-container.component.html
│   ├── explore-container.component.scss
│   ├── explore-container.component.spec.ts
│   ├── explore-container.component.ts
│   └── explore-container.module.ts
├── tab1
│   ├── tab1-routing.module.ts
│   ├── tab1.module.ts
│   ├── tab1.page.html
│   ├── tab1.page.scss
│   ├── tab1.page.spec.ts
│   └── tab1.page.ts
├── tab2
│   ├── tab2-routing.module.ts
│   ├── tab2.module.ts
│   ├── tab2.page.html
│   ├── tab2.page.scss
│   ├── tab2.page.spec.ts
│   └── tab2.page.ts
├── tab3
│   ├── tab3-routing.module.ts
│   ├── tab3.module.ts
│   ├── tab3.page.html
│   ├── tab3.page.scss
│   ├── tab3.page.spec.ts
│   └── tab3.page.ts
└── tabs
    ├── tabs-routing.module.ts
    ├── tabs.module.ts
    ├── tabs.page.html
    ├── tabs.page.scss
    ├── tabs.page.spec.ts
    └── tabs.page.ts

tab1.module.tsでの読み込み

今回はtab1ページに埋め込むことにするので、tab1.module.tsに下記のように登録します。

import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Tab1Page } from './tab1.page';
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
import { ExpandableComponent } from "../components/expandable/expandable.component";

import { Tab1PageRoutingModule } from './tab1-routing.module';

@NgModule({
  imports: [
    IonicModule,
    CommonModule,
    FormsModule,
    ExploreContainerComponentModule,
    Tab1PageRoutingModule
  ],
  declarations: [Tab1Page,ExpandableComponent]
})
export class Tab1PageModule {}

expandable.component.htmlの変更

 src/components/expandable/expandable.component.htmlのファイルを下記のように変更します。

<p>
  expandable works!
</p>

<div #expandWrapper class='expand-wrapper' [class.collapsed]="!expanded">
    <ng-content></ng-content>
</div>

expandable.component.scssの変更

.expand-wrapper {
  transition: max-height 0.4s ease-in-out;
  overflow: hidden;
  height: auto;
}

.collapsed {
  max-height: 0 !important;
}

expandable.component.tsの変更

Before

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

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

  constructor() { }

  ngOnInit() {}

}

After

import { Component, AfterViewInit, Input, ViewChild,  ElementRef, Renderer2 } from "@angular/core";

@Component({
  selector: "app-expandable",
  templateUrl: "./expandable.component.html",
  styleUrls: ["./expandable.component.scss"]
})
export class ExpandableComponent implements AfterViewInit {
  @ViewChild("expandWrapper", { read: ElementRef }) expandWrapper: ElementRef;
  @Input("expanded") expanded: boolean = false;
  @Input("expandHeight") expandHeight: string = "150px";

  constructor(public renderer: Renderer2) {}

  ngAfterViewInit() {
    this.renderer.setStyle(this.expandWrapper.nativeElement, "max-height", this.expandHeight);
  }
}

tab1.page.htmlの変更

Before

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Tab 1
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Tab 1</ion-title>
    </ion-toolbar>
  </ion-header>

  <app-explore-container name="Tab 1 page"></app-explore-container>
</ion-content>

After

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Tab 1
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-card (click)="expandItem(item)" *ngFor="let item of items">
    <ion-card-header>
      <ion-card-title>隣のトトロ</ion-card-title>
    </ion-card-header>

    <ion-card-content>
      <app-expandable expandHeight="100px" [expanded]="item.expanded">
        <p>
          とっとろ、と、とーろ
        </p>
        <p>
          とっとろ、と、とーろ
        </p>
      </app-expandable>
    </ion-card-content>
  </ion-card>
</ion-content>

tab1.page.tsの変更

Before

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

@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page {

  constructor() {}

}

After

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

@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
  public items: any = [];

  constructor() {
    this.items = [
      { expanded: false },
      { expanded: false },
      { expanded: false },
      { expanded: false },
      { expanded: false },
      { expanded: false },
      { expanded: false },
      { expanded: false },
      { expanded: false }
    ];
  }

  expandItem(item): void {
    if (item.expanded) {
      item.expanded = false;
    } else {
      this.items.map(listItem => {
        if (item == listItem) {
          listItem.expanded = !listItem.expanded;
        } else {
          listItem.expanded = false;
        }
        return listItem;
      });
    }
  }
}

FileZillaでローカルのファイルにアクセスできない

FileZillaで「このディレクトリを表示する権限がありません」という表示が出てローカルのファイルにアクセスできないときは、

左上のリンゴのマークから「システム環境設定」から「セキュリティとプライバシー」を選択し、「ファイルとフォルダ」からFileZillaのアクセスできる場所を設定します。

自社内アプリを開発する私がIonicを気に入っている理由

Ionicとはなにか

会社の中で使う業務アプリをよくlonic(Angular)で作っています。ここでは、自社内アプリを開発する私がIonicを気に入っている理由を解説します。

一応Ionicについて説明しておくと、

  • HTMLやJavascript(TypeScript)系のWeb技術を使ってAndroid, iOSのスマホアプリやWindows,Mac向けのデスクトップアプリを作るフレームワーク
  • 「Web Components」というWeb標準の技術が採用されており、もっとも推奨されているのはAngularですが、ReactやVueからも利用可能

結論

先に結論書くとIonicを使う理由は

  • UI構築が楽
  • ページ遷移時の動作が良い
  • バンドルサイズが小さい
  • Capacitorと連携してNativeの機能にアクセスできる

からです。

UI構築が楽

自社内アプリなので、デザインは凝らなくていいんですよね。ださくなければいいです。以前Cordovaでスマホアプリを開発していましたのですが、さすがにHTMLとJavaScriptで記述したWebページがそのままAndroid, iOSで動作するとダサいです。

そ決してCordovaが悪いわけではないのですが、HTMLとJavaScriptで記述したWebページがそのままアプリになる以上、どうしてもダサいです。例えばボタン一つとってもbootstrapを使えばWebでは綺麗に見えるのにアプリで見るとなんか違う感が拭えなかったです。Monacaのonsen.uiというものでNativeの見た目を再現できるということでしたが、個人的な感想としてMonacaは嫌いでした。

  • ビルドがあまりにも遅すぎてPDCAが回せない
  • 有料でも作成できるプロジェクト数が不便を感じるほど限られている
  • Monacaという名称やonsen.uiという名前がときめかない
  • Monacaで一度作ると、Monaca上でしかメンテナンスできなくなる

というデメリットを感じ使用を中止しました。特に最後の「Monacaで一度作ると、Monaca上でしかメンテナンスできなくなる」というのは強烈で、有料プランでエンパワーメントしてもらえると思っていたのに、実際は囲い込まれているということで非常に残念に思いました。

その点、IonicはiOS/Android別の美しいUIを提供してくれます。IonicがAngularやReactから使えることからわかるように、Ionicの本質はUIコンポーネントのライブラリです。あるコンポーネント(例えばボタン)を設置したとしましょう。そうすると、iOSで表示した場合はAppleのHuman Interface Guidelinesに基づいたデザイン、Androidで表示した場合はマテリアルデザインと自動的に表示が切り替わります。これは実際に書いていて最高にウキウキするIonicの素晴らしいところです。

また、こちらの記事(Cordovaをdisる人類全員に読んでほしい「Cordovaつらいを考える」)に記載されているように、60fpsでの表示で滑らかに動くことや、無限スクロールが可能なことも拡張性を保証する大事なポイントだと考えています。

ページ遷移時の動作が良い

最近、スマホ画面の下にTabがあるアプリケーションが多くあります。Cordovaでも当然実装できるのですが、音楽を入れた時にページが切り替わるたび音楽が初めから再生されるという非常に残念な思いをしたことがあります。

その点、IonicはTabを切り替えてもバックグラウンド音楽が途切れない。。Nativeで開発しているわけではないのに、Nativeで違うことで悩まされないというのは地味ですが大きいと考えています。

この辺を非線形ナビゲーション」と「ナビゲーションスタック」というらしいですが、Ionicのドキュメントからコアコンセプトのページがなくなっていたので詳細をしらべることができませんでした。

バンドルサイズが小さい

特にAndroidでは顕著ですが、アプリケーションのバンドルサイズは非常に重要視されています。Androidアプリを開発されている方はご存知かもしれませんが、近年、Androidアプリの拡張子は.apkではないのです。デベロッパーはaboというプレビルドしたような形でアップロードしておいて、ユーザーがインストールする際、そのデバイスに最適化したapkをGoogleが生成してダウンロードします。日本のように通信環境が整っていない国も多いなかで、バンドルサイズを小さくすることがいかに重要視されているかわかると思います。

その点、lonicは、「Stencil」というWeb Components開発ライブラリによって作られており、バンドルサイズはとても小さくなります。

また、LazyLoadingという最初に全てを読み込むわけではなく、必要なものはあとで読み込む機能を実装しているので、軽量に高速で動作します。LazyLoadingはAngularにも実装されているので迷うかもしれませんが、こちらに英語ですがIonic X AngularにおけるLazy Loadingの実装方法について記事がまとまっています。このように公式が充実しているのもIonicのいいところです。https://ionicframework.com/blog/how-to-lazy-load-in-ionic-angular/

なお、Angularについて解説した記事はこちらですので、Angularについて知りたい方はどうぞ。

https://np-sys.com/general/%e5%80%8b%e4%ba%ba%e3%83%bb%e5%b0%91%e4%ba%ba%e6%95%b0%e3%81%ae%e3%82%b7%e3%82%b9%e3%83%86%e3%83%a0%e9%96%8b%e7%99%ba%e3%81%ab%e3%81%afangular%e3%81%8c%e3%82%aa%e3%82%b9%e3%82%b9%e3%83%a1/

Nativeの機能にアクセスできる

Ionicは(Cordovaもそうですが)Webページをアプリにできるだけではなく、そこにNativeのカメラ、位置情報などのAPIにアクセスできることは大きな魅力です。Ionicにはさまざまなプラグインがありますので、これを利用することでWeb技術を使いながら、それを超えたアプリケーションを作成することが可能です。

Ionicプラグインの導入方法

総括、その他

 Ionicについて簡単に解説しました。拡張性が担保されたフレームワークでプロジェクトをひとつ作り、それをそれぞれの最高のUIでクロスプラットフォームに出力できる点が一番気に入っています。

AngularでIonicを用いてtab barのデザインをカスタマイズする

Angualrとクロスプラットフォームアプリ開発用のフレームワークであるIonicを組み合わせて、Tab barをカスタマイズする方法を解説します。

ポイント

  • Tab barを備えたプロジェクトはIonicが提供しているテンプレートを使用
  • 比較的コードは単純で実装は容易

Github

Githubにコードあげています。

https://github.com/NP-Systems/Ionic_tips/tree/id-1113-customize-tab-bar

実装手順

Ionicが提供しているTab barを備えたテンプレートをダウンロードします。

% ionic start

Pick a framework! 😁

Please select the JavaScript framework to use for your new app. To bypass this
prompt next time, supply a value for the --type option.

? Framework: Angular

Every great app needs a name! 😍

Please enter the full name of your app. You can change this at any time. To
bypass this prompt next time, supply name, the first argument to ionic start.

? Project name: myApp

そうするとつらつら自動でダウンロードしてくれるので、完了後作成したフォルダへ移動します。

% cd myApp

フォルダ構成

tabsコンポーネント

tabs.page.htmlとtabs.page.scssの二つを変更します。

tabs.page.html

修正前

<ion-tabs>

  <ion-fab vertical="bottom" horizontal="center" translucent="true">
    <ion-fab-button (click)="goToPictures()">
      <ion-icon name="camera"></ion-icon>
    </ion-fab-button>
  </ion-fab>

  <ion-tab-bar slot="bottom" class="ion-no-border">


    <ion-tab-button tab="tab-conversations" class="comments">
      <ion-icon name="triangle"></ion-icon>
      <ion-label>Tab 1</ion-label>
      <ion-badge >Hi</ion-badge>
    </ion-tab-button>

    <svg height="50" viewBox="0 0 100 50" width="100" xmlns="http://www.w3.org/2000/svg">
      <path d="M100 0v50H0V0c.543 27.153 22.72 49 50 49S99.457 27.153 99.99 0h.01z" fill="red" fill-rule="evenodd">
      </path>
    </svg>

    <ion-tab-button tab="tab-notifications" class="notifs">
      <ion-icon name="notifications"></ion-icon>
      <ion-badge *ngIf="new_activities">{{new_activities}}</ion-badge>
    </ion-tab-button>


  </ion-tab-bar>
</ion-tabs>

修正後

<ion-tabs>

  <ion-fab vertical="bottom" horizontal="center" translucent="true">
    <ion-fab-button (click)="yourCustomFunction()">
      <ion-icon name="camera"></ion-icon>
    </ion-fab-button>
  </ion-fab>

  <ion-tab-bar slot="bottom" class="ion-no-border">

    <ion-tab-button tab="tab-conversations" class="comments">
      <ion-icon name="triangle"></ion-icon>
      <ion-label>Tab 1</ion-label>
      <ion-badge >Hi</ion-badge>
    </ion-tab-button>

    <svg height="50" viewBox="0 0 100 50" width="100" xmlns="http://www.w3.org/2000/svg">
      <path d="M100 0v50H0V0c.543 27.153 22.72 49 50 49S99.457 27.153 99.99 0h.01z" fill="red" fill-rule="evenodd">
      </path>
    </svg>

    <ion-tab-button tab="tab-notifications" class="notifs">
      <ion-icon name="notifications"></ion-icon>
    </ion-tab-button>


  </ion-tab-bar>
</ion-tabs>

tabs.page.scss

最初は何も書いてありませんが、下記のように変更します。


ion-tabs{
	ion-fab {
		margin-bottom: env(safe-area-inset-bottom); /* fix notch ios*/
		ion-fab-button {
			--box-shadow: none;
		}
	}
	ion-tab-bar {
		--border: 0;
		--background: transparent;
		position: absolute;
		bottom: 0;
		left:0; right: 0;
		width: 100%;
		&:after{
			content: " ";
			width: 100%;
			bottom: 0;
			background: var(--ion-color-light);
			height: env(safe-area-inset-bottom);
			position: absolute;
		}
		ion-tab-button {
			--background: var(--ion-color-light);
		}
		ion-tab-button.comments {
			margin-right: 0px;
			border-top-right-radius: 18px;
		}
		ion-tab-button.notifs {
			margin-left: 0px;
			border-top-left-radius: 18px;
		}
		svg {
			width: 72px;
			margin-top: 19px;
			path{
				fill:  var(--ion-color-light);
			}
		}
	}
}

結果

アイコンにメッセージを追加している部分はこのように書いています。

    <ion-tab-button tab="tab-conversations" class="comments">
      <ion-icon name="triangle"></ion-icon>
      <ion-label>Tab 1</ion-label>
      <ion-badge >Hi</ion-badge>
    </ion-tab-button>

元ネタだとちゃんと条件分岐をしていました。

<ion-badge *ngIf="new_message">{{new_message}}</ion-badge>

参考文献

https://forum.ionicframework.com/t/ionic-5-awesome-tab-bar-with-curve/186579

新しいMacでXcodeの証明書を引き継ぐ

新しいMacでXcodeでアプリ開発をするための設定についてまとめます。

  • 以前のMacは必要なく、新しいMacだけで完結します

まず、ブランクのアプリを作ってみることから始めます。そうすると、Signing&Capabilityの部分でTeamを選べない問題に直面します。

そこで、これをまず解決する必要がりますので、Add an Accountというボタンを押して、Apple IDでサインインします。

そうすると、一応ビルドはできるようになります。しかし、そのままだとCodeSignの「キーチェーンログインのパスワードを入力してください」でパスワードを繰り返し求められて先に進みませんでした。

そこで、https://developer.apple.comからApple Developerにサインインして証明書をダウンロードしました。

ここで、該当するProfileをダウンロードしてダブルクリックしたら、動作するようになりました。

こんなに簡単だったっけって感じです。。あとで落とし穴がないか怖いです。

MacのXcodeのデベロッパーライセンスを更新するときにやること

Xcodeのデベロッパーライセンスを更新するときにやることをまとめておく。

まず、Appleからライセンス切れる旨を伝えるメールが来るので購入する。

次に、Apple Developerにログインして、Certificates&Identifiers & ProfilesのDevicesを選ぶ。

Continueを選ぶ。下記のようにデバイスリストを最新にしてくださいというメッセージが表示される。

Now that you’ve started a new membership year, make sure your device list is up to date. Once you complete this process, new devices can be added.

Select the devices you’d like to keep for this membership year and deselect the devices you’d like to remove. Only your currently enabled devices are shown and all previously disabled devices will be removed automatically. To keep a previously disabled device, visit the All Devices page, enable the devices you’d like to keep, and return to this page.

Continueを押下

この画面の「I acknowledge that any devices I disable during this membership year will continue to count towards my total registered devices.」にチェックを入れてResetする。

私は以上だけでした。

Macの環境設定の備忘録

データ分析とWebアプリ開発をしているエンジニアのMacの設定方法です。Mac book Pro 2019,2020でそれぞれBig SurとCataliaにて設定したものを備忘録として書いたものです。

  • CapsLockでの英語/日本語の切り替え
  • スクロール方向の切り替え
  • スクリーンショットの拡張子変更
  • aliasの設定
  • 一般ソフト(Chrome,Gimp,FileZilla,Atom,VScode)のインストール
  • Tableau Publicのインストール
  • node.jsのインストール
  • Ionicのインストール
  • Xcodeのインストール
  • CocoaPodのインストール
  • Pythonのインストール
  • GitHubのssh設定

CapsLockでの英語/日本語の切り替え

CapsLockで英語/日本語の切り替えができるようにします。

  1. デスクトップの左上にあるAppleのマークからシステム環境設定を選び
  2. 「キーボード」を選択し
  3. 上に並んでいるタブのうち、左から4番目の「入力ソース」を選択し
  4. ウィンドウの下にある「CapsLockでABC入力と切り替える」のチャックを打つ

スクロール方向の切り替え

タッチパッドの指の動かし方と画面の遷移方向を設定します。

  1. デスクトップの左上にあるAppleのマークからシステム環境設定を選び
  2. 「トラックパッド」を選択し
  3. 上に並んでいるタブのうち、左から2番目の「スクロールとズーム」を選び
  4. 「スクロールの方向:ナチュラル」のチェックを外す

スクリーンショットの拡張子変更

デフォルトのpngだと容量が大きすぎるので、jpgに変更して容量が小さくなるようにします。

defaults write com.apple.screencapture type 拡張子

で拡張子を指定すれば変更できるので、

defaults write com.apple.screencapture type jpg
killall SystemUIServer
exit

とします。jpgをpng,pdf,tiffなど任意のものに変更可能です。

aliasの設定

terminalにaliasを設定して、任意のコマンドを自分好みのコマンドで打てるようにします。

使用シェルがbashなら~/に.bash_profileと.bashrcを作成し、zshなら.zshrcと.zshprofileを作成し設定します。ログイン時に実行されるのは.bash_profileと.zshprofileで、.bashrcと.zshrchはターミナル起動時にいつも実行されます。

私はBig Surだったので、.zshrcに

alias o='open ./'
alias p='pwd'
alias l='ls'
alias n='cd /Users/masaya/Desktop;open /Users/masaya/Desktop'

と書きました。設定を反映するために再起動をするか

source ~/.zshrc

します。

一般ソフト(Chrome,Gimp,FileZilla,Atom,VScode)のインストール

いずれも公式サイトにアクセスして、ファイルをダウンロードすれば特に設定なしで使えるものです。

項目
Chrome普通にインストールします。dmgファイルをインストールしてから、ダブルクリックで起動して画面の操作にしたがってアイコンを移動するだけです。インストールあと、dmgファイルはゴミ箱に移動です、
VScodehttps://azure.microsoft.com/ja-jp/products/visual-studio-code/からダウンロード
Atomhttps://atom.io/からダウンロード

その他、Vivaldi,Firefoxなども同じようにインストールします。

Tableau Public

データ可視化用にTableau Publicを入れます。これも公式からダウンロードしてぽちぽちするだけです。

FileZillaのインストール

Macで無料で使えるFTPソフトです。

node.jsのインストール

Ionicのインストール

AndroidStudioのインストール

https://developer.android.com/studioからインストールします。

Xcodeのインストール

AppStoreから最新をインストールします。11GB程度と非常に重いので注意。

証明書関係が面倒ですが、以前のメモには下記のように書いてあります。

めちゃくちゃ苦労した。全て記録に残せたわけではないが、少なくとも以前のMacは必要なかった。AppleDeveloperサイトに行って、ログインする。Certificates, Identifiers & ProfilesからPlatformのカラムがALLのもののうち、DevelopmentとDeistributionを選んで、ダウンロードしてキーチェーンに登録する。これを新しい方でダブルクリックして登録する。それと、app developer idでパソコンでログインして、新しいMacを登録する。そうするとチームが選べるようになる。基本的には以上の二つでOK.ってこんだけのことだったっけ?

CocoaPodのインストール

https://cocoapods.org/に基づいて

sudo gem install cocoapods

でインストールします。

Pythonのインストール

以前はAnacondaを使っていましたが、有料化したので使うのはやめました。有料のAnacondaをインストールしたいなら、公式からダウンロードしてぽちぽちすればいけます。

https://www.anaconda.com/products/individual

GitHubのssh設定

Github用に .sshをホームディレクトリに配置して設定します。

ホーム画面で.sshディレクトリを作成します。

% mkdir ./.ssh
masaya@local-host ~ % cd .ssh 

鍵を作成します。

%  ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/masaya/.ssh/id_rsa): /Users/masaya/.ssh/id_rsa
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /Users/masaya/.ssh/id_rsa.
Your public key has been saved in /Users/masaya/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:chw/VNuyHQ9D0DLooAVRfu9KtEK4TXoOVh36Gkw2c9U masaya@local-host
The key's randomart image is:
+---[RSA 3072]----+
|      o+.  .oo.  |
|       .o ..o=.  |
|       ooo+ +oE  |
|      .o B.+ + = |
|      o S * o . .|
|       & * +     |
|      = * + .    |
|     . + = .     |
|        o .      |
+----[SHA256]-----+

権限を読み取り専用に変更して

 chmod 600 id_rsa

情報をコピーして

% pbcopy < ~/.ssh/id_rsa.pub

GitHubに行って、アイコンをクリックしてから「settings」 > 「SSH and GPG keys」 > 「New SSH Key」と進んで、情報を貼り付ける

あとは下記で疎通できることを確認します。

% ssh -T git@github.com
The authenticity of host 'github.com (13.**.40.48)' can't be established.
RSA key fingerprint is SHA256:********pJWGl7E1IGOCspRomTxdCARLviKw6E5SY8.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'github.com,13.***.40.48' (RSA) to the list of known hosts.
Hi N****! You've successfully authenticated, but GitHub does not provide shell access.

https://qiita.com/unsoluble_sugar/items/14bea376d8e6fce82eb3

設定でよく使うコマンド

macOSでデスクトップ表示 :「Fn」+「F11」

隠しファイルの表示;command shift .

Macにionicをインストールしてサンプルプロジェクトを作成する

クロスプラットフォームアプリケーションのフレームワークであるIonicをMacにインストールする方法について紹介します。

まず、node.jsをインストールする必要があります。インストールしていない方はこちらに最も簡単なインストール方法を記載していますのでご覧ください。

インストール方法

npm経由でのインストールとなりますので、sudo npm install -g @ionic/cliコマンドを打ちます。

% sudo npm install -g @ionic/cli
Password:
⸨   ░░░░░░░░░░░░░░░⸩ ⠇ loadDep:yallist: sill resolveWithNewModule smart-buffer@

ionic -vでバージョンが表示されたらインストール完了です。

% ionic -v
6.12.3

アプリを作ってみる

ionic startコマンドで簡単なアプリを作ってみます。

 % ionic start

Pick a framework! 😁

Please select the JavaScript framework to use for your new app. To bypass this
prompt next time, supply a value for the --type option.

? Framework: (Use arrow keys)
❯ Angular | https://angular.io 
  React   | https://reactjs.org 

今回はAngularを選びます。プロジェクト名やCapacitorがいるかなど聞かれますので、入力します。CapacitorはIonicのプロジェクトをAndroid,iOS,デスクトップアプリとして動作させる環境(ランタイム)です。Web用なら入りませんが、今回は入れておきます。

? Project name: my-first-app

Let's pick the perfect starter template! 💪

Starter templates are ready-to-go Ionic apps that come packed with everything
you need to build your app. To bypass this prompt next time, supply template,
the second argument to ionic start.

? Starter template: tabs
✔ Preparing directory ./my-first-app in 1.93ms
✔ Downloading and extracting tabs starter in 998.19ms
? Integrate your new app with Capacitor to target native iOS and Android? (y/N) 
y

ダウンロードしたら、プロジェクトへ移動します。

% cd my-first-app/

ionic serveコマンドでビルドしてみます。

% ionic serve    
> ng run app:serve --host=localhost --port=8100

アプリケーションが立ち上がったら完了です。

参考文献

https://ionicframework.com/docs/intro/cli

MacにFileZillaをインストールする

Macで無料で使えるFTPソフトであるFIleZillaをインストールする方法について解説します。

まず、公式サイトからFileZillaの圧縮ファイルをダウンロードします。

https://filezilla-project.org/download.php?type=client#close

FileZilla_3.51.0_macosx-x86.app.tar.bz2というファイルがダウンロードされますので、ダウンロード先でダブルクリックして解凍します。

下記のようなアイコンのついたものが作成されます。

これがアプリケーション本体なので、このファイルをコピペして、アプリケーションフォルダへ移動して完了です。

普通にダブルクリックしたら起動するので、あとはサーバ情報を入力して接続します。

Macでnode.jsをインストールする

日本ではhomebrewでインストールする方法ばかりが紹介されていますが、海外ではほとんど情報がなくマイナーな方法です。node.jsの公式からダウンロードしてインストールする方法が多く紹介されていますが、こちらの方が簡単なので今回はその方法を紹介します。

installerを公式から落とす

https://nodejs.org/en/にアクセスしてinstallerをダウンロードします。

LTSと書いてあるものがサポートの長い安定版です。

ダウンロードしたら、ファイルをクリックして起動させます。

途中、インストール先が表示されるのでメモっておきます。あとは画面の指示に従って進みます。完了したらインストーラはゴミ箱に移動していいですかと聞かれます。これが表示されたらもうこれだけでインストール完了です。

動作確認

ターミナルで下記のように打ち込んでバージョンが表示されたらOKです。

% node -v
v14.15.1

もしバージョンが表示されない場合、下記のコマンドを打って現在のパスを通している場所を表示させ、インストーラで保存先に指定していた場所が入っているかどうかを確かめます。

% echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

簡単なプログラムを書いてみる

node.jsがちゃんと動作しているか簡単なコマンドで調べてみます。

% node
Welcome to Node.js v14.15.1.
Type ".help" for more information.
> console.log('hello node.js')
hello node.js
undefined

ちゃんと動作していることが確認できたら、control + cを2回打って終了します。

【oandapyV20】OandaAPIでオーダブック、ポジションブックを取得する

この記事ではoandapyV20を使ってオーダブック、ポジションブックを取得する方法について記載します。

oandapyV20では、オーダーブックをとるためにはoandapyV20.endpoints.instrumentsのInstrumentsOrderBookというクラスで/v3/instruments/{instrument}/orderBookをGet通信で叩きます。

ポジションブックの場合は、oandapyV20.endpoints.instrumentsのInstrumentsPositionBookというクラスで/v3/instruments/{instrument}/positionBookをGet通信で叩きます。

いずれの場合もparamsで指定できるパラメータはinstrumentとtimeだけです。instrumentは必須引数ですが、timeは指定しない場合は最新が返されます。timeだUNIXTIMEを文字列型で渡します。最新を取りたい場合は、paramsは{}でいいと思います。

オーダーブックもポジションブックもほとんど同じなので、今回はオーダブックで説明したいと思います。

ドル円の最新のオーダーブックを取得するコードはこちらです。ポジションブックを見たい場合はInstrumentsPositionBookを使ってください。

# -*- coding: utf-8 -*-
import json
from oandapyV20 import API
from oandapyV20.endpoints.pricing import PricingInfo
from oandapyV20.exceptions import V20Error
import oandapyV20.endpoints.instruments as instruments
import datetime
import pandas as pd

def main():
    account_id="XXX-XXX-XXXXXXX-XXX"
    access_token = "***************************************************************"
    api = API(access_token=access_token, environment="practice")#or live

    params ={}
    r = instruments.InstrumentsOrderBook(instrument="EUR_USD",
                                         params=params)
    api.request(r)
    data = (r.response)
    print(data)

if __name__ == "__main__":
    main()

返り値の中身を詳細に見てみます。所期のデータはdata[‘orderBook’][‘buckets’]にリストで返ってきています。

#まずはなんの型が帰ってきているかきます。
(Pdb) type(data)
<class 'dict'>
(Pdb) data.keys()
dict_keys(['orderBook'])

(Pdb) type(data['orderBook'])
<class 'dict'>
(Pdb) data['orderBook'].keys()
dict_keys(['instrument', 'time', 'unixTime', 'price', 'bucketWidth', 'buckets'])

(Pdb) data['orderBook']['instrument']
'EUR_USD'
(Pdb) data['orderBook']['time']
'2020-12-07T12:00:00Z'
(Pdb) data['orderBook']['price']
'1.21131'
(Pdb) data['orderBook']['bucketWidth']
'0.00050'

(Pdb) type(data['orderBook']['buckets'])
<class 'list'>
(Pdb) len(data['orderBook']['buckets'])
5548
(Pdb) data['orderBook']['buckets'][0]
{'price': '0.00000', 'longCountPercent': '0.4861', 'shortCountPercent': '0.3107'}
(Pdb) data['orderBook']['buckets'][1000]
{'price': '1.12050', 'longCountPercent': '0.0351', 'shortCountPercent': '0.0200'}
(Pdb) data['orderBook']['buckets'][3000]
{'price': '7.59750', 'longCountPercent': '0.0000', 'shortCountPercent': '0.0150'}
(Pdb) data['orderBook']['buckets'][5000]
{'price': '107.77800', 'longCountPercent': '0.0025', 'shortCountPercent': '0.0000'}

綺麗な最終結果だけが欲しいならdata[‘orderBook’][‘buckets’]をPandasに変換します。

(Pdb) pd.DataFrame(data['orderBook']['buckets'])
                 price longCountPercent shortCountPercent
0              0.00000           0.4861            0.3107
1              0.00050           0.1428            0.0802
2              0.00100           0.2956            0.0451
3              0.00150           0.0576            0.0251
4              0.00200           0.0401            0.0225
...                ...              ...               ...
5543    34887878.00000           0.0025            0.0000
5544   100000000.00000           0.0025            0.0000
5545  1000000000.00000           0.0000            0.0050
5546  7000000000.00000           0.0000            0.0025
5547  9999999999.00000           0.0025            0.0000

[5548 rows x 3 columns]

参考文献

https://developer.oanda.com/rest-live-v20/instrument-ep/

https://developer.oanda.com/rest-live-v20/instrument-ep/

ローソクを取得したい場合はこちらをご覧ください

https://np-sys.com/hacks-with-it/oanda%e3%81%a7%e3%81%ae%e7%82%ba%e6%9b%bf%e3%83%87%e3%83%bc%e3%82%bf%e3%82%92%e5%8f%96%e5%be%97/

現在の価格をリアムタイムに欲しい場合はこちらをご覧ください

https://np-sys.com/hacks-with-it/%e3%80%90oandapyv20%e3%80%91oandaapi%e3%81%a7%e4%be%a1%e6%a0%bc%e3%82%92streaming%e9%85%8d%e4%bf%a1%e3%81%a7%e5%8f%96%e5%be%97%e3%81%99%e3%82%8b/

【oandapyV20】OandaAPIで価格をStreaming配信で取得する

この記事ではoandapyV20を使ってStreaming配信でリアルタイムに価格を取得する方法について記載します。

Streaming配信は非常に便利であるものの、最大で250msごとのデータしか提供されませんので急激な値動きまで把握できるわけではない事に注意が必要です。公式では「1秒間に4回のウィンドウを作成し、それぞれのウィンドウの最後で有効な値を返す」という記載があります。詳細は記載されていないものの、250ms程度のデータであること、アカウントごとにデータを生成するので、すべての人に同じデータが配信されるわけではない事に留意が必要です。

ストリーミング配信は/v3/accounts/{accountID}/pricing/streamにGet通信でアクセスする事で提供されます。

oandapyV20を使った基本のコードはこちらです(ドル円の価格を取得)。

# -*- coding: utf-8 -*-
import json
from oandapyV20 import API
from oandapyV20.endpoints.pricing import PricingInfo
from oandapyV20.exceptions import V20Error
import oandapyV20.endpoints.pricing as pricing
import oandapyV20.endpoints.instruments as instruments
import datetime
import pandas

def main():
    account_id="XXX-XXX-XXXXXXX-XXX"
    access_token = "******************************************************************"
    api = API(access_token=access_token, environment="practice")#or live

    params ={
            "instruments": "USD_JPY,EUR_JPY"
            }
    r = pricing.PricingStream(accountID=account_id, params=params)
    rv = api.request(r)
    maxrecs = 100
    for ticks in rv:
        print(json.dumps(ticks, indent=4),",")
        if maxrecs == 0:
            r.terminate("maxrecs records received")

if __name__ == "__main__":
    main()

これを実行すると、価格のリアルタイムデータが取得でします。ticksという変数にはこのような値が入ります。

{
    "type": "PRICE",
    "time": "2020-12-04T21:59:55.602697089Z",
    "bids": [
        {
            "price": "104.185",
            "liquidity": 250000
        }
    ],
    "asks": [
        {
            "price": "104.191",
            "liquidity": 250000
        }
    ],
    "closeoutBid": "104.177",
    "closeoutAsk": "104.199",
    "status": "non-tradeable",
    "tradeable": false,
    "instrument": "USD_JPY"
} ,

基本的には通貨ペア(Instruments)しかカスタマイズしないと思います。通貨ペアを変更したいときはparamsの値を変えてください。2つ以上のペアを同時にデータ取得したいときは、paramsにそれぞれのペアをカンマで区切って指定すればOKです。例えば、2つを指定したいときはこのように指定します。

    params ={
            "instruments": "USD_JPY,EUR_JPY"
            }

そうすると下記のデータが返ってきます。

{
    "type": "PRICE",
    "time": "2020-12-04T21:59:53.324563034Z",
    "bids": [
        {
            "price": "126.254",
            "liquidity": 250000
        }
    ],
    "asks": [
        {
            "price": "126.320",
            "liquidity": 250000
        }
    ],
    "closeoutBid": "126.238",
    "closeoutAsk": "126.336",
    "status": "non-tradeable",
    "tradeable": false,
    "instrument": "EUR_JPY"
} ,
{
    "type": "PRICE",
    "time": "2020-12-04T21:59:55.602697089Z",
    "bids": [
        {
            "price": "104.185",
            "liquidity": 250000
        }
    ],
    "asks": [
        {
            "price": "104.191",
            "liquidity": 250000
        }
    ],
    "closeoutBid": "104.177",
    "closeoutAsk": "104.199",
    "status": "non-tradeable",
    "tradeable": false,
    "instrument": "USD_JPY"
} ,
{
    "type": "HEARTBEAT",
    "time": "2020-12-06T11:28:32.333476447Z"
} ,

リアルタイムでデータが取得できるのはとても便利です。

通貨ペアの指定について

円関連

“USD_JPY”,’EUR_JPY’,’GBP_JPY’,’AUD_JPY’,’NZD_JPY’,’CAD_JPY’,’CHF_JPY’,

ドル関連

“EUR_USD”,’GBP_USD’,’AUD_USD’,’NZD_USD’,’USD_CAD’,’USD_CHF’,’USD_CNH’,

ユーロ関連

‘EUR_GBP’,’EUR_AUD’,’EUR_NZD’,’EUR_CHF’,’GBP_AUD’

参考文献

https://developer.oanda.com/rest-live-v20/pricing-ep/

https://developer.oanda.com/rest-live-v20/pricing-ep/

ローソクチャートを取得したいときはこちらの記事をご覧ください。

https://np-sys.com/hacks-with-it/oanda%e3%81%a7%e3%81%ae%e7%82%ba%e6%9b%bf%e3%83%87%e3%83%bc%e3%82%bf%e3%82%92%e5%8f%96%e5%be%97/

GCP(Google Cloud Platform)の特徴を解説。主要サービスの紹介も。

GCP(Google Cloud Platform)は、GoogleがGmail,Youtubeなど一般向けに開発しているサービスのバックエンドで使われているクラウド基盤を外部に公開したものです。GoogleはGCPですべてのサービスを運用しており、同じ環境を安価に利用することができます

「Datacenter as a Computer」という言葉に集約されるように、GCPではGoogleの世界中にあるコンピュータをまとめて一つのコンピュータのように扱えるようにするというコンセプトがあり、エンジニアに取って使いやすいサービスとなっています。

歴史

GCPは2008年に公開されたGoogleAppEngineが元になっており、IaaSとして始まったAWSとは異なり当初はPaaSとして導入されました。その後、2011年にGoogleCloudSQL ,GoogleCloudStoragが導入され、2013年にIaaSであるGCEが導入され現在の主要サービスがそろいました。

東京リージョンが開設されたのは2016年であり、まだまだ日本では導入後まもないサービスです

基本概念

GCPではユーザー、課金アカウント、プロジェクトという3つの基本概念があります。ユーザーとは、Googleのサービスを利用するときに利用しているIDのことです。GCP専用のアカウントは存在せず、GoogleアカウントにGCPの情報を紐づけることとなります。プロジェクトとは、GCPの中で機能やサービスを束ねるためのものです。プロジェクト内では各サービスの連携は容易ですが、プロジェクトを跨ぐことは基本的にはできません。さらにプロジェクトには1つの課金アカウントが設定されている必要があり、こちらに対して請求が来る事になります。

コンポーネントについて

GCPには様々なサービスがありますが、代表的なものを下記にまとめました。

分類名称機能
コンピューティングComputeEngineクラウド上で仮想マシンを立ち上げられます。OS以上のレイヤーを自由に設定できる一方で、環境構築やメンテナンスを自分でやらなければならないため上級者向けです。
コンピューティングAppEngineアプリケーションを運用してくれるPaaSサービス。GUIのアプリのホスティングサービス。基本的にはリクエスト受け取り後、60秒以上の処理はできない。ローカルファイルへのアクセスは禁止されている。
コンピューティングCloudFunctions関数ベースでGUIを伴わないサービスについてはこちらで十分。関数プログラムをホスティングできるサーバレスなコンピューテング環境
分類名称機能
ストレージCloud Storageストレージサービス。データ保存だけではなく、HTMLや画像などの静的なデータを直接Webに公開することも可能。バケットと呼ばれるコンテナで管理される。ファイルやフォルダと言ったGCS上のデータはオブジェクトと呼ばれる。オブジェクト数に上限はないが、1オブジェクト5TBまで。
ストレージSQLMySQLなどのリレーショナルデータベース
ストレージFirestoreNoSQLサービス
ストレージBigtableスケーラブルなNoSQLサービス
ビッグデータBigQuery低コストなビッグデータ解析用サービス。SQLでデータ取得可能。読み出しに強いのがSQLとの違い。

その他、様々なAPIが提供されています。

特徴

AmazonやMicrosoftと比較したGoogleの特徴の一つは、インフラを自社で保有していることです。Googleでは、データセンタ、光ファイバ回線などを独自で設計し導入しています。世界規模のクローズトネットワークを保有している数少ない事業者がGoogleですが、これによりサーバ間のデータ転送は極めて高速で可用性に優れているという特徴があります。また、元々、世界中の個人に対してサービスを提供してきた背景から、スケーラビリティと大規模処理に非常に強く大量のデータを保有しているために機械学習などのモデル精度に優れています。日本ではAWSが最も勢いに乗っていると思いますが、独自のサービスやデータに強いというところで一定の存在感を持ったサービスであり続ける可能性が高いです。

サービスアカウントとメンバーについて

https://cloud.google.com/iam/docs/service-accounts?hl=ja

GCPではメンバーとサービスアカウントという概念があります。メンバーはプロジェクトに登録した個々のGoogleアカウント(ユーザーアカウント)とサービスアカウントであり、サービスアカウントはアプリケーションなどで使用される特別なアカウントです。リソースへのアクセスの際はサービスアカウントを介する必要があります。

重要なのは、サービスアカウントを作成するとメンバーにも登録されることです。メンバーを登録してもサービスアカウントは作成されないです。サービスアカウントの方が強いイメージです、

もし証明書を介してリソースにアクセスする際、下記の手順を踏みます。

  • サービスアカウントを作成する
  • そのサービスアカウントはメンバーとしても登録されているので、メンバーの画面から権限を編集
  • サービスアカウントの画面から証明書の発行

という流れになります。確証はありませんが、証明書の権限は発行された際のメ権限を反映しているような挙動になりました。そんなはずはないですが、でもそれで少し詰まりました。おそらく多少はタイムラグがあるということかもしれないです。

【oandapyV20】OandaAPIでローソクを取得

この記事ではoandapyV20を使ってローソクチャートを取得する方法について記載します。ローソクの長さ、取得データの種類、取得期間の指定方法なども含めて記載します。

oandapyV20では、PricingInfoというクラスで/v3/instruments/{instrument}/candlesをGet通信で叩いています。

ドル円のローソクチャートを取得基本のコードはこちらです。

# -*- coding: utf-8 -*-
import json
from oandapyV20 import API
from oandapyV20.endpoints.pricing import PricingInfo
from oandapyV20.exceptions import V20Error
import oandapyV20.endpoints.instruments as instruments
import datetime
import pandas

def main():
    account_id="XXX-XXX-XXXXXXX-XXX"
    access_token = "***************************************************************"
    api = API(access_token=access_token, environment="practice")#or live

    params = {"instruments": "USD_JPY"}
    pricing_info = PricingInfo(accountID=account_id, params=params)


    r = instruments.InstrumentsCandles(instrument="USD_JPY",
                                    params={
                                        "granularity": "S15", # ロウソク足の種類を選択
                                        "alignmentTimezone": "Japan", # タイムゾーン
                                        #"from":datetime.datetime.now()
                                    }
    )
    data = api.request(r)
    for candle in r.response["candles"]:
        print(candle)

if __name__ == "__main__":
    main()

データの返り値であるcandleの変数には下記の値が入ります

(Pdb) candle
{'complete': True, 'volume': 117597, 'time': '2020-11-05T08:00:00.000000000Z', 'mid': {'o': '104.314', 'h': '104.388', 'l': '103.360', 'c': '103.424'}}

ローソク足の長さを指定する

上記ではローソクの長さを15秒に指定しますが、最短で5秒、最大で1ヶ月まで変更できます。oandapyV20では、InstrumentsCandlesをインスタンス化する際のparamという変数で様々な取得条件を指定しますが、ローソク足の長さはgranularityというキーで指定します。これに対応する値は文字列型で下記で指定します。

    r = instruments.InstrumentsCandles(instrument="USD_JPY",
                                    params={
                                        "granularity": "S15", # ロウソク足の種類を選択
                                        "alignmentTimezone": "Japan", # タイムゾーン
                                        #"from":datetime.datetime.now()
                                    }
    )
引数間隔
S55秒間
S1010秒間
S1515秒間
S3030秒間
M11分間
M22分間
M33分間
M44分間
M55分間
M1010分間
M1515分間
M3030分間
H11時間
H22時間
H33時間
H44時間
H66時間
H88時間
H1212時間
D1日
W1週間
M1ヶ月

総データ取得期間の指定

総データ取得期間を指定する際は、同様にparamという辞書の中で指定します。この中でfromとtoというキーで時間を「文字列型」のUNIXTIMEで指定します。

r = instruments.InstrumentsCandles(
                        instrument="USD_JPY",
                        params={
                            "granularity": "D",
                            "alignmentTimezone": "Japan",
                            "from":str((datetime.datetime.now()-datetime.timedelta(days=30)).timestamp()),
                            "to":str((datetime.datetime.now()-datetime.timedelta(days=25)).timestamp())
                            }
                        )

キャンドルの本数の指定

データの期間を指定する代わりに、キャンドルの本数で総データ取得期間を指定することもできます。デフォルトは500本となります。キャンドルの本数で帰って来るデータの量を指定したい場合はcount引数を整数で指定します。なお、期間をfrom,toで指定した場合こちらは使用できません。countの最大は5000本です。

r = instruments.InstrumentsCandles(
instrument="USD_JPY",
params={
"granularity": "D",
"alignmentTimezone": "Japan",
"count":100}
)

ローソクの価格種を指定する

ask,bid,middleのいずれのローソクを取得するかについてはparamsでpriceキーで指定できます。“M” はmidpoint candles、 “B” はbid candles)、 “A” はask candlesとのことで、デフォルトは”M”となります。

midの場合

(Pdb) candle
{'complete': True, 'volume': 117597, 'time': '2020-11-05T08:00:00.000000000Z', 'mid': {'o': '104.314', 'h': '104.388', 'l': '103.360', 'c': '103.424'}}

bidの場合

(Pdb) candle
{'complete': True, 'volume': 117597, 'time': '2020-11-05T08:00:00.000000000Z', 'bid': {'o': '104.308', 'h': '104.382', 'l': '103.354', 'c': '103.418'}}

通貨を指定する

これはinstrument引数で指定します。通貨のペアを/ではなく_でつないで指定します。

例えば,よく使うものを列挙すると

ペアコード
ドル円USD_JPY
ドルユーロEUR_USD
ユーロ円EUR_JPY

https://www.oanda.jp/course/currencypair

参考文献

https://oanda-api-v20.readthedocs.io/en/latest/endpoints/instruments/instrumentlist.html

https://developer.oanda.com/rest-live-v20/instrument-ep/

リアルタイムでデータを取得したい場合はこちらの記事をご覧ください。

https://np-sys.com/hacks-with-it/%e3%80%90oandapyv20%e3%80%91oandaapi%e3%81%a7%e4%be%a1%e6%a0%bc%e3%82%92streaming%e9%85%8d%e4%bf%a1%e3%81%a7%e5%8f%96%e5%be%97%e3%81%99%e3%82%8b/

Adsenseの住所確認の日数と手順について

Google Adsenseでは、収益が1000円程度を超えると住所確認のために郵送でPINコードが発送されます。振り込み準備に向けて、これに印字された6桁の番号をAdsenseのHPで入力し、住所が正しいことを認証伝えておく必要があります。記載された通りに実施するとうまくいきませんので、この方法について解説します。

Adsenseからメールが届きます

収益が一定を超えると、Adsenseからメールが届きます。

Screenshot
AdSense でお知らせいただいたお支払い先住所宛に、個人識別番号(PIN)を記載したハガキを 11月 18, 2020 付で発送いたしました。
ハガキが届きましたら、このメールにある [住所を確認する] をクリックし、AdSense のホーム画面で PIN をご入力ください。あるいは AdSense アカウントにログインして、ホーム画面で直接、同じ手順を行っていただくこともできます。
重要: この PIN による住所確認が最初の郵送日から 4 か月以内に行われなかった場合、広告配信が停止されますのでご注意ください。

4ヶ月以内に行わないと広告停止されると書いてありますが、葉書が到着するまでに結構時間がかかるので不安になります。日本では結構時間がかかるようで心配は入りません。私は2つのサイトで実施したことがありますがそれぞれ3週間弱かかりました。直近のサイトについては、11月18日発送で自宅に届いたのは12月4日でした。18日かかったことになります。

PINの入力方法

葉書の通りにやってもうまくいきません。葉書では歯車のアカウントからPINを送信してくださいと書いてありますが、アカウント の画面にはPINのことなど書いておりませんPCでAdsenseにログインして、トップページから辿る必要があります。

Screenshot

赤丸で囲ったところから入力するようにしてください。

Angular(Ionic)で非同期通信を実装する

フロントエンドアプリケーションフレームワークであるAngular(Ionic)で非同期通信を実装する方法をサンプルアプリとして紹介します。動作確認用にPythonで作成したバックエンドコード付きです。

Angularの場合、非同期通信を実装するにはHttpClientModuleをapp.module.tsに追加して、tsファイルで呼び出して使うだけです。AngularはReactと異なりフルスタックなので、この辺の安心感はReactにはるかに勝ると思います。

Angular公式サイトの説明

ソースコードの素性

ソースコードの素性ですが、Ionic(Aunglar)のblankアプリから実装しました。なお、バックエンドのサンプルコードも作成しましたが、こちらはPythonのFlaskを使っています。Flaskでホスティングした http;//127.0.0.1:5000 のURLを叩いてデータを取ってくるアプリです。
GitHubにコードあげたのでそのまま動作確認できると思います。

環境

フロントエンド

Angular CLI: 10.0.5
Ionic CLI : 6.10.0
Node: 12.18.0

バックエンド

Python 3.7.3
Flask 1.1.2
Flask-Cors 3.0.9

コード

app.module.ts

app.module.tsに
import { HttpClientModule } from ‘@angular/common/http’;
を追加します。importするのも忘れずに

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule,HttpClientModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Ionicの場合、moduleファイルが複数あるけど、app.module.tsにだけ書いておけば問題ないです。

home.page.ts

こちらには
import { HttpClient } from ‘@angular/common/http’;
import { DomSanitizer } from ‘@angular/platform-browser’;
の2つを追加します。あとは処理を書くだけです。

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { DomSanitizer } from '@angular/platform-browser';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  unsafeImageUrl:any;
  imageUrl:any;

  constructor(
    private http: HttpClient,
    private sanitizer:DomSanitizer)
  {
    this.hello();
  }

  hello(){
    this.http.get('http://127.0.0.1:5000',{responseType:'blob'})
     .subscribe(
       (res:any) => {
         console.log(res);
         this.unsafeImageUrl = URL.createObjectURL(res);
         this.imageUrl = this.sanitizer.bypassSecurityTrustUrl(this.unsafeImageUrl);
       })
  }
}

home.page.html

htmlはを追加するだけです。

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Blank
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Blank</ion-title>
    </ion-toolbar>
  </ion-header>

  <div id="container">
    <img [src]=imageUrl>
    <strong>Ready to create an app?</strong>
    <p>Start with Ionic <a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">UI Components</a></p>
  </div>
</ion-content>

これでOK.anyを指定してしまっていますので書き方知っていたら教えてください。

バックエンド

バックエンドはこちら

# -*- coding: utf-8 -*-
import io
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from flask import Flask, send_file
from flask_cors import CORS
import json

app = Flask(__name__)
CORS(app)

@app.route('/',methods=["GET","POST"])
def hello():
    image = io.BytesIO()
    x = np.linspace(0, 10)
    y = np.sin(x)
    plt.plot(x, y)
    plt.savefig(image, format='png')
    image.seek(0)
    return send_file(image,
                     attachment_filename="image.png",
                     as_attachment=True)

if __name__ == "__main__":
    app.run(host='127.0.0.1',debug=True)

AngularでWebAPIからblob形式で返された画像データを取得する方法

フロントエンドアプリケーションフレームワークであるAngular(Ionic)を使って、非同期通信で画像を読み込む方法をサンプルアプリとして紹介します。動作確認用にPythonで作成したバックエンドコード付きです。

Angularの場合、非同期通信を実装するにはHttpClientModuleをapp.module.tsに追加して、tsファイルで呼び出して使うだけです。画像の場合は、さらにresponseTypeに’blob’形式を指定し、DomSanitizerでサニタイズして読み込みます。HTML側は<img [src]=imageUrl>で行けます。

AngularはReactと異なりフルスタックなので、この辺の安心感があります。

参考にしたサイト

stackOverflow

ソースコード

githubはこちら

解説

app.module.ts

app.module.tsに
import { HttpClientModule } from ‘@angular/common/http’;
を追加します。importするのも忘れずに

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule,HttpClientModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Ionicの場合、moduleファイルが複数あリますが、app.module.tsにだけ書いておけば問題ないです。

home.page.ts

こちらには
import { HttpClient } from ‘@angular/common/http’;
import { DomSanitizer } from ‘@angular/platform-browser’;
の2つを追加します。あとは処理を書くだけです。

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { DomSanitizer } from '@angular/platform-browser';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  unsafeImageUrl:any;
  imageUrl:any;

  constructor(
    private http: HttpClient,
    private sanitizer:DomSanitizer)
  {
    this.hello();
  }

  hello(){
    this.http.get('http://127.0.0.1:5000',{responseType:'blob'})
     .subscribe(
       (res:any) => {
         console.log(res);
         this.unsafeImageUrl = URL.createObjectURL(res);
         this.imageUrl = this.sanitizer.bypassSecurityTrustUrl(this.unsafeImageUrl);
       })
  }
}

これでOK。anyを指定してしまっていますので書き方知っていたら教えてください。

home.page.html

htmlはを追加するだけです。

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Blank
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Blank</ion-title>
    </ion-toolbar>
  </ion-header>

  <div id="container">
    <img [src]=imageUrl>
    <strong>Ready to create an app?</strong>
    <p>Start with Ionic <a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">UI Components</a></p>
  </div>
</ion-content>

バックエンド

バックエンドはこちら

# -*- coding: utf-8 -*-
import io
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from flask import Flask, send_file
from flask_cors import CORS
import json

app = Flask(__name__)
CORS(app)

@app.route('/',methods=["GET","POST"])
def hello():
    image = io.BytesIO()
    x = np.linspace(0, 10)
    y = np.sin(x)
    plt.plot(x, y)
    plt.savefig(image, format='png')
    image.seek(0)
    return send_file(image,
                     attachment_filename="image.png",
                     as_attachment=True)

if __name__ == "__main__":
    app.run(host='127.0.0.1',debug=True)