- 投稿日:2021-01-20T23:03:07+09:00
SwiftyTesseractとOpenCVを入れたら「duplicate symbol」しまくった話
タイトルの通り。
duplicate symbol
が出まくって面を食らったので解決策を備忘録として残しておきます。環境情報
名前 バージョン Xcode 12.3 (12C33) Swift 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28) SwiftyTesseract 4.0.0 CMake 3.19.3 Python 2.7.16 xcode-select 2384 OpenCV 4.5.1 SwiftyTesseractとOpenCVを同じプロジェクトで使う
二つを同じプロジェクトで使うための手順を示します。
SwiftyTesseractをプロジェクトに追加する
2021年1月19日現在、SwiftyTesseractはSwift Package Managerからしか追加できませんので、Swift Package Managerから追加します。
Xcode上で「File > Swift Packages > Add Package Dependency...」と選択して、
入力欄に以下のGitHubのURLを入力します。https://github.com/SwiftyTesseract/SwiftyTesseractOpenCVをプロジェクトに追加する
iOS用にビルドする
GitHubからOpenCVを入手して、
platform/ios/
にある以下のコマンドでiOS用にビルドします。$ ./platform/ios/build_framework.py <output_dir>
ビルドしたframeworkファイルをプロジェクトに追加する
プロジェクトファイルを選んで、左ペインの「TARGETS」からアプリを選択します。
そして「Build Phases > Link Binary With Libraries (n items)」と選択して、ビルドで出力された「opencv2.framework」をドラッグ&ドロップなどで追加します。
Other Linker Flagsに
-all_load
を追加するプロジェクトファイルを選んで、左ペインの「PROJECT」からアプリを選択します。
「Build Settings」を選択して、「Linking > Other Linker Flags」に-all_load
を追加します。
libc++.tbdをプロジェクトに追加する
同じくプロジェクトファイルの「TARGETS」からアプリを選択します。
そして「General > Frameworks, Libraries, and Embedded Content」と選択して、左下の「+」ボタンを押します。
検索窓に「libc++」と入力すると「libc++.tbd」が出てくると思うのでそれを追加します。
試しにビルドしてみるが…
試しにビルドしてもこの時点ではうまくいきません。エラーを見ると「509 duplicate symbols for architecture x86_64(シミュレータなら×86_64、実機ならarm64)」というものすごい数の競合が起きていることがわかります。
解決方法ですが、opencv2.frameworkから競合が起きているファイルを削除することで解消できます。競合が起きているファイル(.oファイル)をまとめる
エラーを頼りに競合が起きてるファイルをまとめます。執筆者の環境では以下のファイル内で競合が起きていました。
jutils.o pngrio.o pngwio.o jdcoefct.o jaricom.o jdmainct.o jdcolor.o pngerror.o jfdctint.o pngget.o pngrutil.o jmemmgr.o jccoefct.o jcmaster.o jidctflt.o pngpread.o jcprepct.o jerror.o pngset.o jctrans.o jcparam.o jdinput.o pngwtran.o jdarith.o jdsample.o jcomapi.o png.o pngrtran.o jchuff.o jddctmgr.o jidctfst.o jfdctfst.o pngwrite.o jfdctflt.o jdatasrc.o jdapistd.o pngmem.o jdapimin.o pngwutil.o jcdctmgr.o jdtrans.o jdhuff.o jdmaster.o jcapistd.o jdatadst.o jquant2.o jdmerge.o jmemnobs.o jccolor.o jdmarker.o jdpostct.o jcmarker.o jidctred.o jcarith.o jcmainct.o pngtrans.o jcinit.o jcsample.o jquant1.o pngread.o jidctint.o jcapimin.o上記リストは、後でコマンドに引数として食わせるときに使いやすいようテキストファイルなどに書いておきます。
$ echo "jutils.o pngrio.o ..." > duplicate_lib_listOpenCVをアーキテクチャごとに分解する
lipoコマンドを使って、フレームワークフォルダ内のopencv2をアーキテクチャごとに分解します。
まず何が含まれているか以下コマンドで確認して、
$ cd opencv2.framework $ lipo -info opencv2 Architectures in the fat file: opencv2 are: armv7 armv7s i386 x86_64 arm64以下コマンドでバラします。
$ lipo -thin armv7 opencv2 -output opencv2_armv7 $ lipo -thin armv7s opencv2 -output opencv2_armv7s $ lipo -thin i386 opencv2 -output opencv2_i386 $ lipo -thin x86_64 opencv2 -output opencv2_x86_64 $ lipo -thin arm64 opencv2 -output opencv2_arm64競合が起きているファイルを削除する
バラしたファイルに対して以下コマンドを実行して、競合が起きているファイルをそれぞれから削除します。
$ cat duplicate_lib_list | xargs ar -dv opencv2_armv7 $ cat duplicate_lib_list | xargs ar -dv opencv2_armv7s $ cat duplicate_lib_list | xargs ar -dv opencv2_i386 $ cat duplicate_lib_list | xargs ar -dv opencv2_x86_64 $ cat duplicate_lib_list | xargs ar -dv opencv2_arm64バラしたファイルから再生成する
以下コマンドを実行してopencv2を再生成しましょう。
$ lipo -create opencv2_arm64 opencv2_armv7 opencv2_armv7s opencv2_i386 opencv2_x86_64 -output opencv2あとは途中作ったテキストファイルなどの余計なファイルを削除してリビルドしましょう。
競合が解消されてビルドできるようになっているはずです!参考記事
- 投稿日:2021-01-20T21:06:04+09:00
今更だけどUIViewの座標変換系メソッドを理解する(Swift)
はじめに
ごくまれに
UIView
の convert 系のメソッドを使って座標を変換したいときがありますがいつもどっちに何指定するのかな?と忘れるのでまとめておきます。View 準備
下記のような構成で座標変換をしてみます。
ScrollView を ViewController の View と同じサイズで配置してその中に
contentView
(緑色)を幅は ScrollView と同じ高さは 1200 pt で配置しています(StackView は左端のメモリ用です)。contentView
の中に縦横 120 pt のtargetView
(白色)をcontentView
の Top 900pt、横は中央寄せで配置して、そのtargetView
(白色)の中心にボタンを配置しています。座標変換
座標変換には
UIView
に下記のメソッドが用意されており、これを使って変換します。どちらのメソッドも対象の View は同一 Window にある必要があります。引数の
view
がnil
の場合は Window の座標系に変換されます。convert(_: to:)
point
はレシーバーの座標系を指定し、view
には変換したい座標系の View を指定します。func convert(_ point: CGPoint, to view: UIView?) -> CGPoint // ex. aView.convert(CGPoint.zero, to: bView)例だと
aView
の左上の座標をbView
の座標系に変換しています。convert(_: from:)
point
はview
の座標系を指定し、レシーバーの座標系に変換します。func convert(_ point: CGPoint, from view: UIView?) -> CGPoint // ex. aView.convert(CGPoint.zero, from: bView)例だと
bView
の左上の座標をaView
の座標系に変換しています。実践
ボタン押下時に
targetView
の座標系からscrollView
と ViewController のview
の座標系に変換してみます!@IBAction private func convertPoints(_ sender: Any) { // targetViewの右上の座標をscrollViewの座標系に変換 let p1 = targetView.convert(CGPoint.zero, to: scrollView) print("p1: \(p1)") // p1: (127.5, 900.0) // targetViewの右上の座標をviewの座標系に変換 let p2 = targetView.convert(CGPoint.zero, to: view) print("p2: \(p2)") // p2: (127.5, 367.0) * scrollViewのoffsetで変わる // targetViewの右上の座標をscrollViewの座標系に変換 let p3 = scrollView.convert(CGPoint.zero, from: targetView) print("p3: \(p3)") // p3: (127.5, 900.0) // targetViewの右上の座標をviewの座標系に変換 let p4 = view.convert(CGPoint.zero, from: targetView) print("p4: \(p4)") // p4: (127.5, 367.0) * scrollViewのoffsetで変わる }上記を見てもらうと分かる通り、p1 と p3、p2 と p4 は同じ結果になります。
これを使ってボタン押下時に
targetView
が画面外にある場合にスクロールのオフセットを調節してみます。@IBAction private func convertPoints(_ sender: Any) { // targetViewの左下の座標をviewの座標系に変換 let p1 = targetView.convert(CGPoint(x: 0, y: targetView.frame.height), to: view) // targetViewの左下がviewの範囲内か確認 if !view.frame.contains(p1) { // targetViewが画面内におさまるようスクロール let p2 = targetView.convert(CGPoint(x: 0, y: targetView.frame.height), to: scrollView) scrollView.setContentOffset(.init(x: 0, y: p2.y - scrollView.frame.height), animated: true) } }こんな感じです
おわりに
to と from の意味を考えたらわかるんですが
point
はどの座標系のやついれればいいんだっけ?とよく迷います。。。もうこれで迷わない!!(たぶん)
ちなみに Rect の変換メソッドもあります。
* convert(_: to:)
* convert(_: from:)SwiftUI になるとこういうの使うこともなくなるのかな
参考
- 投稿日:2021-01-20T17:37:58+09:00
【Swift5】iOSの設定アプリに遷移するための処理と注意点
iOSの設定アプリに遷移させるための処理
guard let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) else { return } UIApplication.shared.open(url, options: [:], completionHandler: nil)注意点
①設定アプリに遷移するときの挙動
「設定アプリ」内にアプリ固有の設定項目がある場合は、アプリ固有の設定画面へ遷移します。
詳細な挙動については、下記の記事が参考になります。
UIApplicationOpenSettingsURLStringから設定アプリを開いたときの挙動調査
②設定アプリの特定の画面に遷移させる処理(
App-Prefs:root=
)を使うとリジェクトされる設定アプリの特定の画面に遷移させるとリジェクトされた例があるらしいので注意が必要です。
詳しくは下記記事に書かれています。
App-Prefs:root= を使っていたため2.5.1リジェクトされたときの対処法
- 投稿日:2021-01-20T14:58:13+09:00
【Swift】 SpriteKit 入門 ②
SpriteKitの使い方を解説します。
SpriteKit 入門 ① で画像を表示させることができたので、続いて画像をドラッグして動かせるようにしていきたいと思います。
(環境:Xcode12.1 Swift5.3)画像をドラッグで移動させる
前回作成したMyScene.swiftファイルにコードを追加していきましょう。タッチされた画面上の位置は
touchesMoved()
で取得することができます。また画像(SpriteNode)をコードで操作できるようにMyScene.sksファイルにてNameを設定しておきます(ここではSampleImage)。
self.childNode(withName: )
のwithNameプロパティには設定したNameを指定します。MyScene.swiftimport SpriteKit class MyScene: SKScene { var image: SKSpriteNode! override func sceneDidLoad() { //MyScene.sksからSpriteNodeを作成 self.image = self.childNode(withName: "SampleImage") as! SKSpriteNode? self.image.size = CGSize(width: 200, height: 200); } // ドラッグした際に呼ばれる override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { for t in touches { self.touchMoved(toPoint: t.location(in: self)) } } // 画像の位置を更新 func touchMoved(toPoint pos : CGPoint) { self.image.position = pos } }これでシミュレーターを起動してみると、ドラッグで画像を移動することができるようになっているのが確認できると思います。
改良①
ドラッグで画像を移動させることはできるようになったのですが、現状だと画像でない部分をドラッグするとその位置に画像が瞬間移動してしまいます。画像でない部分をドラッグした場合には移動しないようにするには
func touchMoved(toPoint pos : CGPoint)
の中身を以下のように変更します。MyScene.swift// 画像の位置を更新(改良①) func touchMoved(toPoint pos : CGPoint) { if (pos.x < self.image.position.x + self.image.size.width/2 && pos.x > self.image.position.x - self.image.size.width/2)&&(pos.y < self.image.position.y + self.image.size.height/2 && pos.x > self.image.position.y - self.image.size.height/2) { self.image.position = pos } }これで画像部分をドラッグしたときのみ移動するようになりました。
改良②
画像が小さい場合、改良①の処理だとうまくドラッグさせることができないおそれがあります。また、ドラッグ中の画像が指で隠れてしまうこともあるでしょう。この問題を解決するために、ドラッグされた方向に相対的に画像を移動できるようにしてみましょう。注意点としては、指が画面から離れた際に
beforeDragPosition = nil
とするのを忘れないようにしましょう。これを忘れると、再びドラッグする際に前回のドラッグ終了地点の情報が残ったままになり、画像が瞬間移動してしまいます。MyScene.swift//改良② import SpriteKit class MyScene: SKScene { var image: SKSpriteNode! var beforeDragPosition: CGPoint? override func sceneDidLoad() { //MyScene.sksからSpriteNodeを作成 self.image = self.childNode(withName: "SampleImage") as! SKSpriteNode? self.image.size = CGSize(width: 200, height: 200); } // ドラッグした際に呼ばれる override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { for t in touches { self.touchMoved(toPoint: t.location(in: self)) } } //ドラッグが終了したとき(画面から指が離れたとき)に呼ばれる override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { for t in touches { self.touchUp(atPoint: t.location(in: self)) } } // 画像の位置を更新 func touchMoved(toPoint pos : CGPoint) { if let p1 = self.beforeDragPosition { self.image.position.x += pos.x - p1.x self.image.position.y += pos.y - p1.y } self.beforeDragPosition = pos } func touchUp(atPoint pos : CGPoint){ self.beforeDragPosition = nil } }この方法はドラッグする画像が1つの場合のみ有効です。2つ以上の画像をドラッグする必要がある場合には改良①の方法を採用すると良いでしょう。また、改良②の問題点として、画像が画面外にあっても移動できてしまうためどこに画像があるのか分からなくなるおそれがあります。これを解消するためには
touchMoved()
の中身を次のようにコードを追加すると良いでしょう。MyScene.swift// 画像の位置を更新 func touchMoved(toPoint pos : CGPoint) { if let p1 = self.beforeDragPosition { self.image.position.x += pos.x - p1.x self.image.position.y += pos.y - p1.y //左右に飛び出ないようにする if !(self.image.position.x < self.size.width/2 && self.image.position.x > -self.size.width/2) { self.image.position.x -= pos.x - p1.x } //上下に飛び出ないようにする if !(self.image.position.y < self.size.height/2 && self.image.position.y > -self.size.height/2) { self.image.position.y -= pos.y - p1.y } } self.beforeDragPosition = pos }まとめ
さて今回は画像をドラッグして動かせるようにすることができました。次回はSKShapeNodeを使って画像(自機)から弾を発射するようにしていきましょう。