20190530のSwiftに関する記事は6件です。

【学習記録39】2019/5/30(木)

学習時間

3.0H

使用教材

学習分野

コメント

・アプリ制作
学習開始からの期間:40日目
今日までの合計時間:127.0H

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

iPhone、iPadで複雑な形状をした図形内のタッチイベントを簡単に取得する方法 〜CAShapeLayer編〜

現在開発中のアプリで矩形以外の複雑な形をした画像をのタッチイベントを取得する必要があり日本語、英語で軽く検索してみたが、”これだ!”という文献が探せなかったので共有しておきます。Playgroud環境下ですぐにお試しできます。
ezgif-5-9653f8f1f6ea.gif

下準備 〜画像作成〜

最終的にCAShapeLayerのPath(CGPath)に代入するので、Illustrator, Sketch, Figmaなどでベクター画像を作り、PaintCodeなどでUIBezierPathに変換します。
またPocketSVGというライブラリを使えば、SVGファイルCGPathとして取得できます。
Screen Shot 2019-05-30 at 12.15.53 AM.png

今回はその生成物(UIBezierPath)を解りやすいようにコード内にハードコーディングしていますが、図形の形状が複雑になると長くなってコードの可読性が悪くなるので別ファイル化した方が良いでしょう。
下記はUIBezierPath(上記の生成物をコピー)をCGPathで返す単純なfunctionです。

UIBezierPath
    func getPath()  -> CGPath {
        //Start: Created by PaintCode
        let bezierPath = UIBezierPath()
        bezierPath.move(to: CGPoint(x: 13.08, y: 129.49))
        bezierPath.addLine(to: CGPoint(x: 1.58, y: 117.99))
        bezierPath.addLine(to: CGPoint(x: 14.58, y: 104.99))
        bezierPath.addLine(to: CGPoint(x: 0.71, y: 91.11))
        bezierPath.addLine(to: CGPoint(x: 27.71, y: 64.1))
        bezierPath.addLine(to: CGPoint(x: 27.71, y: 53.7))
        bezierPath.addLine(to: CGPoint(x: 49.97, y: 31.45))
        bezierPath.addLine(to: CGPoint(x: 49.97, y: 23.61))
        bezierPath.addLine(to: CGPoint(x: 73.08, y: 0.5))
        bezierPath.addLine(to: CGPoint(x: 101.97, y: 0.5))
        bezierPath.addLine(to: CGPoint(x: 110.6, y: 9.13))
        bezierPath.addLine(to: CGPoint(x: 125.95, y: 9.13))
        bezierPath.addLine(to: CGPoint(x: 155.8, y: 39.71))
        bezierPath.addLine(to: CGPoint(x: 155.8, y: 52.35))
        bezierPath.addLine(to: CGPoint(x: 138.37, y: 69.78))
        bezierPath.addLine(to: CGPoint(x: 97.72, y: 29.13))
        bezierPath.addLine(to: CGPoint(x: 61.51, y: 65.34))
        bezierPath.addLine(to: CGPoint(x: 68.66, y: 72.49))
        bezierPath.addLine(to: CGPoint(x: 33.66, y: 107.49))
        bezierPath.addLine(to: CGPoint(x: 47.91, y: 121.74))
        bezierPath.addLine(to: CGPoint(x: 40.16, y: 129.49))
        bezierPath.addLine(to: CGPoint(x: 13.08, y: 129.49))
        bezierPath.close()
        //END
        return bezierPath.cgPath
    }

CAShapeLayerとは?

CAShapeLayerは名前からわかるようにCALayerのサブクラスで、UIBezierPathなどで作ったベクター画像を表示します。ベクター画像は通常のイメージと異なり拡大縮小してもボケたりジャギーが出たりすることがないので、@2x@3xの画像を作成する必要がありません。またCAShapeLayerのプロパティでパスの線の形状、色、パス内の色などを変更することが可能で、その多くがアニメーション可能です。そのため、簡単にタップしたらアニメーション付きで、色が変わる、大きさが変わるなどの仕掛けを作ることができます。

CALayerCAShapeLayerの他にも、CATextLayerCAGradientLayerCATiledLayer など数々の表示に関するサブクラスがあります。下記記事は英語ですが、それらがよくまとまっているので、一読をお勧めします。
CALayer Tutorial for iOS: Getting Started

ベクター画像の表示

CAShapeLayerを使った画像の表示手順は、
1. CAShapeLayerのインスタンスを生成
2. そのインスタンスのpathプロパティに上記で作成したパス(CGPath)を代入。
 shapeLayer.path = getPath()
3. インスタンスのプロパティで線種、線色、色などを設定(任意)
4. UIViewlayeraddSublayerメソッドを使って、上記のCAShapeLayerのインスタンスを追加
 self.layer.addSublayer(shapeLayer)
今回は上記手順をfunction(setupView())にまとめ、UIViewのカスタムクラスのイニシャライザでコールしています。

UIBezierPath
    private let shapeLayer = CAShapeLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    private func setupView()    {
        shapeLayer.path = getPath()
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 2.0
        shapeLayer.strokeColor = UIColor.blue.cgColor
        self.layer.addSublayer(shapeLayer)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        let point = touch!.location(in: self)
        shapeLayer.fillColor = shapeLayer.path!.contains(point) ? UIColor.red.cgColor : UIColor.white.cgColor
    }

タッチイベントの設定

さて、いよいよ本題のタッチイベントの設定です。今回はカスタムUIViewに画像を表示しているので、先ずはUIViewtouchesBeganをオーバーライドしてUITouchからタッチしたポイントを取得しています。let point = touch!.location(in: self)
その後、CGPathcontainsメソッドでそのポイントがパス内にあるかどうか確認しています。shapeLayer.path!.contains(point) このメソッドは領域内であればtrueを、外ならfalseを返します。
この例では画像をタッチした場合は画像を赤に、外であれば画像を白に変更しています。

VectorImageTouchEvent
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        let point = touch!.location(in: self)
        shapeLayer.fillColor = shapeLayer.path!.contains(point) ? UIColor.red.cgColor : UIColor.white.cgColor
    }

まとめ

知ってしまえばとても簡単です。CAShapeLayer素晴らしい。下記にコード全文を載せておきますので、Playgroudにコピー、実行すれば試せます。

TouchableVectorImageView
import UIKit
import PlaygroundSupport
let v = UIViewController()
PlaygroundPage.current.liveView = v


class TouchableVectorImageView:UIView   {
    private let shapeLayer = CAShapeLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }


    private func setupView()    {
        shapeLayer.path = getPath()
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 2.0
        shapeLayer.strokeColor = UIColor.blue.cgColor
        self.layer.addSublayer(shapeLayer)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        let point = touch!.location(in: self)
        shapeLayer.fillColor = shapeLayer.path!.contains(point) ? UIColor.red.cgColor : UIColor.white.cgColor

    }
}

extension TouchableVectorImageView    {

    func getPath()  -> CGPath {
        //Start: Created by PaintCode
        let bezierPath = UIBezierPath()
        bezierPath.move(to: CGPoint(x: 13.08, y: 129.49))
        bezierPath.addLine(to: CGPoint(x: 1.58, y: 117.99))
        bezierPath.addLine(to: CGPoint(x: 14.58, y: 104.99))
        bezierPath.addLine(to: CGPoint(x: 0.71, y: 91.11))
        bezierPath.addLine(to: CGPoint(x: 27.71, y: 64.1))
        bezierPath.addLine(to: CGPoint(x: 27.71, y: 53.7))
        bezierPath.addLine(to: CGPoint(x: 49.97, y: 31.45))
        bezierPath.addLine(to: CGPoint(x: 49.97, y: 23.61))
        bezierPath.addLine(to: CGPoint(x: 73.08, y: 0.5))
        bezierPath.addLine(to: CGPoint(x: 101.97, y: 0.5))
        bezierPath.addLine(to: CGPoint(x: 110.6, y: 9.13))
        bezierPath.addLine(to: CGPoint(x: 125.95, y: 9.13))
        bezierPath.addLine(to: CGPoint(x: 155.8, y: 39.71))
        bezierPath.addLine(to: CGPoint(x: 155.8, y: 52.35))
        bezierPath.addLine(to: CGPoint(x: 138.37, y: 69.78))
        bezierPath.addLine(to: CGPoint(x: 97.72, y: 29.13))
        bezierPath.addLine(to: CGPoint(x: 61.51, y: 65.34))
        bezierPath.addLine(to: CGPoint(x: 68.66, y: 72.49))
        bezierPath.addLine(to: CGPoint(x: 33.66, y: 107.49))
        bezierPath.addLine(to: CGPoint(x: 47.91, y: 121.74))
        bezierPath.addLine(to: CGPoint(x: 40.16, y: 129.49))
        bezierPath.addLine(to: CGPoint(x: 13.08, y: 129.49))
        bezierPath.close()
        //END
        return bezierPath.cgPath
    }
}


v.view.addSubview(TouchableVectorImageView(frame: CGRect(x: 50, y: 50, width: 200, height: 200)))
v.view.backgroundColor = UIColor.gray

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

【swift】Timer について

Timer

指定されたメッセージをターゲットオブジェクトに送信して、一定の時間間隔が経過した後に起動するタイマー。

Declaration 宣言

class Timer : NSObject

Overview 概要

タイマーは実行ループと連携して動作します。 実行ループはそれらのタイマーへの強力な参照を保持しているので、実行ループに追加した後にタイマーへのあなた自身の強力な参照を保持する必要はありません。
タイマーを効果的に使用するには、実行ループがどのように機能するのかを知っておく必要があります。 詳細については、Threading Programming Guideを参照してください。
タイマーはリアルタイムメカニズムではありません。 長時間の実行ループの呼び出し中、または実行ループがタイマーを監視していないモードにある間にタイマーの起動時間が発生した場合、次回の実行ループがタイマーをチェックするまでタイマーは起動しません。 したがって、タイマーが作動する実際の時間はかなり遅くなる可能性があります。 タイマー許容値も参照してください。
Timerは、Core FoundationのCFRunLoopTimerとフリーダイヤルブリッジされています。 詳細については、フリーダイヤルブリッジングを参照してください。

繰り返しタイマーと非繰り返しタイマーの比較

作成時にタイマーを繰り返すかどうかを指定します。 繰り返しのないタイマーは一度起動してから自動的に無効になり、それによってタイマーが再度起動するのを防ぎます。 対照的に、繰り返しタイマーは起動してから、同じ実行ループで自分自身を再スケジュールします。 繰り返しタイマーは、実際の起動時間とは対照的に、常にスケジュールされた起動時間に基づいて自分自身をスケジュールします。 たとえば、タイマーが特定の時間に起動し、その後5秒ごとに起動するようにスケジュールされている場合、実際の起動時間が遅れても、スケジュールされた起動時間は常に元の5秒の時間間隔になります。 発射時間が1つ以上の予定発射時間を過ぎるまで遅れると、タイマーはその期間中に1回だけ発射される。 その後、タイマーは、起動後に、次に予定されている起動時間に再スケジュールされます。

タイマー公差

iOS 7以降およびmacOS 10.9以降では、タイマーの許容値(tolerance)を指定できます。タイマーが作動したときのこの柔軟性により、節電と応答性の向上のために最適化するシステムの能力が向上します。タイマーは、その予定された発射日と予定された発射日と許容差との間のいつでも発火することができる。タイマーは、予定された開始日より前には開始されません。繰り返しタイマーの場合、ドリフトを避けるために、個々の発射時間に適用される許容範囲に関係なく、次の発射日が元の発射日から計算されます。デフォルト値はゼロです。これは、追加の許容誤差が適用されないことを意味します。システムは、toleranceプロパティの値に関係なく、特定のタイマーに少量の許容値を適用する権利を留保します。
タイマーのユーザーとして、タイマーの適切な許容範囲を決定できます。一般的な規則では、繰り返しタイマーの許容範囲を間隔の少なくとも10%に設定します。わずかな許容誤差でも、アプリケーションの電力使用量に大きなプラスの影響を与えます。システムは許容誤差の最大値を強制することができます

実行ループでのタイマーのスケジューリング

タイマーは一度に1つの実行ループにのみ登録できますが、その実行ループ内の複数の実行ループモードに追加することはできます。タイマーを作成する方法は3つあります。
scheduleTimer(timeInterval:invocation:repeats :)またはscheduleTimer(timeInterval:target:selector:userInfo:repeats :)クラスメソッドを使用して、タイマーを作成し、既定のモードで現在の実行ループにスケジュールします。
init(timeInterval:invocation:repeats :)またはinit(timeInterval:target:selector:userInfo:repeats :)クラスメソッドを使用して、実行ループでスケジュールせずにタイマーオブジェクトを作成します。 (作成後、対応するRunLoopオブジェクトのadd(:forMode :)メソッドを呼び出して、手動でタイマーを実行ループに追加する必要があります。)
タイマーを割り当て、init(fireAt:interval:target:selector:userInfo:repeats :)メソッドを使用してタイマーを初期化します。 (作成後、対応するRunLoopオブジェクトのadd(
:forMode :)メソッドを呼び出して、手動でタイマーを実行ループに追加する必要があります。)
実行ループでスケジュールされると、タイマーは無効になるまで指定された間隔で起動します。非繰り返しタイマーは、起動後すぐに無効になります。ただし、繰り返しタイマーの場合は、invalidate()メソッドを呼び出してタイマーオブジェクトを自分で無効にする必要があります。このメソッドを呼び出すと、現在の実行ループからタイマーを削除するように要求されます。結果として、タイマーがインストールされたのと同じスレッドからinvalidate()メソッドを常に呼び出す必要があります。タイマーを無効にするとすぐに無効になり、実行ループには影響しなくなります。次に、runループは、invalidate()メソッドが返される直前、またはそれ以降の時点で、タイマー(およびタイマーに対する強力な参照)を削除します。いったん無効にすると、タイマーオブジェクトは再利用できません。
繰り返しタイマーが起動した後、指定された許容範囲内で、最後にスケジュールされた起動日以降のタイマー間隔の整数倍である最も近い将来の日付に次回の起動がスケジュールされます。セレクターまたは呼び出しを実行するための呼び出しにかかる時間が指定された間隔よりも長い場合、タイマーは次の起動のみをスケジュールします。つまり、タイマーは、指定されたセレクターまたは呼び出しを呼び出している間に発生していたはずの失敗した起動を補おうとしません。

サブクラス化ノート

Timerをサブクラス化しないでください。

Topics

Creating a Timer

class func scheduledTimer(withTimeInterval: TimeInterval, repeats: Bool, block: (Timer) -> Void) -> Timer

デフォルトモードでタイマーを作成し、現在の実行ループでスケジュールします。

class func scheduledTimer(timeInterval ti: TimeInterval, target: Any, selector: Selector, userInfo: Any?, repeats: Bool) -> Timer

デフォルトモードでタイマーを作成し、現在の実行ループでスケジュールします。

ti秒が経過すると、タイマーが作動し、メッセージaSelectorをtargetに送信します。

class func scheduledTimer(timeInterval: TimeInterval, invocation: NSInvocation, repeats: Bool) -> Timer

新しいタイマーを作成し、デフォルトのモードで現在の実行ループにスケジュールします

ti秒が経過すると、タイマーが起動して呼び出しが呼び出されます。

init(timeInterval: TimeInterval, repeats: Bool, block: (Timer) -> Void)

指定された時間間隔とブロックでタイマーオブジェクトを初期化します。

init(timeInterval: TimeInterval, invocation: NSInvocation, repeats: Bool)

指定されたオブジェクトとセレクタを使用してタイマーオブジェクトを初期化します。

add(_:forMode :)を使用して、新しいタイマーを実行ループに追加する必要があります。その後、ti秒が経過した後、タイマーが作動し、メッセージaSelectorをtargetに送信します。 (タイマーが繰り返すように構成されている場合は、後で実行ループにタイマーを追加する必要はありません。)

init(fire: Date, interval: TimeInterval, repeats: Bool, block: (Timer) -> Void)

指定されたブロックを使用して、指定された日時のタイマーを初期化します。

init(fireAt: Date, interval: TimeInterval, target: Any, selector: Selector, userInfo: Any?, repeats: Bool)

指定されたオブジェクトとセレクタを使ってタイマーを初期化します。

add(_:forMode :)を使用して、新しいタイマーを実行ループに追加する必要があります。起動すると、タイマーはメッセージaSelectorをtargetに送信します。 (タイマーが繰り返すように構成されている場合は、後で実行ループにタイマーを追加する必要はありません。)

Firing a Timer タイマーを起動する

func fire()

タイマーのメッセージをターゲットに送信します。

このメソッドを使用すると、通常の起動スケジュールを中断することなく繰り返しタイマーを起動できます。 タイマーが繰り返されていない場合は、予定された開始日が到来していなくても、開始後に自動的に無効になります。

Stopping a Timer タイマーを停止する

func invalidate()

タイマーが二度と起動しないように停止し、実行ループからの削除を要求します。

このメソッドは、RunLoopオブジェクトからタイマーを削除する唯一の方法です。 RunLoopオブジェクトは、invalidate()メソッドが返される直前またはそれ以降の時点で、タイマーへの強い参照を削除します。
ターゲットおよびユーザー情報オブジェクトで構成されている場合、受信側はそれらのオブジェクトへの強い参照も削除します。
特別な考慮事項
このメッセージは、タイマーがインストールされているスレッドから送信する必要があります。 このメッセージを別のスレッドから送信すると、タイマーに関連付けられている入力ソースがその実行ループから削除されず、スレッドが正常に終了しなくなる可能性があります。

Retrieving Timer Information タイマー情報の取得

var isValid: Bool

タイマーが現在有効かどうかを示すブール値。

レシーバがまだ起動可能な場合はtrue、タイマーが無効化されていて起動できなくなった場合はfalse

var fireDate: Date

タイマーが作動する日付。

タイマーが無効になった場合は、タイマーが終了した最後の日付。
このプロパティを設定して、繰り返しタイマーの起動時間を調整できます。タイマーの次の起動時間をリセットすることは比較的高価な操作ですが、状況によってはより効率的な場合があります。たとえば、将来、アクションを不定期に複数回繰り返す場合に使用できます。単一のタイマーの起動時間を調整すると、複数のタイマーオブジェクトを作成し、それぞれを実行ループでスケジュールしてから破棄するよりも費用が少なくて済みます。
無効になっているタイマーの開始日は変更しないでください。これには、既に発行済みの繰り返しのないタイマーも含まれます。まだ発生していない繰り返しのないタイマーの開始日を潜在的に変更することができますが、潜在的な競合状態を回避するために、常にタイマーが接続されているスレッドから変更する必要があります。
isValidメソッドを使用して、タイマーが有効であることを確認します。

var timeInterval: TimeInterval

タイマーの時間間隔(秒単位)。

タイマーが繰り返しでない場合は、時間間隔が設定されていても0を返します。

var userInfo: Any?

受信側のuserInfoオブジェクト

タイマーが無効になった後は、このプロパティにアクセスしないでください。 タイマーが有効かどうかをテストするには、isValidを使用します。

Configuring Firing Tolerance 発射耐性の設定

var tolerance: TimeInterval

タイマーが起動する予定の起動日からの経過時間。
デフォルト値はゼロです。これは、追加の許容誤差が適用されないことを意味します。
タイマーの許容範囲を設定すると、スケジュールされた開始日より遅く開始することができます。タイマーが作動したときにシステムに柔軟性を持たせることで、節電と応答性を向上させるためにシステムが最適化する能力が高まります。
タイマーは、その予定された発射日と予定された発射日と許容差との間のいつでも発火することができる。タイマーは予定された発射日の前に発火しません。繰り返しタイマーの場合、ドリフトを避けるために、個々の発射時間に適用される許容範囲に関係なく、次の発射日が元の発射日から計算されます。このプロパティの値にかかわらず、システムは特定のタイマーにわずかな許容範囲を適用する権利を留保します。

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

[Swift5]Viewをアニメーションさせる

1.はじめに

久しぶりの投稿です。
作成するアプリをリッチにするために、アニメーションを覚えてみたいと思い、調べてみました。

2.全体のソースコード

ソースコードはこんな感じです。UIButtonを作って、押したらそのボタンが動きます。コピペでいけるので試してみてください。

animation.swift
import UIKit



class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()


        // UIButtonのインスタンスを作成する
        let button = UIButton()

        // ボタンを押した時に実行するメソッドを指定
        button.addTarget(self, action: #selector(upEvent(_:)), for: UIControl.Event.touchUpInside)
        // ボタンを押した時に実行するメソッドを指定
        button.addTarget(self, action: #selector(downEvent(_:)), for: UIControl.Event.touchDown)

        // ラベルを設定する
        button.setTitle("Enter", for: UIControl.State.normal)

        button.layer.borderColor = UIColor(red: 0, green: 60/255, blue: 0, alpha: 1).cgColor
        button.layer.borderWidth = 3
        button.layer.cornerRadius = 8

        // サイズを決める(自動調整)
        button.frame.size = CGSize(width: 100, height: 40)

        // 位置を決める(画面中央)
        button.center = self.view.center

        // viewに追加する
        self.view.addSubview(button)
        // Do any additional setup after loading the view.
    }


    // ボタンが押された時に呼ばれるメソッド
    @objc func downEvent(_ sender: UIButton) {
        let beforeCenter : CGPoint! = sender.center
        sender.frame.size = CGSize(width: 80, height: 30)
        sender.center = beforeCenter
    }

    // ボタンが押された時に呼ばれるメソッド
    @objc func upEvent(_ sender: UIButton) {
        let beforeCenter : CGPoint! = sender.center
        sender.frame.size = CGSize(width: 100, height: 40)
        sender.center = beforeCenter



        let animationGroup = CAAnimationGroup()
        animationGroup.duration = 0.5
        animationGroup.fillMode = CAMediaTimingFillMode.forwards
        animationGroup.isRemovedOnCompletion = false

        //大きさ(transform.scale)を1.5倍にする
        let animation1 = CABasicAnimation(keyPath: "transform.scale")
        animation1.fromValue = 1.0
        animation1.toValue = 1.5

        //透明度(opacity)を1から0にする
        let animation2 = CABasicAnimation(keyPath: "opacity")
        animation2.fromValue = 1.0
        animation2.toValue = 0.0

        //位置(position.x)を変更する
        let animation3 = CABasicAnimation(keyPath: "position.x")
        animation3.fromValue = sender.center.x
        animation3.toValue = sender.center.x + 50

        //右回りに回転する(transform.rotation.z)する
        let animation4 = CABasicAnimation(keyPath: "transform.rotation.z")
        animation4.toValue = Double.pi * 2

        //アンカーポイントを設定(※viewの位置も変わるため、frameを事前に変数に格納する)
        let frm = sender.frame
        sender.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
        sender.frame = frm

        animationGroup.animations = [animation1,animation2,animation3,animation4]
        sender.layer.add(animationGroup, forKey: nil)
    }

}

3.解説

アニメーション部分に該当する以下の各部分ごとに解説していきます。
UIButtonについては割愛します。

animation.swift
let animationGroup = CAAnimationGroup()
        animationGroup.duration = 0.5
        animationGroup.fillMode = CAMediaTimingFillMode.forwards
        animationGroup.isRemovedOnCompletion = false

        //大きさ(transform.scale)を1.5倍にする
        let animation1 = CABasicAnimation(keyPath: "transform.scale")
        animation1.fromValue = 1.0
        animation1.toValue = 1.5

        //透明度(opacity)を1から0にする
        let animation2 = CABasicAnimation(keyPath: "opacity")
        animation2.fromValue = 1.0
        animation2.toValue = 0.0

        //位置(position.x)を変更する
        let animation3 = CABasicAnimation(keyPath: "position.x")
        animation3.fromValue = sender.center.x
        animation3.toValue = sender.center.x + 50

        //右回りに回転する(transform.rotation.z)する
        let animation4 = CABasicAnimation(keyPath: "transform.rotation.z")
        animation4.toValue = Double.pi * 2

        //アンカーポイントを設定(※viewの位置も変わるため、frameを事前に変数に格納する)
        let frm = sender.frame
        sender.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
        sender.frame = frm

        animationGroup.animations = [animation1,animation2,animation3,animation4]
        sender.layer.add(animationGroup, forKey: nil)

3-1.アニメーショングループの作成

わかんないなりに書いていきます。一応動きます。(一番ダメなやつ。。。)

3-1-1.ソースコード

animation.swift
let animationGroup = CAAnimationGroup()
        animationGroup.duration = 0.5
        animationGroup.fillMode = CAMediaTimingFillMode.forwards
        animationGroup.isRemovedOnCompletion = false

アニメーションを複数同時に実行するために、CAAnimationGroup型の変数を作成します。
後述する各アニメーションで共通する部分の設定はここで行なっていきます。

3-1-2.時間の設定

animation.swift
animationGroup.duration = 0.5

この部分で、アニメーションの長さを設定しています。
0.5とすることで、0.5秒かけてアニメーションが行われます。

3-1-3.おまじない①

animation.swift
animationGroup.fillMode = CAMediaTimingFillMode.forwards

ここはよくわからなかったのですが、各サイトでみんな入れているので入れます。
どういったときに変更するのかご存知の方がいましたらお教えください。

3-1-4.おまじない②

animation.swift
animationGroup.isRemovedOnCompletion = false

アニメーションはViewに追加することで実行されるのですが、これはアニメーションが終了したときにViewから取り除くかどうかの設定、らしいです。

私が実装するときはアニメーションは都度Viewに追加しているので、取り除こうが取り除くまいが関係ない気もしますが、各サイトに倣って取り除かないようにしています。

3-2.各アニメーションの設定

3-2-1.大きさを変更する

animation.swift
//大きさ(transform.scale)を1.5倍にする
        let animation1 = CABasicAnimation(keyPath: "transform.scale")
        animation1.fromValue = 1.0
        animation1.toValue = 1.5

大きさを変更するアニメーションです。
徐々に大きくなるようなアニメーションが実現できます。

animation.swift
let animation1 = CABasicAnimation(keyPath: "transform.scale")

ここでアニメーショングループに追加するアニメーション(CABasicAnimation)の変数を作成します。keyPathに"transform.scale"を与えてあげることで、サイズ変更のアニメーションになります。

animation.swift
animation1.fromValue = 1.0
animation1.toValue = 1.5

ここで大きさを変更するアニメーションのプロパティを設定しています。
fromValue:アニメーション開始時の大きさ(元の大きさを1.0とする)
toValue:アニメーション終了時の大きさ(元の大きさを1.0とする)

わかりやすいですね。始まりと終わりだけ教えてあげればOKです。

3-2-2.透明度を変更する

animation.swift
        //透明度(opacity)を1から0にする
        let animation2 = CABasicAnimation(keyPath: "opacity")
        animation2.fromValue = 1.0
        animation2.toValue = 0.0

透明度を変えます。
「3-2-1.大きさを変更する」と違うところは、keyPathを"opacity"にしています。
これにfromValueとtoValueを設定すれば、少しずつ消えて行ったり、逆に少しずつ出現したりと行ったリッチなアニメーションが簡単に作れます。

3-2-3.位置を変更する

animation.swift
        //位置(position.x)を変更する
        let animation3 = CABasicAnimation(keyPath: "position.x")
        animation3.fromValue = sender.center.x
        animation3.toValue = sender.center.x + 50

位置を変えます。
「3-2-1.大きさを変更する」と違うところは、keyPathを"position.x"にしています。
これにfromValueとtoValueを設定することで、左右にViewを動かすことができます。
fromValueとtoValueは、座標の位置を設定してください。1に設定すると端っこになってしまうので、基本的にはViewのcenter.xから取得すると良いと思います。

また、上下に動かしたい場合はkeyPathを"position.y"に設定すればOKです。

3-2-4.回転させる

animation.swift
//右回りに回転する(transform.rotation.z)する
        let animation4 = CABasicAnimation(keyPath: "transform.rotation.z")
        animation4.toValue = Double.pi * 2

位置を変えます。
「3-2-1.大きさを変更する」と違うところは、keyPathを"transform.rotation.z"にしています。
また、プロパティはtoValueのみ設定し、円周率(Double.pi)*2でz軸上を一回転しています。半周させたい場合は「*2」は不要で、「Double.pi * 2 * 0.25」などとすることで90度回転なども可能です。

keyPathを"transform.rotation.x"や"transform.rotation.y"に設定すれば回転の軸を変えることができます。(※実際に試して感覚をつかんでみてください)

また、回転の中心となる位置は以下のようにすることで変更が可能です。

animation.swift
        //アンカーポイントを設定(※view自体の位置も変わるため、frameを事前に変数に格納する)
        let frm = sender.frame
        sender.layer.anchorPoint = CGPoint(x: 0, y: 0.5)
        sender.frame = frm

3-3.アニメーションを実行

animation.swift
animationGroup.animations = [animation1,animation2,animation3,animation4]
        sender.layer.add(animationGroup, forKey: nil)

アニメーショングループに各アニメーションを追加し、レイヤーにアニメーショングループを追加します。

3.おわりに

テスト用なので拡大しつつフェードアウトして回りながら右に移動するという、妙な動きになっていますが、好きなように変更して良いアニメーションライフをお過ごしください。

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

Realmでenumを使うときメモ

結論

privateプロパティのときは@objcMembersを書いていても@objcを書かないとだめ

これでいけそうだけどだめで

@objcMembers
class Phone: Object {

    dynamic var name = ""
    private dynamic var osTypeRawValue = "" // privateの場合は@objcMembersを書いてあってもこれじゃだめ
    var type: OSType {
        get { return OSType(rawValue: osTypeRawValue) ?? .undefind }
        set { osTypeRawValue = newValue.rawValue }
    }

    enum OSType: String {
        case ios
        case android
        case undefind
    }
}

こうするとOK

@objcMembers
class Phone: Object {

    dynamic var name = ""
    @objc private dynamic var osTypeRawValue = "" // @objcを明示的につける
    var type: OSType {
        get { return OSType(rawValue: osTypeRawValue) ?? .undefind }
        set { osTypeRawValue = newValue.rawValue }
    }

    enum OSType: String {
        case ios
        case android
        case undefind
    }
}

こんな感じでenumが使える

let phone = Phone()
phone.type = .ios
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift - init(repeating:count:)で配列を生成するときの注意点

基本的な使い方

// 4人で点数を競うときの初期値を0でいれておく

var playerScores = Array(repeating: 0, count: 4)
print(playerScores) // [0, 0, 0, 0]

// 書くプレイヤーがスコアを獲得
playerScores[0] += 5
playerScores[1] += 10
playerScores[2] += 15
playerScores[3] += 20
print(playerScores) // [5, 10, 15, 20]

クラスのインスタンスを格納する配列を生成する

class Player {
  var name = ""
  var score = 0
}

var players = Array(repeating: Player(), count: 4)

// 書くプレイヤーに名前を代入
players[0].name = "John"
players[1].name = "Paul"
players[2].name = "George"
players[3].name = "Ringo"

players.forEach { player in
  print(player.name)
}

/*
Ringo
Ringo
Ringo
Ringo
みんなRingoになってしまった
*/

クラスのような参照型の場合、同じ参照を持つインスタンスがrepeatingにより複数生成されてしまいます。例えば、上記のようにクラスのインスタンスを配列で格納し、セルに表示する情報を管理する場合などは、1つの要素の情報を変えると他の要素にまで影響があるので注意が必要です。

解決策

structなら値型なので上記のように1つの要素の値を変更すると、他の要素まで影響があるということはありません。

struct Player {
  var name = ""
  var score = 0
}

var players = Array(repeating: Player(), count: 4)

// 書くプレイヤーに名前を代入
players[0].name = "John"
players[1].name = "Paul"
players[2].name = "George"
players[3].name = "Ringo"

players.forEach { player in
  print(player.name)
}

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