node dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.xxx.dylibエラー

node6がEOLになったのでnode8にあげてfastlaneでionicプロジェクトをビルドしようとしたら以下のエラー発生

dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.62.dylib
  Referenced from: /usr/local/bin/node
  Reason: image not found
  (省略)
  Command PhaseScriptExecution failed with a nonzero exit code

nodeをインストールしなおせばなおる記事をいくつか見たが解決せず...半日。brewでインストールしていたnodeをndenvでインストールしたらビルドできた。ndenvじゃなくてもbrewを使わなければいいのだろうけど...

以上

carthage SWIFT_VERSION '5.0' is unsupported, supported versions are: 3.0, 4.0, 4.2.エラー

タイトルのエラーの原因と解決策のメモ
今回は「Smile-Lock」というパスコードの入力画面を簡単に実装できるライブラリを使おうとして発生。プロジェクトの都合でxcodeのバージョンは10.1でSwift4.2を使用。

Cartfile

github "recruit-lifestyle/Smile-Lock"

carthage update

$ carthage update Smile-Lock --platform iOS
*** Checking out Smile-Lock at "3.0.8"
*** xcodebuild output can be found in /var/folders/f8/3j706yps6t97pj4d6b49f5200000gn/T/carthage-xcodebuild.4fxFE3.log
*** Building scheme "SmileLock" in SmileLock-Example.xcworkspace
Build Failed
        Task failed with exit code 65:
(省略)

This usually indicates that project itself failed to compile. Please check the xcodebuild log for more details: /var/folders/f8/3j706yps6t97pj4d6b49f5200000gn/T/carthage-xcodebuild.4fxFE3.log

エラーメッセージのログを見てみる

$ cat /var/folders/f8/3j706yps6t97pj4d6b49f5200000gn/T/carthage-xcodebuild.4fxFE3.log

(省略)

note: Using new build system
note: Planning build
note: Constructing build description
Build system information
error: SWIFT_VERSION '5.0' is unsupported, supported versions are: 3.0, 4.0, 4.2. (in target 'SmileLock')

Build system information
error: SWIFT_VERSION '5.0' is unsupported, supported versions are: 3.0, 4.0, 4.2. (in target 'SmileLock')

** ARCHIVE FAILED **

Swift5なんて使ってないのにと思ってたら勘違いだった。
Carthage/Checkouts/Smile-Lock/SmileLock-Example/SmileLock-Example.xcworkspace の 「Swift Language Version」がSwift5.0が指定されてた

Cartfile

github "recruit-lifestyle/Smile-Lock" "3.0.7"

Swift5対応される前のバージョンを指定してcarthage updateしたらうまくいった。
けどXcodeとSwiftのバージョンそろそろあげないとな...以上です

android ListViewでContextMenuを使う

今回やりたかったことは、ListViewの各行を長押ししたらコンテキストメニューを表示して選択されたメニューによって処理を行うということ

http://kimagureneet.hatenablog.com/entry/2019/02/19/031844
ListViewはこちらに書いたものを使用する

registerForContextMenuでListViewを設定するとonCreateContextMenuが呼ばれるのでこれをオーバーライドしてメニューアイテムを表示するだけだと思っていたらonCreateContextMenuが呼ばれずにハマった...解決策はListViewの行アイテムにisLongClickable = trueをセットすることでした

MainActivity.kt

class MainActivity : AppCompatActivity() {

  ・・・

  override fun onCreate(savedInstanceState: Bundle?) {
    ・・・

    // ListViewにContextMenuを登録
    registerForContextMenu(listView)
  }

  override fun onCreateContextMenu(menu: ContextMenu?, v: View?, menuInfo: ContextMenu.ContextMenuInfo?) {
    super.onCreateContextMenu(menu, v, menuInfo)

    val info = menuInfo as AdapterView.AdapterContextMenuInfo
    menuInflater.inflate(R.menu.comment_context, menu)
  }

  override fun onContextItemSelected(item: MenuItem?): Boolean {
    val menuInfo = item!!.menuInfo as AdapterView.AdapterContextMenuInfo

    // メニューを表示した行番号を取得
    // menuInfo.position

    when(item.itemId) {
      R.id.edit -> {
        // 編集
      }
      R.id.delete -> {
        // 削除
      }
    }

    return true
  }
}

res/menu/user_context.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android" >
  <item android:id="@+id/edit"
        android:title="編集"/>
  <item android:id="@+id/delete"
        android:title="削除"/>
</menu>

Adapter/UserAdapter.kt

class UserAdapter(val context: Context, val users: List<User>): BaseAdapter() {

  ・・・

  override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
    val view = layoutInflater.inflate(R.layout.user_item, parent, false)

    ・・・

    // ★長押しを有効化
    view.isLongClickable = true

    return view
  }
}

以上です

android ステータスバーの文字色を変更する

今回やりたかったことはステータスバーの色を白にして、文字色を黒にしたかったが文字色の変え方が見つからない...

https://stackoverflow.com/questions/30464234/android-lollipop-set-status-bar-text-color
こちらによるとAndroid6以降なら「android:windowLightStatusBar」を指定することでステータスバーの色によって文字色をいい感じに変更してくれるみたい

f:id:yoppy0066:20190513001119p:plain:w180

Android5以下でのやり方がわからなかったので、Androidのバージョンを確認して5以下だったらステータスバーの色を黒っぽくすることにした...

res/values/styles.xml

<resources xmlns:tools="http://schemas.android.com/tools" tools:locale="es">
  <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    ・・・
    <item name="android:statusBarColor">@color/colorPrimaryDark</item>
    <item name="android:windowLightStatusBar" tools:targetApi="M">true</item>
  </style>
  ・・・
</resources>

MainActivity.kt

class MainActivity : AppCompatActivity() {

  ・・・

  // ステータスバーの色を黒っぽく
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
    this.window.statusBarColor = ContextCompat.getColor(this, R.color.abc_primary_text_material_light)
  }
}

以上です

ionic cordova-plugin-ionic-webviewプラグインでアプリインストール直後だけFileプラグインでのファイル処理が動作しなかった原因

誰にも必要とされないと思われる情報

cordova-plugin-ionic-webviewプラグインのバージョンを2.4から4.01にバージョンアップした際になぜかアプリインストール直後だけファイルプラグインによるファイルの読み込みがうまくいかなかった。2回目の起動以降はうまくいくのに、1からプロジェクトから作り直したらうまくいくのに。。各プラグインのバージョンとnpmでインストールされるパッケージのバージョンもすべてそろえたのになぜか...ちなみに試したコードは以下だが、readAsTextのSuccessもFailedもログ出力されない...

export class MyApp {

  constructor(
    platform: Platform,
    statusBar: StatusBar,
    public file: File
  ) {
    platform.ready().then(() => {

      let dir = cordova.file.dataDirectory;
      let fileName = 'test';

      let json = JSON.stringify({ session:'xxxxxxxxx1' });
      this.file.writeFile(dir, fileName, json, { replace: true }).then((result) => {
        console.log('Success writeFile');

        this.file.checkFile(dir, fileName).then((result) => {
          console.log('Success checkFile');

          this.file.readAsText(dir, fileName).then((result) => {
            console.log('Success readAsText');
          }).catch((err) => {
            console.log('Failed readAsText');
          });

        }).catch((err) => {
          console.log('Failed checkFile');
        });

      }).catch((err) => {
          console.log('Failed checkFile');
      });

    });
  }
}

結論

src/index.htmlでサードパーティのライブラリを読み込んでいてそれが、ionic関連のjsより先に読み込んでいたのが原因でした。これを1番最後に読み込むように修正した動いた。インストール直後だけうまくいかないのは不明だが...こんなので1日つぶした

ionicでのハイブリッドアプリ開発のメリット・デメリットについてそろそろまとめとく

3年くらい前からionicを使い始めて良いところと悪いところが少しつつわかってきたのでメモしておく

メリッド

学習コストが低い & 開発リソースが確保しやすい

swiftやjavaなどでの実装となるとそれぞれのプログラム言語の学習する必要がある。言語自体の学習コストよりもiosandroidそれぞれのUIKitというか概念を勉強する必要がありやはり慣れるのにそれなりに時間がかかると思う。その点、ionicだとWebの技術(HTML+CSS+Javascript) がある程度わかっていればなんとなく作れてしまう。またアプリ作り始めてもすごい時間がかかったりすると挫折しがちだがなんとなくでも動くものが作れればあとは慣れなのでこれはメリットかと思う。また、ネイティブだとちょっとしたレイアウト調整や文言変更でもそこそこ経験が必要になるがHTML+CSSだと直感的にもわかりやすく作業できる人が多いのでそういう意味でリソースが確保しやすいと思う。

Webベースなので頑張ればデザインはなんとかなる

CSSは自由度も高い方だと思うのでけっこうなんとかなる。CSSはimportantが量産される可能性はあるが...

1ソース(ほぼ)でiOSAndroidの両OS対応のアプリが作れる

なんといってもこれが1番のメリットだと思う。iOSAndroid別々に作ると各OSで仕様の違うものができあがるなんてこともあると思う。もちろんネイティブの機能を使う必要がある場面はあるかもしれないが、あくまでその部分だけswiftとjavaで書くだけでアプリ全体は1ソースで作れるのでやはり工数的にはだいぶ少なくなる。

デメリット

開発環境のバージョンアップが速い

これはメリットというか良いことだとは思うがあえて辛いところもあるということでデメリットの方に書いた。ionic1から2に上がったとき、angular1も2への変更がありangular1と2は全く別物となっていてバージョンアップ=アプリの作り直し ということとなってしまいやはり辛かった。ionicに加えてcordova自体のバージョンが上がるとプロジェクトのファイル構成も変わってしまい、これらを新しい環境にあわせて継続的に対応させ続けていくとなるのでやはり大変。長く続くアプリでは開発環境のバージョンアップも視野にいれないとならない。Swiftもけっこうバージョンアップしてるがangularやcordovaの変更に伴う修正作業と比べたらだいぶ楽なんじゃないかと、、

WebViewベースのガワアプリの開発が難しい

cordova自体の仕組み上の問題だが、アプリの一部にWebViewを組み込むようなアプリの実装が難しい。例えば画面の下タブはionicのコンポーネントを使い、タブを選択するとWebViewが切り替わるようななんてことなさそうなアプリだがこれが難しい。ionicだけで何ができて何が難しいかというのは判断はある程度慣れだとは思うがこういうのが出てくると大変

XcodeAndroid Studioの操作があると手間

例えば info.plistに値を設定だけとかならionicのconfig.xmlに追記すればいいので問題ない。しかしxcode上でプロジェクトのある設定をいじる必要があったりするとionic platformを作り直すたびにその設定をし直す必要があるので漏れの恐れがある。プラグインやフックスプリトで解決できることがほとんどだとは思うが、xcodeならすぐできるところもそういうのがいくつもあるとやっぱり疲れる

ionicの大きな仕様変更にふりまわされた

OSSなので当然考えられることだが。例えば旧バージョンのionicで開発したアプリを新しいバージョンのionicでアップデートした際に、保存していたローカルストレージやクッキーが引き継げないなんてことがあった。また、内部的にはブラウザ(safariまたはchrome)のエンジンを使っているのでこれらに大きな変更があった場合にもろに影響受ける。ソース自体の修正も必要になる場合もあったりするとやっぱり大変。

結論

何年もメンテし続ける必要があるようなアプリだと最終的にかかる工数もネイティブの方が少なくなるんじゃないかと思う。また、失敗が許されないような案件もネイティブの方が確実だと思う。逆にプロタイプのアプリや、画面数が少ないので開発環境のバージョンアップがあってもサクッと作り直せるアプリだとionicがいいのかなと思う。あと今あげたようなデメリットを許容してくれることもけっこうあると思うのでそういう場合もionicで作るのはありだと思う。ionicもけっこうたつのでだいぶ成熟してきた印象はある。flutterには期待してるけど正直まだ勉強中なのでわからん。

結論、どっちがいいかはケースバイケース。けど趣味でアプリ作るなら自分はionicかも、、以上です

ios WKWebViewでWebViewアプリの作り方まとめとく

iOSで簡単なWebViewアプリの作るときに必要そうなことまとめておく。以下のiOS
android WebViewアプリの作り方まとめとく - とりあえずphpとか

WKWebViewを作ってサイトを表示する

ViewController

import UIKit
import WebKit

class ViewController: UIViewController {

    var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        webView = WKWebView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
        view.addSubview(webView)

        let request = URLRequest(url: URL(string: "https://www.google.com")!)
        webView.load(request)
    }
}

クッキーの値を取得する

takuyaokamoto.hateblo.jp

スワイプでブラウザバック

webView.allowsBackForwardNavigationGestures = true

メールや電話の起動リンクに対応

class ViewController: UIViewController, WKNavigationDelegate {
    ・・・
    override func viewDidLoad() {
       ・・・
        webView.navigationDelegate = self
       ・・・
    }

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

        guard let url = navigationAction.request.url else {
            return
        }

        if navigationAction.navigationType == .linkActivated {
            if url.scheme == "mailto" || url.scheme == "tel" {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
                decisionHandler(.cancel)
                return
            }
        }

        decisionHandler(.allow)
    }
}

SSLエラーやhttpとhttpsの混在ページを読み込む

info.plist

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

ViewController

class ViewController: UIViewController, WKNavigationDelegate {
    ・・・
    func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
        if challenge.protectionSpace.host == "example.com" && challenge.protectionSpace.serverTrust != nil {
            let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
            completionHandler(.useCredential, credential)
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }
}

Pull To Refreshを実装する

class ViewController: UIViewController, WKNavigationDelegate {

    var webView: WKWebView!
    var refreshControll: UIRefreshControl!

    override func viewDidLoad() {
        ・・・
        refreshControll = UIRefreshControl()
        self.webView.scrollView.refreshControl = refreshControll
        refreshControll.addTarget(self, action: #selector(ViewController.refresh(sender:)), for: .valueChanged)
        ・・・
    }


    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        self.refreshControll.endRefreshing()
    }

    @objc func refresh(sender: UIRefreshControl) {
        guard let url = webView.url else {
            return
        }
        webView.load(URLRequest(url: url))
    }
}

他のサイトは表示したくない

class ViewController: UIViewController, WKNavigationDelegate {
    ・・・
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        ・・・
        if url.host != "example.com" {
            decisionHandler(.cancel)
            return
        }

        decisionHandler(.allow)
    }
}

全ソース

import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

    var webView: WKWebView!
    var refreshControll: UIRefreshControl!

    override func viewDidLoad() {
        super.viewDidLoad()

        webView = WKWebView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
        webView.navigationDelegate = self
        webView.allowsBackForwardNavigationGestures = true
        refreshControll = UIRefreshControl()
        self.webView.scrollView.refreshControl = refreshControll
        refreshControll.addTarget(self, action: #selector(ViewController.refresh(sender:)), for: .valueChanged)
        view.addSubview(webView)

        let url = "https://example.com"
        let request = URLRequest(url: URL(string: url)!)
        webView.load(request)
    }

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

        guard let url = navigationAction.request.url else {
            return
        }

        if navigationAction.navigationType == .linkActivated {
            if url.scheme == "mailto" || url.scheme == "tel" {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
                decisionHandler(.cancel)
                return
            }
        }

        if url.host != "example.com" {
            decisionHandler(.cancel)
            return
        }

        decisionHandler(.allow)
    }

    func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
        if challenge.protectionSpace.host == "example.com" && challenge.protectionSpace.serverTrust != nil {
            let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
            completionHandler(.useCredential, credential)
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        self.refreshControll.endRefreshing()
    }

    @objc func refresh(sender: UIRefreshControl) {
        guard let url = webView.url else {
            return
        }
        webView.load(URLRequest(url: url))
    }
}

androidiosもそこそこ手間でした、、以上です