20210120のiOSに関する記事は4件です。

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...」と選択して、1.png
入力欄に以下のGitHubのURLを入力します。

https://github.com/SwiftyTesseract/SwiftyTesseract

2.png

OpenCVをプロジェクトに追加する

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」をドラッグ&ドロップなどで追加します。
3.png

Other Linker Flagsに-all_loadを追加する

プロジェクトファイルを選んで、左ペインの「PROJECT」からアプリを選択します。
「Build Settings」を選択して、「Linking > Other Linker Flags」に-all_loadを追加します。
6.png

libc++.tbdをプロジェクトに追加する

同じくプロジェクトファイルの「TARGETS」からアプリを選択します。
そして「General > Frameworks, Libraries, and Embedded Content」と選択して、左下の「+」ボタンを押します。
4.png
検索窓に「libc++」と入力すると「libc++.tbd」が出てくると思うのでそれを追加します。
5.png

試しにビルドしてみるが…

試しにビルドしてもこの時点ではうまくいきません。エラーを見ると「509 duplicate symbols for architecture x86_64(シミュレータなら×86_64、実機ならarm64)」というものすごい数の競合が起きていることがわかります。
7.png
解決方法ですが、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_list

OpenCVをアーキテクチャごとに分解する

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

あとは途中作ったテキストファイルなどの余計なファイルを削除してリビルドしましょう。
競合が解消されてビルドできるようになっているはずです!

参考記事

  1. OpenCV が Objective-C / Swift で使えるようになります & 使ってみた - Qiita
  2. Xcodeでビルドした時の”duplicate symbols”エラーを回避する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更だけどUIViewの座標変換系メソッドを理解する(Swift)

Xcode-12 iOS-14.0 Swift-5.3

はじめに

ごくまれに UIView の convert 系のメソッドを使って座標を変換したいときがありますがいつもどっちに何指定するのかな?と忘れるのでまとめておきます。

View 準備

下記のような構成で座標変換をしてみます。

xib

views

ScrollView を ViewController の View と同じサイズで配置してその中に contentView (緑色)を幅は ScrollView と同じ高さは 1200 pt で配置しています(StackView は左端のメモリ用です)。contentView の中に縦横 120 pt の targetView (白色)を contentView の Top 900pt、横は中央寄せで配置して、その targetView (白色)の中心にボタンを配置しています。

座標変換

座標変換には UIView に下記のメソッドが用意されており、これを使って変換します。

どちらのメソッドも対象の View は同一 Window にある必要があります。引数の viewnil の場合は 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:)

pointview の座標系を指定し、レシーバーの座標系に変換します。

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)
    }
}

こんな感じです:tada:

scroll

おわりに

to と from の意味を考えたらわかるんですが point はどの座標系のやついれればいいんだっけ?とよく迷います。。。

もうこれで迷わない!!(たぶん:rolling_eyes:

ちなみに Rect の変換メソッドもあります。
* convert(_: to:)
* convert(_: from:)

SwiftUI になるとこういうの使うこともなくなるのかな:thinking:

参考

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

【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リジェクトされたときの対処法

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

【Swift】 SpriteKit 入門 ②

SpriteKitの使い方を解説します。
SpriteKit 入門 ① で画像を表示させることができたので、続いて画像をドラッグして動かせるようにしていきたいと思います。
(環境:Xcode12.1  Swift5.3)

画像をドラッグで移動させる

前回作成したMyScene.swiftファイルにコードを追加していきましょう。タッチされた画面上の位置はtouchesMoved()で取得することができます。また画像(SpriteNode)をコードで操作できるようにMyScene.sksファイルにてNameを設定しておきます(ここではSampleImage)。
self.childNode(withName: )のwithNameプロパティには設定したNameを指定します。

SK1.png

MyScene.swift
import 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を使って画像(自機)から弾を発射するようにしていきましょう。

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