20200329のiOSに関する記事は10件です。

MVVMについて簡単なまとめ

Model

アプリケーションで使うデータの基本的な振る舞いやそれに関するロジックを保持

具体的には、

  • データ構造の表現
  • Web APIとのやりとり
  • ローカルデータベースなどへの保存

ViewModel

  • ModelとView、ViewController層の仲介役
  • Modelからデータを受け取り、それらをUIに反映できるような形で出力
  • View、ViewControllerからユーザーのアクション情報を受け取り、Modelに伝え、Modelからデータを受け取りUIに反映できるような形で出力

View、ViewController

View

  • UIの表示
  • データを表示するようなUIの場合、Controllerからデータを受け取り、UIに反映
  • ユーザーインタラクションの認知し、必要に応じてUIを更新する。
  • もしくは、そのユーザーインタラクションをした結果、何かアクションをしたいとき、ユーザーインタラクションの情報をControllerに伝達する。
  • View層はデータ構造に関する一切のロジックを保持しない。

ViewController

  • ViewとViewModelの仲介役を行う。
  • ViewModelから受け取った出力をViewに反映させてUIを更新
  • ユーザーのアクションをViewModelに伝え、ViewModelから新しい出力を受け取り、Viewに反映させてUIを更新
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutter】バグ解決: Bad state: Stream has already been listened to.

今回の件のGitHubはこちら

https://github.com/Tetsukick/flutter_BLoc/commit/76748e912be5896f8e998a5a50244fcb77d659b6

現象

StreamControllerを使ってBLoCパターンでstatement管理をしていたが、TabBarViewを追加して、
Tabを切り替えて、再度同じタブに復帰した際に画面の表示が崩れるようになりました。

その際のerrorがこちら。

Bad state: Stream has already been listened to.

解決策

StreamControllerではなく、BehaviorSubjectを使用することで解消

元のソースコード

エラー解消前
import 'dart:async';

class CounterBloc {
  // input
  final _actionController = StreamController<bool>();
  Sink<void> get changeCountAction => _actionController.sink;

  //output
  final _countController = StreamController<int>();
  Stream<int> get count => _countController.stream;

  int _count = 0;

  CounterBloc() {
    _actionController.stream.listen((isPlus) {
      if (isPlus) {
        _count++;
      } else {
        _count--;
      }
      _countController.sink.add(_count);
    });
  }

  void dispose() {
    _actionController.close();
    _countController.close();
  }
}
エラー解消後
import 'dart:async';
import 'package:rxdart/rxdart.dart';

class CounterBloc {
  // input
  final _actionController = BehaviorSubject<bool>();
  Sink<void> get changeCountAction => _actionController.sink;

  //output
  final _countController = BehaviorSubject<int>();
  Stream<int> get count => _countController.stream;

  int _count = 0;

  CounterBloc() {
    _actionController.stream.listen((isPlus) {
      if (isPlus) {
        _count++;
      } else {
        _count--;
      }
      _countController.sink.add(_count);
    });
  }

  void dispose() {
    _actionController.close();
    _countController.close();
  }
}

その他

勉強不足でStreamControllerBehaviorSubjectの違いをまだ理解できておりません。
ご存知の方いましたら、教えていただけますと幸甚です。

参考記事

https://stackoverflow.com/questions/51396769/flutter-bad-state-stream-has-already-been-listened-to

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

[iOS]Tesseract OCRで認識させた日本語の情報を取得する

※ これは 2018/4/8,16 に個人ブログへ投稿した内容をまとめて転記したものです。

Google が出資しているという文字認識 API の iOS 用ラッパー、 Tesseract OCR iOS を使って文字認識をさせてみた備忘録です。
環境: macOS HighSierra, MacBook Pro 2016, XCode9.3

準備

本体のリポジトリはここ。ただし、 CocoaPods から読ませるので、直接 Clone しない。
CocoaPods が入ってない場合は、まず CocoaPods をインストール
終わったら下記の記事を参考にインストールしていく。
Xcode 7.0 + Swift2 でTesseract-OCR-iOSを使う(追記あり) - 今日も微速転進

動かしてみる

テストコードは公式 Wiki の方を使用した。シミュレータで動かす分にはこちらのほうがシンプルで良い。
上の記事でも言っているとおり、 BridgingHeader はいらないので惑わされないように1
Tesseract OCR iOS の中身は Tesseract3.03 らしく、最新2の学習データ (tessdata) は使えない。
Github で公開されている 3.04 か、 Wiki からそれ以前のものを選んで使う。

英語版 (eng.tessdata) は 3.04 そのままでも動くが、日本語版 (jpn.tessdata) は 3.04 を使うとエラーが出る。

read_params_file: parameter not found: allow_blob_division

これを解決するために、下記に従って学習データ内のパラメータを変更するのだが、このときビルドに使うソースは3.04を使うこと
4.00 だと加工後の tessdata を読み込んでくれない。

それから、 ./configure にパラメータをつけると私の環境では失敗してしまったので、パラメータをなくしたら上手くいった。
これはこの記事を読みながら事前に

brew link icu4c --force

しておいたからかも。

./autogen.sh
./configure
make
sudo make install
make training
sudo make training-install

なお、一度 4.00 をインストールしてしまったときは、 make したディレクトリで

sudo make uninstall
sudo make training-uninstall

したらアンインストールできた…気がする。

日本語を認識させてみる

日本語を認識させて結果の情報を取得してみました。

ソース

Swift 初心者なので、 API リファレンスを見ながら、 Xcode に言われるがまま書いたソースはこちら

結果

入力
20180416230659.png

出力
20180416234126.png

あえてちょっと斜めにしてみましたが、いい感じに認識していますね!

技術的なメモ

認識結果とその表示

let box = block.boundingBox(atImageOf: actualImageRect.size)

Tesseract では元画像のサイズを 1×1 としたときの相対位置で対象 (文字) の座標を認識しているようです。
なので、デバイスの画面に収まるように画像を自動レイアウトした場合は、レイアウト後のサイズを取得し、それを atImageOf に指定して変換してもらう必要があります。
レイアウト後の画像の実サイズを取得する Extension、UIImageView.ContentClippingRect は下記より。
How to find an aspect fit image’s size inside an image view

Extensionって最高ですね…!

文字のまとまり

let blocks = tesseract.recognizedBlocks(by: G8PageIteratorLevel.symbol)

G8PageIteratorLevel で認識結果のまとまりの大きさを指定できます。
意味深な定数名が付いていますが、もちろん日本語の形態素解析をして云々なんてしていないので、一通り試してみました。

block: ブロック。段落を複数認識したら、そのまとまりということかな?
20180416234131.png

paragraph: 段落
20180416234231.png

textline: 行
20180416234249.png

word: 語。やはり英語前提でスペース区切りで認識されていそう。
20180416234301.png

symbol: 文字
20180416234316.png


  1. リンクが通らなくて1時間くらい悩んだ。 

  2. 2018/4/8 時点で master は 4.00 

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

iOSのWebViewをMacでデバッグする

はじめに

以前Webブラウザー作っていた時、一番困ったのが「Safariで動くのにこのブラウザーでは動かない!!」というお問い合わせでした。
そんな時、お世話になった"Webインスペクタ"の使い方を記事にすることで、誰か救われたらいいなーと思い書くことにしました。

必要なもの

  • Mac端末(当時Yosemiteでやってたので、それなりに古くても大丈夫だと思います)
  • iOS端末(当時iOS 8でやってた記憶があるので、それ以降ならできます)
  • MacとiOSを接続する設備(MacによってはUSB typeCしかないので変換アダプタだったり必要かと)

手順

1.iOSの[設定]アプリでSafariのWebインスペクタを有効化
Webインスペクタ有効.png

2.MacのSafariで「開発」メニューを表示する
開発メニュー有効.png

3.MacとiOS端末を接続して、Mac Safariの開発メニューを表示すると・・・
開発メニュー表示.png

こんな感じで表示されます!!!
今回はSafariを例に出していますが、UIWebView/WKWebViewでコンテンツ表示しているアプリはリストアップされます。その際はDebugビルドしたアプリでご利用くださいね!(Releaseビルドしたアプリは表示されません)

例えば、qiita.comを選んで、Webインスペクタの要素タブでヘッダーの部分を選択すると、iOS端末側で該当箇所にスモークがかかり、示してくれます。
Webインスペクタ例.png

その他にも、

  • デバッガタブでJavaScriptにブレークポイント張ってステップ実行
  • コンソールログ確認
  • ネットワークタブで、発生した通信の確認
  • ストレージタブで記録されてるCookieの確認

などなど、いろいろなことができるようになります。

なぜか表示されない・・・そんな時

  • 開発メニュー内に接続したiOS端末が表示されない
    →MacのSafariを開いた状態でiOS端末を接続した時、うまく行かないケースが私の手元で何度か見られたので、その時はMacのSafariを再起動してみてください。私の手元では解消しました。
    (iOS端末の再接続では解決せず)

  • 開発メニューで接続したiOS端末は表示されたが、アプリが表示されない
    →Debugビルドしたアプリを使ってますか?(Releaseビルドしたアプリは表示されません)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ARKit 3.5のScene Reconstructionサンプルのコードを読む

ARKit 3.5とLiDAR搭載の新型iPad Proが出ましたね。これらを試せるAppleの公式サンプル「Visualizing and Interacting with a Reconstructed Scene」のソースコードを読んでみたメモです。

どんな感じのサンプルかはこちらのツイートの動画がよくわかります。

Scene Understandingの設定

ARView.Environment.SceneUnderstanding構造体のoptions(型はARView.Environment.SceneUnderstanding.Options)に.occlusion.physicsを追加することでオクルージョンと物理計算を有効化。

arView.environment.sceneUnderstanding.options = []

// Turn on occlusion from the scene reconstruction's mesh.
arView.environment.sceneUnderstanding.options.insert(.occlusion)

// Turn on physics for the scene reconstruction's mesh.
arView.environment.sceneUnderstanding.options.insert(.physics)

SceneUnderstandingオブジェクトはARViewenvironmentプロパティ(型はARView.Environment)が持つsceneUnderstandingプロパティにセットする。

https://developer.apple.com/documentation/realitykit/arview/environment/sceneunderstanding/options

デバッグ用にメッシュを可視化

公式サンプルではメッシュが深度に応じて色分けされた見事なビジュアライゼーションが実現されている。

どうやっているのかというと、ARViewdebugOptionsプロパティに.showSceneUnderstandingを追加するだけ。

arView.debugOptions.insert(.showSceneUnderstanding)

https://developer.apple.com/documentation/realitykit/arview/debugoptions/3521397-showsceneunderstanding

ちなみにARSCNViewではこれができない。

Scene Reconstructionの有効化

ARViewの自動設定をオフにし、ARWorldTrackingConfigurationsceneReconstructionプロパティに.meshWithClassificationを指定する。デフォルト以外に.none.mesh(メッシュのClassificationを行わない)がある。

arView.automaticallyConfigureSession = false
let configuration = ARWorldTrackingConfiguration()
configuration.sceneReconstruction = .meshWithClassification

configuration.environmentTexturing = .automatic
arView.session.run(configuration)

https://developer.apple.com/documentation/arkit/arworldtrackingconfiguration/3521376-scenereconstruction

タップでClassification結果を可視化

Classificationというのはそのメッシュが壁なのか床なのかテーブルなのか、という分類を示す情報で、ARMeshClassificationというenumで定義されている。

public enum ARMeshClassification : Int {
   case none = 0
   case wall = 1
   case floor = 2
   case ceiling = 3
   case table = 4
   case seat = 5
   case window = 6
   case door = 7
}

https://developer.apple.com/documentation/arkit/armeshclassification

公式サンプルでは、画面(ARView)タップでraycastによる(.estimatedPlaneとの)当たり判定を行い、classificationの判定結果をテキスト表示している。

if let result = arView.raycast(from: tapLocation, allowing: .estimatedPlane, alignment: .any).first {
   let resultAnchor = AnchorEntity(world: result.worldTransform)
   ...
}

ここらへんのコードが実はおもしろくて、ここまで簡単にScene Reconstructionやメッシュの可視化ができるのでClassificationの結果もARMeshAnchorのプロパティから取り出して終わりでしょ、と思いきや、そんなプロパティはなく、意外とめんどくさい処理をやっている。

https://developer.apple.com/documentation/arkit/armeshanchor

ARMeshAnchorからARMeshClassificationを得る

具体的にはARMeshAnchorgeometryプロパティに入っているARMeshGeometryオブジェクトのclassificationプロパティから取り出しているのだが、これがまだARMeshClassification型ではなくて、ARGeometrySource型なのである。

var vertices: ARGeometrySource

https://developer.apple.com/documentation/arkit/armeshgeometry

で、ARGeometrySourceというのが割とめんどくさいクラスで、そのデータをMTLBufferというMetalのバッファに保持している。

open class ARGeometrySource : NSObject, NSSecureCoding {


   /**
    A Metal buffer containing per-vector data for the source.
    */
   open var buffer: MTLBuffer { get }


   ...
}

https://developer.apple.com/documentation/arkit/argeometrysource

サンプルではこのバッファをGPUではなくCPUでほじくり、とりだした生の数値からARMeshClassificationを初期化している。

extension ARMeshGeometry {
    ...

    /// To get the mesh's classification, the sample app parses the classification's raw data and instantiates an
    /// `ARMeshClassification` object. For efficiency, ARKit stores classifications in a Metal buffer in `ARMeshGeometry`.
    func classificationOf(faceWithIndex index: Int) -> ARMeshClassification {
        guard let classification = classification else { return .none }
        assert(classification.format == MTLVertexFormat.uchar, "Expected one unsigned char (one byte) per classification")
        let classificationPointer = classification.buffer.contents().advanced(by: classification.offset + (classification.stride * index))
        let classificationValue = Int(classificationPointer.assumingMemoryBound(to: CUnsignedChar.self).pointee)
        return ARMeshClassification(rawValue: classificationValue) ?? .none
    }

タップ位置に近いメッシュのfaceを抽出

前項の処理で説明してないことがあって、classificationOf(faceWithIndex:)メソッドではfaceのindexを引数に渡し、そのfaceについてのclassifiation結果を取り出している。

メッシュの中にたくさんある中で、タップした位置に近いfaceを取り出してそのclassification結果を可視化しているわけだ。

その「タップした位置に近い(5cm以内)場所にあるfaceを取り出す」実装はこうなっている。

for anchor in meshAnchors {
    for index in 0..<anchor.geometry.faces.count {
        // Get the center of the face so that we can compare it to the given location.
        let geometricCenterOfFace = anchor.geometry.centerOf(faceWithIndex: index)

        // Convert the face's center to world coordinates.
        var centerLocalTransform = matrix_identity_float4x4
        centerLocalTransform.columns.3 = SIMD4<Float>(geometricCenterOfFace.0, geometricCenterOfFace.1, geometricCenterOfFace.2, 1)
        let centerWorldPosition = (anchor.transform * centerLocalTransform).position

        // We're interested in a classification that is sufficiently close to the given location––within 5 cm.
        let distanceToFace = distance(centerWorldPosition, location)
        if distanceToFace <= 0.05 {
            ...
        }
    }
}

この計算をするためには、ARMeshGeometryverticesプロパティとfacesプロパティを使用する必要があり、これまたARGeometrySource型。Metalバッファから数値を読み出すために、ARMeshGeometryのextensionとして次のようなメソッドが実装されている。

extension ARMeshGeometry {
    func vertex(at index: UInt32) -> (Float, Float, Float) {
        assert(vertices.format == MTLVertexFormat.float3, "Expected three floats (twelve bytes) per vertex.")
        let vertexPointer = vertices.buffer.contents().advanced(by: vertices.offset + (vertices.stride * Int(index)))
        let vertex = vertexPointer.assumingMemoryBound(to: (Float, Float, Float).self).pointee
        return vertex
    }

    ...

    func vertexIndicesOf(faceWithIndex faceIndex: Int) -> [UInt32] {
        assert(faces.bytesPerIndex == MemoryLayout<UInt32>.size, "Expected one UInt32 (four bytes) per vertex index")
        let vertexCountPerFace = faces.indexCountPerPrimitive
        let vertexIndicesPointer = faces.buffer.contents()
        var vertexIndices = [UInt32]()
        vertexIndices.reserveCapacity(vertexCountPerFace)
        for vertexOffset in 0..<vertexCountPerFace {
            let vertexIndexPointer = vertexIndicesPointer.advanced(by: (faceIndex * vertexCountPerFace + vertexOffset) * MemoryLayout<UInt32>.size)
            vertexIndices.append(vertexIndexPointer.assumingMemoryBound(to: UInt32.self).pointee)
        }
        return vertexIndices
    }

    func verticesOf(faceWithIndex index: Int) -> [(Float, Float, Float)] {
        let vertexIndices = vertexIndicesOf(faceWithIndex: index)
        let vertices = vertexIndices.map { vertex(at: $0) }
        return vertices
    }

    func centerOf(faceWithIndex index: Int) -> (Float, Float, Float) {
        let vertices = verticesOf(faceWithIndex: index)
        let sum = vertices.reduce((0, 0, 0)) { ($0.0 + $1.0, $0.1 + $1.1, $0.2 + $1.2) }
        let geometricCenter = (sum.0 / 3, sum.1 / 3, sum.2 / 3)
        return geometricCenter
    }
}

この実装はたぶんめちゃくちゃ多くの人がコピーして使うと思う。サンプルの鑑。

関連

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

[悲報]Xcode 11.4 でナビゲーションバーのタイトルの色が黒に固定される(*条件あり)

それはXcodeをアップデートした直後のことでした・・・
俺氏「ん?!ナビゲーションタイトルの文字が黒にしかならんやんけ?」
スクリーンショット 2020-03-29 2.11.31.png

発生条件

  • navigation controllerのBar Tintを変更している場合

上記場合に、Title Colorを変更しても、タイトルの色は黒固定になっちゃいます。

Bar Tintをデフォルトにしとけばタイトルカラーを変更できます。
スクリーンショット 2020-03-29 2.10.29.png

デフォルトカラー以外では変更できないから超不便?

解決法

  1. 多分Xcode11.4のバグ、アップデートを待ちましょう←
  2. Xcode11.3.1にダウングレードする(多分これがいい(小並感))
  3. おーばーふろーで無理やり変更してる人もいた

 

 それではみなさん楽しいバグライフを!?

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

【悲報】Xcode 11.4 でナビゲーションバーのタイトルの色が黒に固定される(*条件あり)

それはXcodeをアップデートした直後のことでした・・・
俺氏「ん?!ナビゲーションタイトルの文字が黒にしかならんやんけ?」
スクリーンショット 2020-03-29 2.11.31.png

発生条件

  • storyboardでnavigation controllerのBar Tintを変更している場合

上記場合に、Title Colorを変更しても、タイトルの色は黒固定になっちゃいます。

Bar Tintをデフォルトにしとけばタイトルカラーを変更できます。
スクリーンショット 2020-03-29 2.10.29.png

デフォルトカラー以外では変更できないから超不便?

解決法

  1. 多分Xcode11.4のバグ、アップデートを待ちましょう←
  2. Xcode11.3.1にダウングレードする(多分これがいい(小並感))
  3. おーばーふろーで無理やり変更してる人もいた

 

 それではみなさん楽しいバグライフを!?

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

【初心者向け】【まとめ】iOSアプリを作ろうとしている方がまず見るべきQiitaの記事をまとめてみました【Swift】

iOSアプリのためのまとめ記事が欲しかった

iOSエンジニアになって早8ヶ月。

最近ようやく慣れてきたものの、初めは実装するのにとっても時間がかかりました。

というのは、実装についてまとまったオールインワンの記事があまりなかったことがもしかしたら原因なのかもと思っています。(自分でもっと勉強しろって話かもしれないですが…)

なので、今回は、自身が開発をしていく上で参考にした記事をまとめてみたいと思います。

これからiOS開発をする方に参考になればと思っています。

参考になるかもしれないソースコード

この記事を作っていくにあたって、以下の記事を参考にして実装したものを載せています。

これを見ながら記事と共に勉強していただければ幸いです。

https://github.com/taichi6930/iOSAppBase/tree/master

早速見るべき記事

ライフサイクル

iOSアプリがどのように動いているか、といった感じです。
作る際にここがわかっていないと「??」となりまくってしまいます(自分がそうでした)。

アプリ作成前に一度読んでおくと良いと思います。

iOSアプリのライフサイクル
https://qiita.com/KenNagami/items/766d5f95940c76a8c3cd

UIViewControllerのライフサイクル
https://qiita.com/motokiee/items/0ca628b4cc74c8c5599d

AppDelegate,UIViewController,UIViewのライフサイクル/iOS/Swift
https://qiita.com/kayo311/items/4710c4ac02a191652a96

iosアプリ ViewControllerのライフサイクル
https://qiita.com/usutan/items/71760df10e8523166bab

ページ遷移について

ページ間での移動についてです。
ここも全然分からなかったので、最初に読んでおいた方がいいと思います。
(ページ遷移しないと、ただのワンページアプリになってしまうのでw)

同じ/異なるStoryboardでの画面遷移
https://qiita.com/kedarui/items/97b5cc1410d9c61933d5

Swiftのページ遷移【Navigation Controller】
https://qiita.com/ryu1_f/items/4a0e452e94c9ba609220

TableView

画像のようにcellが縦方向に続いていくViewです。

自身のソースコードではセルの生成とタップ時の処理を記載しました。
(例が少ないので足していく予定です…)

スクリーンショット 2020-03-29 13.27.16.png

参考になった方々の記事です↓

SwiftでTableViewを使ってみよう
https://qiita.com/pe-ta/items/cafa8e20029047993025

UITableViewの使い方 【Swift4.2 , Xcode10】
https://qiita.com/abouch/items/3617ce37c4dd86932365

UITableViewのデリゲートメソッドまとめ
https://qiita.com/kagemiku/items/22b74010365723c5c4fe

CollectionView

画像のようにcellが横方向に続いていくViewです。

自身のソースコードでは、セルの生成とタップ時の処理を記載しています。

TableViewと同様に作成すれば良かったので、まずはTableViewを作成してみてください!

スクリーンショット 2020-03-29 13.30.56.png

WebView

アプリ内でWebページが見れるViewです。

今回はWKWebViewを使用して作成しました。

cookie処理などは入れていないので、今後行っていければいいなと思っています。

スクリーンショット 2020-03-29 13.34.23.png

参考になった方々の記事です↓

WebKit View(WKWebView) を実装
https://qiita.com/MdRk/items/34912e7ba43568f15905

WKWebViewについてのまとめ
https://qiita.com/s_emoto/items/dc3d61626155f5cf83e7
(まだ実装出来ていないので今後していきたい…)

生体認証

端末によってはFaceIDやTouchIDでログインすることができるアプリがあります。

端末によってtouchIDだったりFaceIDだったり変化させています。

そこまで難しくなかったので実装してみました。

スクリーンショット 2020-03-29 13.50.56.png

参考になった方の記事↓

【iOS 11】LocalAuthenticationでFace IDとTouch IDの認証を実装する
https://qiita.com/MilanistaDev/items/b0cd432290d18f336766

Alert表示

画像を見れば明らかですがアラート表示についてです。

スクリーンショット 2020-03-29 13.51.14.png

参考になった方々の記事↓

【Swift】アラートを表示する(Alert/ActionSheet)
https://qiita.com/funacchi/items/b76e62eb82fc8d788da5

最後に

この記事は自分のために作ったみたいなところがあります。
今後もメモのような感じで残せたらと思います。

何か不足だったり指摘だったり、こんなの載せて欲しいというのがあったら是非気軽に言っていただきたいです!

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

Vue.js×TypeScriptでのテキストコピー(iOS対応)

Vue.js×TypeScriptで「ワンタップでテキストをコピーする」ボタンを作ったらハマりポイントがたくさんありました。
生jsやjQueryでの解決策はたくさん見つかりましたが、Vue.js×TSは見つからなかったのでメモです。

やりたいこと

フォームに文字入力した時に、飾り文字を追加した文章を出力して、ワンタップでコピーできるようにする。

recipe formatter qiita.gif

完成したコード

    <template lang="pug">
      .CopyText
        button(@click.prevent="copyTexts")
          span.copy-message クリップボードにコピー
        .formatted-text
          span.recipe
            span#copy-text {{formattedTitle}}<br>
    </template>
    <script lang="ts">
    import { Recipe } from "../../components/molecules/RecipeTitle.vue";
    import Vue, { PropType } from "vue";
    export default Vue.extend({
      props: {
        recipe: {
          type: Object as PropType<Recipe>,
          default: {}
        }
      },
      computed: {
        //inputで入力した内容ではなく、ここでフォーマットしたテキストがコピー対象
        formattedTitle(): string {
          return this.recipe.title ? `【${this.recipe.title}】` : "";
        }
      },
      methods: {
        //iOSの判定
        isIOS() {
          const agent = window.navigator.userAgent;
          return agent.indexOf("iPhone") != -1 || agent.indexOf("iPad") != -1;
        },
        //コピー
        copyTexts(): void {
          if (this.isIOS()) {
            //iOSの場合
            const doc: HTMLInputElement = document.getElementById(
              "copy-text"
            ) as HTMLInputElement;
            const selected = window.getSelection();
            const range = document.createRange();
            range.selectNodeContents(doc);
            selected!.removeAllRanges();
            selected!.addRange(range);
            document.execCommand("copy");
          } else {
            //それ以外
            const formattedText = `${this.formattedTitle}`;
            navigator.clipboard.writeText(formattedText);
          }
        }
      }
    });
    </script>

参考:Javascriptによるコピー機能(クロスブラウザ対応)

ハマった部分の解説

iOSでのコピー

jsでコピーをしようと思ったらnavigator.clipboardを使用するのが一般的かと思います。

ユーザーエージェントなどの情報を扱うNavigatorインターフェイスにclipboardプロパティを追加して、writeText()メソッドを呼び出すことで、テキストがコピーできます。

    navigator.clipboard.writeText(text);

しかし、このnavigator.clipboardはiOSの10以降、textareaなど一部のタグからしかコピーできないなど仕様が変わっています。

今回はinputに入力した文字ではなく、フォーマットをかけたテキストをコピーするため、まさにこの条件に引っかかり、iOSのsafariとchromeで動作しませんでした。

参考: Copy to clipboard using Javascript in iOS

そのため、iOSとそれ以外でコピーの処理を変える必要があります。

iOSかどうかの判定

  isIOS() {
    const agent = window.navigator.userAgent;
    return agent.indexOf("iPhone") != -1 || agent.indexOf("iPad") != -1 || agent.indexOf("iPod") != -1;
        }

navigator.userAgentを使います。

今回はブラウザではなくiOSかどうかだけ判定するので、上記のようにしてみました。

iOS用のコピー

iOSのコピーは、コピーしたい文章を選択→コピーの実行という流れで行います。

 const doc = document.getElementById("copy-text");
 const selected = window.getSelection();

 const range = document.createRange();
 range.selectNodeContents(doc);

 selected.removeAllRanges();
 selected.addRange(range);

 document.execCommand("copy");

ユーザーはワンタップするだけですが、内部の動作はマウスなどで文章選択→コピーをするのと同じです。

2行目のwindow.getSelectionはselectionオブジェクトを取得するものです。

selectionオブジェクトは、ユーザーが選択した範囲のDOMに関する情報を持つことができます。

3行目のcreateRangeはdocument中のテキストやノードに関する情報を持つrangeオブジェクトを作成します。

rangeオブジェクトを作成しただけでは何も情報を持っていないため、4行目のrange.selectNodeContents(doc)で、最初に取得した要素を渡します。

5行目は2行目に取得したselectionオブジェクトが現在持っているrangeに関する情報をあらかじめ削除する処理です。文章がすでに選択されてselectionオブジェクトに情報が設定されている場合、この後の処理が無視されるので先に削除してしまいます。

これにより、6行目でselectionオブジェクトに作成したrangeオブジェクトを追加することができます。

最後のdocument.execCommand()はhtmlのdocumentオブジェクトを操作するコマンドを実行します。copyは選択範囲をクリップボードにコピーするコマンドです。

これでiOSでもテキストコピーができるようになりました!

参考: memo: テキスト全選択の JavaScript コードが動かなくなったので修正した

TSで"Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Node'"

上記のコードはType Scriptを使うと以下の部分でエラーを吐きます。

  //Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Node'
  const doc = document.getElementById("copy-text");

document.getElementById() はHTMLElement型もしくはnullを返しますが、nullを返す可能性があるとTSがエラーを出すようです。

そのため、返り値がHTMLElement型であることを明示的に示します。

  const doc: HTMLInputElement = document.getElementById("copy-text") as HTMLInputElement;

参考: Typescript で TS2322 の対処方法

TSでObject is possibly 'null'エラー

rangeの削除、追加部分でも型エラーが出ます。

  selected.removeAllRanges(); //Object is possibly 'null'
  selected.addRange(range); //Object is possibly 'null'

これはselectedの部分がnullの可能性があることで出るエラーです。

そこで、!をつけて、selectedがnullでもundefinedでもないことを推論させます。ただ、この方法はESlintで"Forbidden non-null assertion"の警告が出ます。

参考: 非nullアサーション演算子(Non-Null Assertion Operator)

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

【Xcode】CocoaPods で複数ライブラリを導入したらハマった話

はじめに

個々のサンプルプロジェクトでは問題なく動いていたのに、1つのプロジェクトにライブラリをまとめたらハマってしまったので、原因と対策をメモしておきます。

発端

イーサリアム用のライブラリと、AWS 用のライブラリのサンプルをそれぞれ試して導入の目処がたったので、下記の Podfile にて、開発中のプロジェクトへ導入しました。

Podfile
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.2'

target 'AppEnv' do
    use_frameworks!
    pod 'web3swift'
    pod 'AWSAppSync'
    pod 'AWSS3'
end

で、サンプルのコードを開発プロジェクトへ取り込んでコンパイルしたところ、web3swift 関連コードでエラーが発生。「あれ?」と思ってフレームワークを見てみると、なぜか頭が大文字にリネーム(?)された Web3swift に差し変わっていてポカンとなりました。

原因

pod install のログをよくみてみたらヒントがありました。

$ pod install
Analyzing dependencies
Downloading dependencies
Installing AWSAppSync (3.1.0)
Installing AWSCore (2.13.1)
Installing AWSS3 (2.13.1)
…省略…
Installing Starscream (3.0.6)
Installing SwiftRLP (1.2)
Installing secp256k1_swift (1.0.3)
Installing web3swift (2.1.2)
Generating Pods project
Integrating client project

web3swift のバージョンが (2.1.2) です。

サンプルでは最新の (2.2.1) だったので、古いバージョンがインストールされたことになります。(※ web3swiftWeb3swift にリネームされたように見えたのは、(2.1)(2.2) のタイミングで、たまたまフレームワーク名が変更されていたからのようでした)

で、2つのサンプルプロジェクトの Podfile.lock を見比べたところ、Starscream が両ライブラリから参照されており、このライブラリのバージョンを AWS 側へあわせるために、web3swift のバージョンが下げられてしまっていたようです。

回避策

コンパイルを通すため、web3swift の最新版がインストールされるように試した結果が下記となります。

まず、インストール済みの関連ファイル(Pods フォルダ、Podfile.lock, プロジェクトのワークスペース)を一旦削除し、下記の内容で web3swift だけをインストールしました。

Podfile(AWSの2ライブラリはコメントアウト)
target 'AppEnv' do
    use_frameworks!
        pod 'web3swift'
#        pod 'AWSAppSync'
#        pod 'AWSS3'
end

$ pod install
Analyzing dependencies
Downloading dependencies
Installing BigInt (4.0.0)
Installing CryptoSwift (1.0.0)
Installing PromiseKit (6.8.5)
Installing Starscream (3.1.1)
Installing secp256k1.c (0.1.2)
Installing web3swift (2.2.1)
Generating Pods project
Integrating client project

これで、最新版の、(2.2.1) がインストールされました。
つづいて、AWS の2ライブラリを追加でインストール。

Podfile(AWSのコメントを外して有効に)
target 'AppEnv' do
    use_frameworks!
        pod 'web3swift'
        pod 'AWSAppSync'
        pod 'AWSS3'
end

$ pod install
Analyzing dependencies
Downloading dependencies
Installing AWSAppSync (2.15.0)
Installing AWSCore (2.12.7)
Installing AWSS3 (2.12.7)
Installing ReachabilitySwift (4.3.1)
Installing SQLite.swift (0.12.2)
Generating Pods project
Integrating client project

Starscreamweb3swift に変化がなく、AWS 関連のライブラリだけがインストールされていることがわかります。

で、コンパイルも無事通りました。

AWS側のバージョンが若干古くなってしまいましたが、テストした結果、欲しい機能への影響はなかったので一件落着です。

まとめ

異なるライブラリを CocoaPods で一気に導入する際は、ライブラリ間の相性により、全てのライブラリを最新バージョンにできないことがあるようです。もし、期待するバージョンがインストールされなかった場合は、優先度の高いライブラリ側へ合わせて、インストールの順番の調整することで妥協点を探りましょう。

補足

後から導入するライブラリを pod update でインストールするとどうなるでしょうか?

今回の例だと、こんな感じになります。

$ pod update
Analyzing dependencies
Downloading dependencies
Downloading dependencies
Installing AWSAppSync (3.1.0)
Installing AWSCore (2.13.1)
Installing AWSS3 (2.13.1)
…省略…
Installing Starscream 3.0.6 (was 3.1.1)
Installing SwiftRLP (1.2)
Installing secp256k1_swift (1.0.3)
Installing web3swift 2.1.2 (was 2.2.1)
Removing secp256k1.c
Generating Pods project
Integrating client project

pod update により、導入済みのライブラリが「アップデート」され、Starscream(3.1.1)(3.0.6) に、 web3swift(2.2.1)(2.1.2) に、それぞれデグレードされています(※そのかわり、AWSAppSyncAWSS3 が最新になっています)。

今回のように、既存のライブラリのバージョンを変えられたくない場合、 pod update の利用は控えておきましょう。

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