20190730のiOSに関する記事は8件です。

「ソースコード全部読まなきゃ病」と闘う方法

「OSS読んでパターンの勉強したくて、毎回挑戦してるんだけど、いっつも諦めるんだよね...。」とか「会社で新しいソースコード読む事になったんだけど量が多くて大変だよね...。」みたいな事をよく思っていた自分のような方を対象に、こうすればいいんじゃい?と自分が思ったことをまとめます。

最大の恐怖「全部読まないといけない」

自分のOSSリーディング活動を邪魔している感情や考えが、まずこの「全部読まないといけない病」にあることを自覚しました。「すごい人はOSSを毎日のように読んでる」「フレームワークの中身を全て知っている」こんな決めつけが根っこにありました。

それでGitHubでアルファベット順にならぶコードを上から順番に読もうとして途中で飽きたり挫けたり...。あるあるじゃないでしょうか?

ただよく考えるとそれって意味ある?という疑問が湧きました。
例えば小説や本などは最初のページから最後のページまでの時系列データなので、順番に読む事に意味はあります。ただ、ソースコードに関しては時系列データじゃないよね?と。

なのでいくつか読み方を考えてみました。

対処法1 「Facadeを読む」

これは自分が上記の問題に対して一番初めに試してみたものなのですが、かなりの効果を感じています。 ~Manager や、OSS利用の窓口になっているAPIって必ずありますよね?まずはそこから読み始めます。一番使う回数の多いところ周辺から徐々に理解をしていく方式に切り替えると、繋がりが分かりやすく意味ある読み方ができそうです。

対処法2 「必要なところだけ読む」

対処法1を理解していても「全部理解しなきゃ」と思っていると、どんどん読み込もうとしてしまって時間がかかったり嫌になったりします。なので、今知りたい知識が理解できたらそれより深くは読まないとか、利用していて分からないクラスや疑問が止まらない対象が出てきたら読む。という進め方が出来そうです。

対処法3 「時間を決めて読む」

「いつまでも読める」と考えると、どうしても必要のないコードを読んでしまいます。なので「このコードを読む時間は30分までと決める!この時間内で読むしかない」とすれば、コードを読む優先度を考えて、優先度の高いものから考えて読むでしょう。それを繰り返していくと良いと思います。

実際、自分の抱えているコードすべてをきちんと読もうとすると、読むだけで5年かかることもあります。業務でそればかりするわけにもいかないので、読みきれませんね。関連する部分(+アルファ)だけ集中的に読みます。

presented by @7of9 さん

対処法4 「古いバージョンから読む」

OSSだと古いバージョンほど機能が少なく、コアな機能しか実装されていないので、そのバージョンやコミットを読んでいくことで短い時間で効率良くコア部分に詳しくなる事ができます。

アンチパターンとして新しいバージョンで破壊的変更が行われている場合で、せっかく理解したのに...。という事が起こり得ます。

presented by @pCYSl5EDgo さん

終わりに

「全部読まなきゃ!」→諦めた。成果ゼロ。
よりも
「窓口だけ読んでみよう!」→3ファイル読めた。
の方が健全に進捗が出せていると思うので、「〜できないと〜する価値がない」みたいな考え方はやめて、まず読めるコードから読んでいって、いつのまにか全部読んでた。くらいで良いのではないかと思っています。

PS

とりあえず公開して修正してくパターンを取っているので、今後色々加筆修正していきます。

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

[Swift]UIViewにaddGestureRecognizerしようとしてつまずいた話

問題

UIViewにUITapGestureRecognizerをaddGestureRecognizerしようとしてもできなかった

前提

・isUserInteractionEnabled = true
・UIViewもnilではなかった

解決方法

addGestureRecognizer(.init(target: self, action: #selector(tapped)))

initを直したら動きました

addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapped)))

最後に

今思うとaddGestureRecognizerなので、.initで書いたらサブクラスのUITapGestureRecognizerではなく,スーパークラスのUIGestureRecognizerが呼ばれるので、それはそうですよねというお話(引数が同じなのですっかりハマってしまいました。??

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

SwiftでMapタイプの切り替え方

何をやるのか?

簡易的なMapアプリのオプション機能として、mapタイプの切り替えを行う。

前提

MapKitにてMapが設置されていることを前提とする。

Mapの種類

画面 定義 内容        
スクリーンショット 2019-07-30 17.41.45.png .mutedStandard 交通機関
スクリーンショット 2019-07-30 17.42.29.png .standard 標準の地図
スクリーンショット 2019-07-30 17.43.53.png .satellite 航空写真
スクリーンショット 2019-07-30 17.44.14.png .hybrid 航空写真
+
ラベル
スクリーンショット 2019-07-30 17.44.40.png .satelliteFlyover 3D Flyover
スクリーンショット 2019-07-30 17.45.07.png .hybridFlyover 3D Flyover
+
ラベル

実装コード

@IBAction func changeMapButton(_ sender: UIButton) {
        if Map.mapType == .standard {
            Map.mapType = .satellite
        } else if Map.mapType == .satellite{
            Map.mapType = .hybrid
        } else if Map.mapType == .hybrid{
            Map.mapType = .satelliteFlyover
        } else if Map.mapType == .satelliteFlyover{
            Map.mapType = .hybridFlyover
        } else if Map.mapType == .hybridFlyover{
            Map.mapType = .mutedStandard
        } else {
            Map.mapType = .standard
        }
    }

if文を使い現在のmapTypeと各mapTypeを順番に比較し、もし同じTypeだった場合次のTypeを代入するという簡単なコードになっています。
x-codeのフレームワークのButtonをStoryboardに貼り、それに対応する関数内に記述すれば問題ありません。

最後に

非常に簡単な実装ですが、割と本格的なアプリのように仕上がるので、初学者の方やプログラミングやった事ないけど興味ある人も是非一度実装して、開発の楽しさを体験してみましょう。

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

UserDefaultsをテストする際にsuiteNameを利用する件

UserDefaultsをテストする際にinit(suiteName:)でUserDefaultsをアプリとは別に作成し、それに対してデータを保存したり読み出したりしていたが、自分の意図している挙動と違うことがあったので動作確認コードを書いた。

そうやってからようやくリファレンスが理解できるようになった。
だけどそのリファレンスを解説してみても間違いがあるかもしれないので、自分の至った結論と、その結論に至った理由と動作確認のコードを示す。

最初に結論

  • テストごと(Specごと)に別々のinit(suiteName:)でUserDefaultsを作ったが、テストごとに分ける意味はないかもしれない
    • 別々のsuiteNameで作っても相互にその値を 見ることができる
      • 理由は後述
  • テスト用のsuiteNameとして一つを作るのは意味がある
    • 理由は後述
  • 書き込みできているかのテストなら書き込む前にクリアすべきかも
    • QuickならbeforeSuiteでクリアすると非同期にitが動作しているので他のテストに影響を与えてしまう
      • 書き込むitがあるならbeforeEachでクリアするほうが良さそう
  • アプリ側の挙動を変えないように
    • テストが終わったらinit(suiteName:)で作成したデータが参照されないように削除しておく

理由

suiteNameで作った場合のテストとアプリという視点で見たデータ共有

suiteNameでテストごとにUserDefaultsを作る視点で挙動をまとめる

  • テストターゲットでinit(suiteName:)でUserDefaultsを作っても、それはアプリのstandardなUserDefaultsから読み込める
    • ただし、アプリ側で同様のkeyを使いすでに保存してたらそれが優先される(nilならテストコードで入れた値が使われる)
  • アプリのstandardで作成したUserDefaultsはテストターゲットのinit(suiteName:)では読み込めない

よくわからないので視点を変える

suiteNameで分ける場合の細かな挙動

suiteNameで分ける場合の細かな挙動をまとめる

  • init(suiteName:)で作成して書き込んだデータはstandardなデータ(.plist)とは別のデータ(.plist)が作成される
      • UserDefaults(suiteName: "A")のsetメソッドでA.plistとBundleID.plistの2つができる
    • standardなデータ(.plist)にないが別のデータ(.plist)にある場合、それはstandardで別のデータの値で 取り出せる
    • 別のデータ(.plist)にないがstandardなデータにある場合、それはinit(suiteName:)standard取り出せる
  • init(suiteName:)は複数作れる
    • 複数の.plistが作られる
      • A.plistになくB.plistにある場合、init(suiteName:A)でBのデータが 取り出せてしまう

動作確認コード

UserDefaults().removePersistentDomain(forName: "A")
UserDefaults().removePersistentDomain(forName: "B")

// udA
let udA = UserDefaults(suiteName: "A")!

udA.set(1, forKey: "foo")
udA.set(2, forKey: "bar")
udA.set(3, forKey: "baz")
print("udA.foo:", udA.integer(forKey: "foo")) // -> 1
print("udA.bar:", udA.integer(forKey: "bar")) // -> 2
print("udA.baz:", udA.integer(forKey: "baz")) // -> 3
print("udA.ud :", udA.integer(forKey: "ud")) // -> 0

// udB
let udB = UserDefaults(suiteName: "B")!

udB.set(10, forKey: "foo")
print("udB.foo:", udB.integer(forKey: "foo")) // -> 10
print("udB.bar:", udB.integer(forKey: "bar")) // -> 2
print("udB.baz:", udA.integer(forKey: "baz")) // -> 3

// standard
UserDefaults.standard.set(100, forKey: "foo")
UserDefaults.standard.set(200, forKey: "bar")
UserDefaults.standard.set(999, forKey: "ud")

print("stn.foo:", UserDefaults.standard.integer(forKey: "foo")) // -> 100
print("stn.bar:", UserDefaults.standard.integer(forKey: "bar")) // -> 200
print("stn.baz:", UserDefaults.standard.integer(forKey: "baz")) // -> 3
print("stn.ud:", UserDefaults.standard.integer(forKey: "ud")) // -> 999

// foo見たい
print("udA.foo:", udA.integer(forKey: "foo")) // -> 1
print("udB.foo:", udB.integer(forKey: "foo")) // -> 10
print("stn.foo:", UserDefaults.standard.integer(forKey: "foo")) // -> 100

// bar見たい
print("udA.bar:", udA.integer(forKey: "bar")) // -> 2
print("udB.bar:", udB.integer(forKey: "bar")) // -> 2
print("stn.bar:", UserDefaults.standard.integer(forKey: "bar")) // -> 200

// 削除したあとどうなるか
UserDefaults().removePersistentDomain(forName: "A")

print("udA.foo:", udA.integer(forKey: "foo")) // -> 0
print("udB.foo:", udB.integer(forKey: "foo")) // -> 10
print("stn.foo:", UserDefaults.standard.integer(forKey: "foo")) // -> 100

print("udA.bar:", udA.integer(forKey: "bar")) // -> 0
print("udB.bar:", udB.integer(forKey: "bar")) // -> 0
print("stn.bar:", UserDefaults.standard.integer(forKey: "bar")) // -> 200

plistの確認法

~/Library/Developer/CoreSimulator/Devices/<シミュレータのID>/data/Containers/Data/Application/<アプリを区別するID>/Library/Preferences/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UserDefaultsをテストする際にuserSuiteを利用する件

UserDefaultsをテストする際にinit(suiteName:)でUserDefaultsをアプリとは別に作成し、それに対してデータを保存したり読み出したりしていたが、自分の意図している挙動と違うことがあったので動作確認コードを書いた。

そうやってからようやくリファレンスが理解できるようになった。
だけどそのリファレンスを解説してみても間違いがあるかもしれないので、自分の至った結論と、その結論に至った理由と動作確認のコードを示す。

最初に結論

  • テストごと(Specごと)に別々のinit(suiteName:)でUserDefaultsを作ったが、テストごとに複数のinit(suiteName:)を分ける意味はないかもしれない
    • 理由
      • 別々のsuiteNameで作っても相互にその値を 見ることができる
      • standardなUserDefaultsでもsuiteNameで保存した値を 見ることができる
      • Quickで非同期にテストが実行される場合、DIしてないUserDefaultsをbeforeSuiteで決めるのは難しい
  • テスト用のsuiteNameとして一つを作る意味は大きい
    • 理由
    • suiteNameで作られたUserDefaultsはstandardなものとは別なのでテスト終わったら削除すれば参照できなくなる
  • 書き込みできているかのテストなら書き込む前にクリアしておけばいい
    • QuickならbeforeSuiteでクリアすると非同期にitが動作しているので他のテストに影響を与えてしまう
      • 書き込むitがあるならbeforeEachでクリアするほうが良さそう

もしグローバルなUserDefaultsを保持しているなら

let myUserDefaults = MyUserDefaults.shared()
class ASpec: QuickSpec {
    override func spec() {

        beforeEach {
            // 都度suiteNameで指定
            myUserDefaults.ud = UserDefaults(suiteName: "A")
            // removePersistentDomain(forName:)で都度クリアし他のテストの影響を受けないようにする
            UserDefaults.removePersistentDomain(forName: "A")
        }

        afterEach {
            // removePersistentDomain(forName:)でクリアしアプリに影響を与えないようにする
            UserDefaults.removePersistentDomain(forName: "A")
        }

        it("...") {
        }

        it("...") {

        }
    }
}

理由

suiteNameで作った場合のテストとアプリという視点で見たデータ共有

suiteNameでテストごとにUserDefaultsを作る視点で挙動をまとめる

  • テストターゲットでinit(suiteName:)でUserDefaultsを作っても、それはアプリのstandardなUserDefaultsから読み込める
    • ただし、アプリ側で同様のkeyを使いすでに保存してたらそれが優先される(nilならテストコードで入れた値が使われる)

よくわからないので視点を変える

suiteNameで分ける場合の細かな挙動

suiteNameで分ける場合の細かな挙動をまとめる

  • init(suiteName:)で作成して書き込んだデータはstandardなデータ(.plist)とは別のデータ(.plist)が作成される
      • UserDefaults(suiteName: "A")のsetメソッドでA.plistができ、もともとの.plistと2つになる
    • standardなデータ(.plist)にないが別のデータ(.plist)にある場合、それはstandardで別のデータの値で 取り出せる
    • 別のデータ(.plist)にないがstandardなデータにある場合、それはinit(suiteName:)standard取り出せる
  • init(suiteName:)は複数作れる
    • 複数の.plistが作られる
      • A.plistになくB.plistにある場合、init(suiteName:A)でBのデータが 取り出せてしまう

動作確認コード

UserDefaults().removePersistentDomain(forName: "A")
UserDefaults().removePersistentDomain(forName: "B")

// udA
let udA = UserDefaults(suiteName: "A")!

udA.set(1, forKey: "foo")
udA.set(2, forKey: "bar")
udA.set(3, forKey: "baz")
print("udA.foo:", udA.integer(forKey: "foo")) // -> 1
print("udA.bar:", udA.integer(forKey: "bar")) // -> 2
print("udA.baz:", udA.integer(forKey: "baz")) // -> 3
print("udA.ud :", udA.integer(forKey: "ud")) // -> 0

// udB
let udB = UserDefaults(suiteName: "B")!

udB.set(10, forKey: "foo")
print("udB.foo:", udB.integer(forKey: "foo")) // -> 10
print("udB.bar:", udB.integer(forKey: "bar")) // -> 2
print("udB.baz:", udA.integer(forKey: "baz")) // -> 3

// standard
UserDefaults.standard.set(100, forKey: "foo")
UserDefaults.standard.set(200, forKey: "bar")
UserDefaults.standard.set(999, forKey: "ud")

print("stn.foo:", UserDefaults.standard.integer(forKey: "foo")) // -> 100
print("stn.bar:", UserDefaults.standard.integer(forKey: "bar")) // -> 200
print("stn.baz:", UserDefaults.standard.integer(forKey: "baz")) // -> 3
print("stn.ud:", UserDefaults.standard.integer(forKey: "ud")) // -> 999

// foo見たい
print("udA.foo:", udA.integer(forKey: "foo")) // -> 1
print("udB.foo:", udB.integer(forKey: "foo")) // -> 10
print("stn.foo:", UserDefaults.standard.integer(forKey: "foo")) // -> 100

// bar見たい
print("udA.bar:", udA.integer(forKey: "bar")) // -> 2
print("udB.bar:", udB.integer(forKey: "bar")) // -> 2
print("stn.bar:", UserDefaults.standard.integer(forKey: "bar")) // -> 200

// 削除したあとどうなるか
UserDefaults().removePersistentDomain(forName: "A")

print("udA.foo:", udA.integer(forKey: "foo")) // -> 0
print("udB.foo:", udB.integer(forKey: "foo")) // -> 10
print("stn.foo:", UserDefaults.standard.integer(forKey: "foo")) // -> 100

print("udA.bar:", udA.integer(forKey: "bar")) // -> 0
print("udB.bar:", udB.integer(forKey: "bar")) // -> 0
print("stn.bar:", UserDefaults.standard.integer(forKey: "bar")) // -> 200

plistの確認法

~/Library/Developer/CoreSimulator/Devices/<シミュレータのID>/data/Containers/Data/Application/<アプリを区別するID>/Library/Preferences/

おわりに

そもそもsuiteNameで.plistが分かれてというのは自分が作成した別アプリやウィジェットとのグループ間での共有のためだろうと思う。だから自分のアプリ単体であればsuiteNameで分かれてようが参照できても異常でない、みたいなことなんだろう。

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

UIDynamicsで物理演算を使った自然なアニメーション

UIKit Dynamics

Viewに物理演算を用いたアニメーションを設定できます。
ドキュメントはこちら

UIDynamicAnimator

Viewに物理に基づいたアニメーションを提供する根幹となるもの

    var dynamicAnimator: UIDynamicAnimator!
    override func viewDidLoad() {
        super.viewDidLoad()
        dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
    }
} 

重力を与える

storyboard上に配置した赤いViewに条件を追加します。
重力を与えるにはUIGravityBehaviorを使用します。

        let gravityBehavior = UIGravityBehavior(items: [redView])
        dynamicAnimator.addBehavior(gravityBehavior)  

ここでデフォルトの重力の大きさは1.0で、大きさ1.0は1000 point/second^2の加速度を意味します。
デフォルトの重力の向きは(0.0, 1.0)です。

パラメータ

パラメータとして次のようなものが設定できます。

プロパティ・メソッド名 説明
gravityDirection CGVector 重力ベクトル(大きさと方向)
angle CGFloat 重力ベクトルの方向(ラジアン)
magnitude CGFloat 重力ベクトルの大きさ
setAngle(_:, magnitude:) (CGFloat, CGFloat) -> Void 重力ベクトルの方向(ラジアン)と大きさ

他にsetAngle(_ angle: CGFloat, magnitude: CGFloat)メソッドを使用しても設定できます。
例えばaddBehavior(_:)する前に以下のように設定すると、

        gravityBehavior.gravityDirection = CGVector(dx: 2.0, dy: 2.0)
        gravityBehavior.magnitude = 1.0

一行目は右下に向かって重力の大きさ2.828..($2\sqrt{2} $)で動くような設定になります。
二行目でmagnitudeを1.0にすると、その方向を保ったままベクトルの大きさが1.0になるので最終的に重力ベクトルは(0.707..., 0.707...)となります。

衝突境界

衝突境界を追加したい場合にはUICollisionBehaviorを使用します。

デバイスの枠を境界とする

translatesReferenceBoundsIntoBoundaryはreferenceViewを境界として扱うかどうかというBool値です。
referenceViewは最初にUIDynamicAnimatorを初期化した時に設定したViewです。
先ほどこれにself.viewを設定したので、この場合デバイスの画面の端が境界になると考えられます。

        let collisionBehavior = UICollisionBehavior(items: [redView])
        collisionBehavior.translatesReferenceBoundsIntoBoundary = true
        dynamicAnimator.addBehavior(collisionBehavior)

自由に境界を設定

CGPointでどこからどこまでを境界とみなすかを指定します。
第一引数で指定するIDは後でこの境界だけ削除する場合などに使います。

        let collisionBehavior = UICollisionBehavior(items: [redView])
        collisionBehavior.addBoundary(withIdentifier: "floor" as NSCopying,
                              from: CGPoint(x: self.view.bounds.width/2 - 20,y: self.view.bounds.height/2 + 200),
                              to: CGPoint(x: self.view.bounds.width/2 + 20, y: self.view.bounds.height/2 + 200))
        dynamicAnimator.addBehavior(collisionBehavior)

この他にUIBezierPathを使用して境界を設定することもできます。

パラメータ

collisionModeで何を衝突対象とするのかを指定できる。
CollisionBehavior.Modeで指定できるのは以下。

CollisionBehavior.Mode 説明 動作
.items UICollisionBehaviorに紐付いているアイテム同士だけが衝突する
.boundaries 設定した境界のみが衝突対象となり、アイテム同士は衝突しない
.everything アイテム同士も境界も衝突する

デフォルトは.everything。

弾性係数・抵抗・摩擦

ある物体に対する弾性係数や抵抗など初期条件になるようなものを設定するにはUIDynamicItemBehaviorを使用します。

        let dynamicItemBehavior = UIDynamicItemBehavior(items: [redView])
        dynamicItemBehavior.elasticity = 1.0
        dynamicItemBehavior.resistance = 0
        dynamicItemBehavior.friction = 0
        dynamicAnimator.addBehavior(dynamicItemBehavior)

パラメータ

設定できる項目には以下のようなものがあります。

プロパティ・メソッド名 説明
density CGFloat 相対的な質量密度(1.0の密度を持つ100 x 100ポイントのitemに、大きさ1.0の力を加えると、100 point/secondで加速する)
elasticity CGFloat 弾性率の大きさ(1.0で完全弾性衝突)
friction CGFloat スライドする時にかかる摩擦の大きさ(1.0で強い摩擦、それ以上の値を指定することも可)
resistance CGFloat 抵抗の大きさ(CGFLOAT_MAXが最高値、1.0を設定した時は力が加えられなくなるとすぐに停止する)
allowsRotation Bool 回転を許可するか(デフォルトでtrue)
angularResistance CGFloat 角抵抗の大きさ
isAnchored Bool アイテムの位置が固定されているか
addLinearVelocity(_:, for:) (CGPoint, UIDynamicItem) -> Void 速度を与える(1秒あたりに動くpoint数を指定)
addAngulerVelocity(_:, for:) (CGFloat, UIDynamicItem) -> Void 角速度を与える(1秒あたりに動くラジアンを指定)

外力

アイテムに外力を加えるにはUIPushBehaviorを使用します。
鉛直投げ上げをするとき、プログラムは以下のようになります。

        let pushBehavior = UIPushBehavior(items: [redView], mode: UIPushBehavior.Mode.instantaneous)
        pushBehavior.pushDirection = CGVector(dx: 0.0, dy: -5.0)
        dynamicAnimator.addBehavior(pushBehavior)

ここで1.0の力とは、連続で1.0の力を与えたとき、密度値が1.0の100ポイントx 100ポイントのビューが100 point/second^2の加速度を持つような大きさをいいます。この値をUIKit Newtonと呼びます。

パラメータ

プロパティ名・メソッド名 説明
mode UIPushBehavior.Mode 力を加えるのが連続的(.continuous)か1度だけ(instantaneous)なのか
pushDirection CGFloat 力のベクトル(大きさと方向)
angle CGFloat 力のベクトルの方向(ラジアン)
magnitude CGFloat 力のベクトルの大きさ
setAngle(_:, magnitude:) (CGFloat, CGFloat) -> Void 力のベクトルの方向(ラジアン)と大きさ
setTargetOffsetFromCenter(_:for:) (UIOffset, UIdynamicItem) -> Void 力が物体のどこにかかるか。指定しない場合は中心にかかる

その他

他にもバネのような動きを実現するUISnapBehaviorや2物体の関係を扱うUIAttachimentBehavior、電場や磁場を設定できるUIFieldBehaviorがあります。

備考

先ほどの自由落下させて完全弾性衝突するプログラムのredViewの開始位置に印をつけました。
誤差があり線を超えたり、線まで届かなかったりします。
大まかな動きを再現するには良いですが、ぴったり数ポイント分の動きを実現したい場合この誤差は無視できないように思います。

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

UIKit Dynamicsで物理演算を使った自然なアニメーション

UIKit Dynamics

Viewに物理演算を用いたアニメーションを設定できます。
ドキュメントはこちら

UIDynamicAnimator

Viewに物理に基づいたアニメーションを提供する根幹となるもの

    var dynamicAnimator: UIDynamicAnimator!
    override func viewDidLoad() {
        super.viewDidLoad()
        dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
    }
} 

重力を与える

storyboard上に配置した赤いViewに条件を追加します。
重力を与えるにはUIGravityBehaviorを使用します。

        let gravityBehavior = UIGravityBehavior(items: [redView])
        dynamicAnimator.addBehavior(gravityBehavior)  

ここでデフォルトの重力の大きさは1.0で、大きさ1.0は1000 point/second^2の加速度を意味します。
デフォルトの重力の向きは(0.0, 1.0)です。

パラメータ

パラメータとして次のようなものが設定できます。

プロパティ・メソッド名 説明
gravityDirection CGVector 重力ベクトル(大きさと方向)
angle CGFloat 重力ベクトルの方向(ラジアン)
magnitude CGFloat 重力ベクトルの大きさ
setAngle(_:, magnitude:) (CGFloat, CGFloat) -> Void 重力ベクトルの方向(ラジアン)と大きさ

他にsetAngle(_ angle: CGFloat, magnitude: CGFloat)メソッドを使用しても設定できます。
例えばaddBehavior(_:)する前に以下のように設定すると、

        gravityBehavior.gravityDirection = CGVector(dx: 2.0, dy: 2.0)
        gravityBehavior.magnitude = 1.0

一行目は右下に向かって重力の大きさ2.828..($2\sqrt{2} $)で動くような設定になります。
二行目でmagnitudeを1.0にすると、その方向を保ったままベクトルの大きさが1.0になるので最終的に重力ベクトルは(0.707..., 0.707...)となります。

衝突境界

衝突境界を追加したい場合にはUICollisionBehaviorを使用します。

デバイスの枠を境界とする

translatesReferenceBoundsIntoBoundaryはreferenceViewを境界として扱うかどうかというBool値です。
referenceViewは最初にUIDynamicAnimatorを初期化した時に設定したViewです。
先ほどこれにself.viewを設定したので、この場合デバイスの画面の端が境界になると考えられます。

        let collisionBehavior = UICollisionBehavior(items: [redView])
        collisionBehavior.translatesReferenceBoundsIntoBoundary = true
        dynamicAnimator.addBehavior(collisionBehavior)

自由に境界を設定

CGPointでどこからどこまでを境界とみなすかを指定します。
第一引数で指定するIDは後でこの境界だけ削除する場合などに使います。

        let collisionBehavior = UICollisionBehavior(items: [redView])
        collisionBehavior.addBoundary(withIdentifier: "floor" as NSCopying,
                              from: CGPoint(x: self.view.bounds.width/2 - 20,y: self.view.bounds.height/2 + 200),
                              to: CGPoint(x: self.view.bounds.width/2 + 20, y: self.view.bounds.height/2 + 200))
        dynamicAnimator.addBehavior(collisionBehavior)

この他にUIBezierPathを使用して境界を設定することもできます。

パラメータ

collisionModeで何を衝突対象とするのかを指定できる。
CollisionBehavior.Modeで指定できるのは以下。

CollisionBehavior.Mode 説明 動作
.items UICollisionBehaviorに紐付いているアイテム同士だけが衝突する
.boundaries 設定した境界のみが衝突対象となり、アイテム同士は衝突しない
.everything アイテム同士も境界も衝突する

デフォルトは.everything。

弾性係数・抵抗・摩擦

ある物体に対する弾性係数や抵抗など初期条件になるようなものを設定するにはUIDynamicItemBehaviorを使用します。

        let dynamicItemBehavior = UIDynamicItemBehavior(items: [redView])
        dynamicItemBehavior.elasticity = 1.0
        dynamicItemBehavior.resistance = 0
        dynamicItemBehavior.friction = 0
        dynamicAnimator.addBehavior(dynamicItemBehavior)

パラメータ

設定できる項目には以下のようなものがあります。

プロパティ・メソッド名 説明
density CGFloat 相対的な質量密度(1.0の密度を持つ100 x 100ポイントのitemに、大きさ1.0の力を加えると、100 point/secondで加速する)
elasticity CGFloat 弾性率の大きさ(1.0で完全弾性衝突)
friction CGFloat スライドする時にかかる摩擦の大きさ(1.0で強い摩擦、それ以上の値を指定することも可)
resistance CGFloat 抵抗の大きさ(CGFLOAT_MAXが最高値、1.0を設定した時は力が加えられなくなるとすぐに停止する)
allowsRotation Bool 回転を許可するか(デフォルトでtrue)
angularResistance CGFloat 角抵抗の大きさ
isAnchored Bool アイテムの位置が固定されているか
addLinearVelocity(_:, for:) (CGPoint, UIDynamicItem) -> Void 速度を与える(1秒あたりに動くpoint数を指定)
addAngulerVelocity(_:, for:) (CGFloat, UIDynamicItem) -> Void 角速度を与える(1秒あたりに動くラジアンを指定)

外力

アイテムに外力を加えるにはUIPushBehaviorを使用します。
鉛直投げ上げをするとき、プログラムは以下のようになります。

        let pushBehavior = UIPushBehavior(items: [redView], mode: UIPushBehavior.Mode.instantaneous)
        pushBehavior.pushDirection = CGVector(dx: 0.0, dy: -5.0)
        dynamicAnimator.addBehavior(pushBehavior)

ここで1.0の力とは、連続で1.0の力を与えたとき、密度値が1.0の100ポイントx 100ポイントのビューが100 point/second^2の加速度を持つような大きさをいいます。この値をUIKit Newtonと呼びます。

パラメータ

プロパティ名・メソッド名 説明
mode UIPushBehavior.Mode 力を加えるのが連続的(.continuous)か1度だけ(instantaneous)なのか
pushDirection CGFloat 力のベクトル(大きさと方向)
angle CGFloat 力のベクトルの方向(ラジアン)
magnitude CGFloat 力のベクトルの大きさ
setAngle(_:, magnitude:) (CGFloat, CGFloat) -> Void 力のベクトルの方向(ラジアン)と大きさ
setTargetOffsetFromCenter(_:for:) (UIOffset, UIdynamicItem) -> Void 力が物体のどこにかかるか。指定しない場合は中心にかかる

その他

他にもバネのような動きを実現するUISnapBehaviorや2物体の関係を扱うUIAttachimentBehavior、電場や磁場を設定できるUIFieldBehaviorがあります。

備考

先ほどの自由落下させて完全弾性衝突するプログラムのredViewの開始位置に印をつけました。
誤差があり線を超えたり、線まで届かなかったりします。
大まかな動きを再現するには良いですが、ぴったり数ポイント分の動きを実現したい場合この誤差は無視できないように思います。

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

Swift Package Manager はじめの一歩

はじめに

Apple公式のパッケージマネージャーのSwift Package Manager(以下SwiftPM)を今回使用してみました。

パッケージマネージャーとはライブラリの依存関係を管理してくれるツールで、
CocoaPodsCarthageと同じようなものです。

https://github.com/apple/swift-package-manager

はじめに

SwiftPMが動作するか確認しましょう。
XcodeをインストールされていればTerminalから下記のコマンドが実行できます。

$ swift package --version
Apple Swift Package Manager - Swift 5.0.0 (swiftpm-14490.62.2)

また、ヘルプを確認するときは下記です。

swift package --help

プロジェクトを作成する

SwiftPMは作業ディレクトリを作成しないので、自分で作成する必要があります。
今回はHello-SwiftPMという名前の作業ディレクトリを作成します。

$ mkdir Hello-SwiftPM
$ cd Hello-SwiftPM/

今回はコマンドラインツールの作成方法です。
オプションに--type executableを追加する必要があります。
これを追加するとmain.swiftも同時に作成されます。

$ swift package init --type executable
Creating executable package: Hello-SwiftPM
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/Hello-SwiftPM/main.swift
Creating Tests/
Creating Tests/LinuxMain.swift
Creating Tests/Hello-SwiftPMTests/
Creating Tests/Hello-SwiftPMTests/Hello_SwiftPMTests.swift
Creating Tests/Hello-SwiftPMTests/XCTestManifests.swift

オプションはそのほかに

entity
library
system-module

があります

ビルド

$ swift package build

Xcodeプロジェクト作成

$ swift package generate-xcodeproj

リリースビルド

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