20200314のiOSに関する記事は3件です。

Vue.js、Capacitorでアプリ開発

ウェブ開発ノウハウを利用してスマートフォンアプリの開発を行うハイブリッドアプリについて、ソリューションの一つとしてCapacitor+Vue.jsを考えてみました。
メリットは、最初のプロジェクトの作成が非常に簡単にできることですので、プロジェクトの作成手順を紹介します。

Vue.jsプロジェクトの作成

https://qiita.com/jjjjjj/items/a43922631f1350bb9c65
を参照

Capacitorのインストール

以下を参考にしました。
https://capacitor.ionicframework.jp/docs/getting-started

$ npm install --save @capacitor/core @capacitor/cli

Capacitor初期設定

$ npx cap init

index.htmlファイルの場所を変更

capacitor.config.jsonのwebDirをwwwからdistに変更する。

プラットフォームのインストール

$ npx cap add ios
$ npx cap add android

Vueプロジェクトをネイティブにコピーする

$ npx cap copy ios
$ npx cap copy android

IDEを開いてビルド、実行する

$ npx cap open ios
$ npx cap open android
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SwiftUI】CustomViewを用いて不要なViewの再描画を避ける

SwiftUIは
宣言的に画面のコードを書くことができるため
UIKitと比べても可読性が高く扱いやすくなっていると感じています。

一方で
現時点では足りない機能(CollectionView的なものがないなど)や
動作がいまいちだなと感じる部分もあるかと思います。

その中の一つに
@Stateなどを用いた際に
値を変更する度に画面全体を再描画してしまうことで
変化のないViewも再描画してしまうという問題があります。

今回は
Viewが何を基準に行われ
必要ないViewの再描画をどう回避できるのかについて
下記の記事を参考に確認してみました。

https://medium.com/flawless-app-stories/swiftui-why-to-write-custom-views-for-better-performance-561962f1c268

サンプルコード

下記のコードを使用します。

struct SampleView: View {

    var texts = ["SwiftUI", "A new declarative UI framework", "Let us try to understand how SwiftUI renders its views"]

    @State var selected = 0
    @State var textFieldValue = ""
    @State var isSwitchOn = false

    var body: some View {
        VStack(spacing: 16) {
            TextField("Enter your name", text: $textFieldValue)
                .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
                .background(Color(UIColor.secondarySystemFill))
                .cornerRadius(4)

            Toggle(isOn: $isSwitchOn, label: {
                Text("A simple Switch")
            })

            Picker(selection: $selected, label: Text(""), content: {
                Text("Word").tag(0)
                Text("Phrase").tag(1)
                Text("Sentence").tag(2)
            }).pickerStyle(SegmentedPickerStyle())

            HStack {
                Text(texts[selected])
                Spacer()
                Button(action: {
                    print("just checking")
                }, label: {
                    Image(systemName: "plus.circle.fill")
                        .foregroundColor(.blue)
                })
            }
        }.padding()}
}

Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-14 at 02.29.46.png

検証内容

  1. 全てのViewにbreakpointを設定します。
  2. アプリを起動して、それぞれのViewを操作してみます。

ここからわかることは

TextFieldToggleButtonに操作に加えても
breakpointに止まりませんが
Pickerの値を変更すると
全てのbreakpointに止まります。
つまりbody全体が再描画されていることがわかります。

スクリーンショット 2020-03-14 2.32.39.png

原因

参考にしている記事によると
SwiftUIは親Viewの変数が(今回はselected)
子Viewに影響を与えるかどうか自体は把握しているものの
どのViewに影響を与えているかまでは把握していないため
body全体を再描画しているということだそうです。
(この記事を書いているのは2020/03/14です。)

実際に
Text(texts[selected])の部分を
Text("")に変更すると
body全体の再描画は発生しません。
(SwiftUIはPickerの変更がどこにも影響しないことがわかっているからです。)

解決策

CustomViewを定義することで
再描画されることを回避することができます。

例えば一番最初のTextFieldの部分を
CustomViewに切り出します。

struct SampleView: View {

    ...

    var body: some View {
        VStack(spacing: 16) {
            MyTextField(placholder: "Enter your name", value: $textFieldValue)
            ...
        }.padding()}
}

struct MyTextField: View {
    var placholder: String
    @Binding var value: String

    var body: some View {
        TextField(placholder, text: $value)
            .padding(EdgeInsets(top: 8, leading: 16,
                                bottom: 8, trailing: 16))
            .background(Color(UIColor.secondarySystemFill))
            .cornerRadius(4)
    }
}

これで上記の検証内容を実行してみます。

するとPickerの値を変更した際に
MyTextFieldのbreakpointには止まりません。

スクリーンショット 2020-03-14 2.56.52.png

なぜ?

SwiftUIはEquatableに適合しているViewを見て
bodyに変更があるかどうか見ており
Pickerに変更があっても
MyTextFieldbodyには変更がないため
再描画が発生しなかったということだそうです。

TextField自体はEquatableに適合していませんが

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct TextField<Label> : View where Label : View {

    /// Declares the content and behavior of this view.
    public var body: some View { get }

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    public typealias Body = some View
}

今回はTextFieldLabelText
TextEquatableに適合しているため
その差分を比較しているのではないかと思っています。

/// A view that displays one or more lines of read-only text.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct Text : Equatable {

    /// Creates an instance that displays `content` verbatim.
    @inlinable public init(verbatim content: String)

    /// Creates an instance that displays `content` verbatim.
    public init<S>(_ content: S) where S : StringProtocol

    /// Creates text that displays localized content identified by a key.
    ///
    /// - Parameters:
    ///     - key: The key for a string in the table identified by `tableName`.
    ///     - tableName: The name of the string table to search. If `nil`, uses
    ///       the table in `Localizable.strings`.
    ///     - bundle: The bundle containing the strings file. If `nil`, uses the
    ///       main `Bundle`.
    ///     - comment: Contextual information about this key-value pair.
    public init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)

    /// Returns a Boolean value indicating whether two values are equal.
    ///
    /// Equality is the inverse of inequality. For any values `a` and `b`,
    /// `a == b` implies that `a != b` is `false`.
    ///
    /// - Parameters:
    ///   - lhs: A value to compare.
    ///   - rhs: Another value to compare.
    public static func == (a: Text, b: Text) -> Bool
}

ここら辺いまいち不明瞭なので
もしご存知の方がいらっしゃいましたら
教えていただけるとうれしいです??‍♂️

変更の影響がある部分を切り出す

上記のMyTextFieldのようにToggleの部分を切り出すことで
TogglePickerの変更で再描画しなくなります。

しかし
これだとViewが追加されるたびに
CustomViewを追加していくことになります。
(可読性などを考えるとそれは良い選択かもしれませんが)

そうである場合にも
あるViewの変更が
関係のないViewにまで影響を及ぼすのは
あまり良いとは思われません。

そこでお互いに影響を与える部分(PickerHStack)を切り出してみます。

struct SampleView: View {
    ...
    var body: some View {
        VStack(spacing: 16) {

            // 確認のため最初の形に戻しています
            TextField("Enter your name", text: $textFieldValue)
                .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
                .background(Color(UIColor.secondarySystemFill))
                .cornerRadius(4)
            ...
            MyPickerTextField()
        }.padding()}
}

struct MyPickerTextField: View {
    var texts = ["SwiftUI", "A new declarative UI framework", "Let us try to understand how SwiftUI renders its views"]

    @State var selected = 0

    var body: some View {
        VStack(spacing: 16) {
            Picker(selection: $selected, label: Text(""), content: {
                Text("Word").tag(0)
                Text("Phrase").tag(1)
                Text("Sentence").tag(2)
            }).pickerStyle(SegmentedPickerStyle())

            HStack {
                Text(texts[selected])
                Spacer()
                Button(action: {
                    print("just checking")
                }, label: {
                    Image(systemName: "plus.circle.fill")
                        .foregroundColor(.blue)
                })
            }
        }
    }
}

ここで再び検証内容を実行してみます。

するとbreakpointはMyPickerTextFieldの中でしか止まらなくなり
親Viewのbody全体の再描画は発生しなくなったことがわかりました。

スクリーンショット 2020-03-14 3.42.24.png

結果として変更の影響範囲を必要な部分にだけに抑えることができました。

まとめ

SwiftUIの再描画について見てみました。

Viewが複雑になってくると
body全体の再描画を繰り返し行うことで
パフォーマンスに大きな影響を与える可能性も出てきます。

Viewの表示速度などで
気になる部分がある場合
こういう点を調査してみるのも何かの解決につながるかもしれませんね?

何か間違いなどございましたら
ご指摘いただけますとうれしいです??‍♂️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

truffle-config.js の最適化オプション runs の検証と Truffle v4 への懸念

はじめに

Truffle 環境の設定をする truffle-config.js ですが、最適化ブロックで runs という指定が使えるようです。

はて、この値はなんなんでしょう?

Solidity公式ページの説明からするに、solc においての runs は下記のようなものらしいです。

デフォルトでは、オプティマイザは、コントラクトがそのライフタイムにわたって200回呼び出されると仮定して、コントラクトを最適化します。 イニシャルコントラクトのデプロイのガス代をより安く、その後の関数実行時のガス代をより高くしたい場合は、--optimize-runs=1 と設定してください。 多くのトランザクションを想定していて、デフォルトより高いデプロイコストとアウトプットサイズを気にしないのであれば、 --optimize-runs を大きな数に設定してください。

どうやら runs の設定は、「値を小さくするとデプロイ時のガス代が下がって トランザクションのガス代が上がる、値を大きくするとデプロイ時のガス代が上がって トランザクションのガス代が下がる」とのこと。

Truffle 環境ではコンパイラとして solcjs が使われているらしいので solc の機能がそのまま使えるとは限らないのですが、物は試しです。どのような効果があるのかテストしてみましょう(※こちらがテストプロジェクトとなります)。

テスト方法

ガス消費の計測用に、構造体を1つ作成して動的配列に追加する関数を持つ、シンプルなコントラクトを準備します(※せっかくなので CryptoKittiesKitty 構造体を拝借しました)。

テストコード(クリックで開閉します)
pragma solidity 0.5.12;

contract CompileOpt5{
  // クリプトキティの構造体を拝借
  struct Kitty {
    uint256 genes;
    uint64 birthTime;
    uint64 cooldownEndBlock;
    uint32 matronId;
    uint32 sireId;
    uint32 siringWithId;
    uint16 cooldownIndex;
    uint16 generation;
  }

  // ニャンコの配列
  Kitty[] internal kitties;

  // 要素数の取得
  function getTotalKitties() public view returns( uint256 ){
    return kitties.length;
  }

  // ニャンコの追加
  function createKitty( uint256 _val256 ) public{
    Kitty memory _kitty = Kitty({
      genes: _val256,
      birthTime: uint64(_val256),
      cooldownEndBlock: uint64(_val256),
      matronId: uint32(_val256),
      sireId: uint32(_val256),
      siringWithId: uint32(_val256),
      cooldownIndex: uint16(_val256),
      generation: uint16(_val256)
    });

    kitties.push( _kitty );
  }
}

上記コードを、runs の値が [1][200][2000] の設定でコンパイルしたコントラクトを、それぞれデプロイした際のガス消費量と、関数にアクセスした際のガス消費量で比較します。ついでに、最適化を無効にした状態もテストしてみましょう。

Truffle v5.1.2 (solc-js v0.5.12) でのテスト

各コントラクトをデプロイし、createKitty 関数へ200回以上アクセスをしてみました(※ひょっとしたら runs で指定した回数のトランザクションを超えるとコストが高くなったりするのかと思ったので多めにテスト)。
で、結果としては、すべてのコントラクトにおいて初回トランザクションだけガス消費が低く、2回目以降は一律の消費という結果になりました。

・最適化 有り(runs=1)
 デプロイ時のガス消費187,711
 初回のガス消費45,551
 2回目以降のガス消費68,963

truffle-config.js(クリックで開閉します)
module.exports = {
  // 〜 省略 〜

  // 最適化有り:runs=1
  compilers: {
    solc: {
      version: "0.5.12",
      parser: "solcjs",
      settings: {
        optimizer: {
          enabled: true,
          runs: 1
        }
      }
    }
  }
}

・最適化 有り(runs=200)
 デプロイ時のガス消費190,327
 初回のガス消費45,503
 2回目以降のガス消費68,915

truffle-config.js(クリックで開閉します)
module.exports = {
  // 〜 省略 〜

  // 最適化有り:runs=200(デフォルト値)
  compilers: {
    solc: {
      version: "0.5.12",
      parser: "solcjs",
      settings: {
        optimizer: {
          enabled: true,
          runs: 200
        }
      }
    }
  }
}

・最適化 有り(runs=2000)
 デプロイ時のガス消費245,294
 初回のガス消費45,419
 2回目以降のガス消費68,831

truffle-config.js(クリックで開閉します)
module.exports = {
  // 〜 省略 〜

  // 最適化有り:runs=2000
  compilers: {
    solc: {
      version: "0.5.12",
      parser: "solcjs",
      settings: {
        optimizer: {
          enabled: true,
          runs: 2000
        }
      }
    }
  }
}

・最適化 無し
 デプロイ時のガス消費220,118
 初回のガス消費55,710
 2回目以降のガス消費79,122

truffle-config.js(クリックで開閉します)
module.exports = {
  // 〜 省略 〜

  // 最適化無し
  compilers: {
    solc: {
      version: "0.5.12",
      parser: "solcjs",
      settings: {
        optimizer: {
          enabled: false
        }
      }
    }
  }
}

さて、各値を比較してみるに、runs の値が小さいほどデプロイ時のガス消費が抑えられているようです。
一方で、runs の値による、トランザクションのコスト変動は微々たるものです。デフォルトの runs=200 に対して、トランザクションを犠牲するはずの runs=1 でも、 優遇されるはずの runs=2000 においても、恩恵を実感できるほどの差が出ませんでした。

また、最適化無しの状態にくらべると、2回目以降のトランザクションのガス消費が13%ほど改善されています。

まとめ

truffle-config.js において runs の値は、特に気にしなくても良さそうです。とりあえず最適化しておくだけで、ガス消費も多少収まってくれるみたいですし、DApp のリリース時に最適化し忘れさえしなければ問題はなさそうですね。

Truffle v4 での最適化はどうなるのか?

さて、ある意味ここからが本題かもしれません。
最適化の指定は Truffle v4 環境でもちゃんと効果があるのでしょうか?

Truffle 関連の情報、特に書籍等では、v4 系の内容を取り扱ったものが多い印象があります。また、truffle console の使い勝手が、v4v5 で結構変わっているので、あえて v4 環境を使い続けている方も少なくないと思います。折角なので、v4 系でもテストしてみましょう。

Truffle v4.1.15 (solcjs v0.4.25) でのテスト

では、v4 向けに名前とコンパイラのバージョンを調整したコントラクトを用意して、テストの開始です。

テストコード(クリックで開閉します)
pragma solidity 0.4.25;

contract CompileOpt4{
  // クリプトキティの構造体を拝借
  struct Kitty {
    uint256 genes;
    uint64 birthTime;
    uint64 cooldownEndBlock;
    uint32 matronId;
    uint32 sireId;
    uint32 siringWithId;
    uint16 cooldownIndex;
    uint16 generation;
  }

  // ニャンコの配列
  Kitty[] internal kitties;

  // 要素数の取得
  function getTotalKitties() public view returns( uint256 ){
    return kitties.length;
  }

  // ニャンコの追加
  function createKitty( uint256 _val256 ) public{
    Kitty memory _kitty = Kitty({
      genes: _val256,
      birthTime: uint64(_val256),
      cooldownEndBlock: uint64(_val256),
      matronId: uint32(_val256),
      sireId: uint32(_val256),
      siringWithId: uint32(_val256),
      cooldownIndex: uint16(_val256),
      generation: uint16(_val256)
    });

    kitties.push( _kitty );
  }
}

・最適化 有り(runs=1)
 デプロイ時のガス消費225,602
 初回のガス消費55,701
 2回目以降のガス消費79,113

truffle-config.js(クリックで開閉します)
module.exports = {
  // 〜 省略 〜

  // 最適化有り:runs=1
  compilers: {
    solc: {
      version: "0.4.25",
      parser: "solcjs",
      settings: {
        optimizer: {
          enabled: true,
          runs: 1
        }
      }
    }
  }
}

・最適化 有り(runs=200)
 デプロイ時のガス消費225,602
 初回のガス消費55,701
 2回目以降のガス消費79,113

truffle-config.js(クリックで開閉します)
module.exports = {
  // 〜 省略 〜

  // 最適化有り:runs=200(デフォルト値)
  compilers: {
    solc: {
      version: "0.4.25",
      parser: "solcjs",
      settings: {
        optimizer: {
          enabled: true,
          runs: 200
        }
      }
    }
  }
}

・最適化 有り(runs=2000)
 デプロイ時のガス消費225,602
 初回のガス消費55,701
 2回目以降のガス消費79,113

truffle-config.js(クリックで開閉します)
module.exports = {
  // 〜 省略 〜

  // 最適化有り:runs=2000
  compilers: {
    solc: {
      version: "0.4.25",
      parser: "solcjs",
      settings: {
        optimizer: {
          enabled: true,
          runs: 2000
        }
      }
    }
  }
}

・最適化 無し
 デプロイ時のガス消費225,602
 初回のガス消費55,701
 2回目以降のガス消費79,113

truffle-config.js(クリックで開閉します)
module.exports = {
  // 〜 省略 〜

  // 最適化無し
  compilers: {
    solc: {
      version: "0.4.25",
      parser: "solcjs",
      settings: {
        optimizer: {
          enabled: false
        }
      }
    }
  }
}

やや、すべての設定において、ガス消費量が完全に同じ値となりました…。

最初はコンパイル指定をミスったと思ったのですが、イーサスキャンでバイトコードを眺めてみると、各コントラクトともバイナリレベルで若干の差が出ています。仮に、同じバイナリデータであれば、イーサスキャンで表示した際、重複コントラクト扱いされるはずなので、個別に表示できている時点でコントラクト間で差分があるのは間違いなさそうです。

それなのに、イーサスキャン上でベリファイコードを登録しようとすると、「Optimization Enabled: No」でないと受け付けてくれません。

まとめると、最適化の設定によりバイナリデータに差が出ているにも関わらず、イーサスキャン上では最適化が認識されていないようなのです。

そして最大の問題が「最適化有りでも無しでも、すべてのコントラクトのガス消費量が同じになっている」という事実。

この結果が、私の環境や手順の間違いによるものであればよいのですが、仮に、 Truffle v4 環境が最適化の指定をコントラクトに反映してくれないのであれば、由々しき事態です。

もし Truffle v4 をお使いで Dapp のリリースを念頭に置いている方がいらっしゃるのであれば、お早めに、お使いの環境で最適化が機能するかテストしてみることをお勧めします。リリース間際になって「最適化が効かない!?」なんて状況は考えるだに恐ろしいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む