20191130のSwiftに関する記事は13件です。

最前面のViewを検出・参照する[Swift]

状況

最前面のViewControllerを取得する記事はたくさんあるけど
Viewに関しては見つからなかったので僕なりのやり方を残しておきます

対策

GirlsViewはcustomViewです

流れ

frontViewに存在するViewのなかで最後に配置したView(最前面のView)を入れておく->右スワイプする->ViewとViewの配列を消去する
Viewを配置するたびにzPositionを上げている-> 最後に配置したView = 最前面のView になります

private func setSwipe(girlsView: GirlsView) {
        let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(didSwipe(sender:)))
        rightSwipe.direction = .right
        girlsView.addGestureRecognizer(rightSwipe)
}

@objc func didSwipe(sender: UISwipeGestureRecognizer) {
        if sender.direction.contains(.right) {
            print("Right!")
            getFrontView().removeFromSuperview()
            girlsViewArray.removeLast()
        }
}

private func getFrontView() -> GirlsView {
        var frontView: GirlsView?
        Array(0..<girlsViewArray.count).forEach {
            if let view: GirlsView? = girlsViewArray[$0] {
                frontView = view!
            }
        }
        return frontView!
    }
override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        Array(0..<girlsViewArray.count).forEach {
            view.addSubview(girlsViewArray[$0]) //$0は0 ~ girlsViewArray-1
            girlsViewArray[$0].layer.zPosition = CGFloat($0) + 1.0
            setSwipe(girlsView: girlsViewArray[$0])
            //省略
        }
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【2019年版】使ってみてよかったiOSデバッグツール

はじめに

  • アプリ開発の中でデバッグは切っても切り離せない作業です。デバッグ作業の効率を高めるためのツールはたくさんあります。 たくさんのツールの中で使ってみてよかったものについてまとめてみました。 *あくまで個人的な意見です

第5位 SwiftyBeaver

SwiftyBeaver/SwiftyBeaver⭐️4.5k

  • 通信周りなどのログを表示できるツール。browserアプリを使うと便利です

第4位 KZFileWatchers

krzysztofzablocki/KZFileWatchers⭐️954

  • localにあるファイルを参照できるデバッグツールです。mockデータで実際のアプリで取得可能になる

?第3位 LifetimeTracker

krzysztofzablocki/LifetimeTracker⭐️2.1k

  • メモリーリークしているviewController,viewを検知できるツール
  • リークしている事に気が付きやすくなるツールです

?第2位 FLEX

Flipboard/FLEX⭐️10.9k

  • 便利機能が豊富に搭載されているユーティリティツール
  • 特にレイアウトチェックや表示しているクラスの検索に便利

?第1位 iCimulator

YuigaWada/iCimulator⭐️80

  • シミュレータでカメラ機能が使えるツール
  • 是非とも標準機能で対応して欲しいです

まとめ

  • アプリ開発の中でデバッグの効率を上げるツールはたくさんある
  • 様々なツールを見ながら、適切なものを選ぶことが重要である
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftのオーバーロード選択のスコア規則

Swiftは関数やメソッドをオーバーロードすることができます。例えば下記のようなコードを書けます。

func f(_ a: Int) {}

func f(_ a: Int?) {}

この時、f(3)という呼び出しは、2つあるfのうち、1つ目の定義の方の呼び出しになる事はよく知られています。

では、下記のコードではどうなるでしょうか。

func f(_ a: Int?) {}

func f(_ a: Any) {}

この場合にf(3)がどちらの呼び出しになるかはよくわからない人が多いと思います。

この記事では、Swiftがこういったケースでどのようにオーバーロードを選択するのか説明します。

推論解のスコア

Swiftコンパイラのアーキテクチャでは、オーバーロードの解決は、型推論器の役割になります。型推論器がオーバーロードごとの仮の解を複数発見した後、その複数の解に対して優先度付けをして、最も良いものを決定します。その優先度付けには複数の仕組みが組み込まれているのですが、ここではスコアについて説明します。

Swiftコンパイラには、複数の種類のスコアがあり、種類ごとに点数を計上します。種類ごとに高さが決められており、高い種類のスコアの点数から順に比較していきます。直感的には、そのまま数字を繋げて十進数だと思って読めば良いです(ある種類の点数が9以下である限り)。このスコアの比較の様子をコンパイラにオプションを与えることで観察することができます。

下記のコードをコンパイラに与えてみます。

// code01.swift

func f(_ a: Int?) { print("Optional") }

func f(_ a: Any) { print("Any") }

f(3)

コマンドは下記のようにします。

$ swiftc -dump-ast -Xfrontend -debug-constraints code01.swift 

すると、250行ほどの出力が得られるのですが、抜粋すると下記のような部分があります。

---Initial constraints for the given expression---
(call_expr type='()' location=code01.swift:5:1 range=[code01.swift:5:1 - line:5:4] arg_labels=_:
  (overloaded_decl_ref_expr type='$T0' location=code01.swift:5:1 range=[code01.swift:5:1 - line:5:1] name=f number_of_decls=2 function_ref=single decls=[
    code01.(file).f@code01.swift:1:6,
    code01.(file).f@code01.swift:3:6])
  (paren_expr type='($T1)' location=code01.swift:5:3 range=[code01.swift:5:2 - line:5:4]
    (integer_literal_expr type='$T1' location=code01.swift:5:3 range=[code01.swift:5:3 - line:5:3] value=3 builtin_initializer=**NULL** initializer=**NULL**)))

これは、ASTで表されたこの式の部分を解析する事を示す出力です。call_exprが関数呼び出しで、overloaded_decl_ref_exprはオーバーロードされた関数の参照です。その中にname=fと書いてあるので、これがオーバーロードされたfを呼び出している箇所であるとわかります。また、行番号5の部分を見てもわかります。

ここでoverloaded_decl_ref_exprの中にtype='$T0'と書かれているところも重要です。これは型変数というもので、型推論器にとって未確定の型を示します。型推論器はこの型変数に割り当てる型を推論します。これは関数の型を示しているので、この型変数に割り当てられる型を確認する事で、オーバーロードされた関数がいずれに決定したのかが後にわかります。

さて、その下に下記のような出力があります。

  $T1 literal conforms to ExpressibleByIntegerLiteral [[locator@0x7fade80c2598 [IntegerLiteral@code01.swift:5:3]]];
  ($T1) -> $T2 applicable fn $T0 [[locator@0x7fade80c26d8 [Call@code01.swift:5:1 -> apply function]]];
  ($T1 fully_bound literal=3 involves_type_vars bindings={(subtypes of) (default from ExpressibleByIntegerLiteral) Int})
  (attempting disjunction choice $T0 bound to decl code01.(file).f@code01.swift:1:6 : (Int?) -> () at code01.swift:1:6 [[locator@0x7fade80c2400 [OverloadedDeclRef@code01.swift:5:1]]];
    (overload set choice binding $T0 := (Int?) -> ())
    ($T1 bindings={(subtypes of) Int})
    Initial bindings: $T1 := Int
    (attempting type variable $T1 := Int
      (increasing score due to value to optional)
      (found solution 0 0 0 0 0 0 0 0 1 0 0 0)
    )
  )
  (attempting disjunction choice $T0 bound to decl code01.(file).f@code01.swift:3:6 : (Any) -> () at code01.swift:3:6 [[locator@0x7fade80c2400 [OverloadedDeclRef@code01.swift:5:1]]];
    (overload set choice binding $T0 := (Any) -> ())
    ($T1 literal=3 bindings={(subtypes of) Any; (subtypes of) (default from ExpressibleByIntegerLiteral) Int})
    Initial bindings: $T1 := Any, $T1 := Int
    (attempting type variable $T1 := Any
      (increasing score due to non-default literal)
      (solution is worse than the best solution)
    )
    (attempting type variable $T1 := Int
      (increasing score due to empty-existential conversion)
      (found solution 0 0 0 0 0 0 0 0 0 1 0 0)
    )
  )

この中に、found solutionと書かれた行が2つあります。これは型推論器が解の選択過程において2つの解を発見した事を現しています。そして、その中に01の並びがありますが、これがスコアです。

スコアは整数の並びになっており、左の桁ほど高く、同じ桁同士では値が大きいほど高いです。最終的に、スコアが最も低い解が選択されます。低いほど選択されやすいので、スコアというよりコストと考えたほうが直感的にわかりやすいかもしれません。(スコアというのはコンパイラ内部でこの概念に与えられている名称です)

さらに続きを見ると下記の出力があります。

--- Solution #0 ---
Fixed score: 0 0 0 0 0 0 0 0 1 0 0 0
Type variables:
  $T1 as Int @ locator@0x7fade80c2598 [IntegerLiteral@code01.swift:5:3]
  $T0 as (Int?) -> () @ locator@0x7fade80c2400 [OverloadedDeclRef@code01.swift:5:1]
  $T2 as () @ locator@0x7fade80c2658 [Call@code01.swift:5:1 -> function result]

Overload choices:
  locator@0x7fade80c2400 [OverloadedDeclRef@code01.swift:5:1] with code01.(file).f@code01.swift:1:6 as f: (Int?) -> ()


Constraint restrictions:
  Int to Optional<Int> is [value-to-optional]

Disjunction choices:

これは2つある解のうち、解0番の中身を現しています。ポイントは、型変数$T0(Int?) -> ()が割り当てられていることです。これは引数の型がInt?である方のfを選択する事を示しています。
そしてスコアは0 0 0 0 0 0 0 0 1 0 0 0です。右から4番目の桁が1になっています。

桁に対応する定義はソースコードのこちらを見るとわかります。それによると、4番目はSK_ValueToOptionalであり、これは型Tを型T?に変換する時に加算されるスコアです。

さらに続きをみると下記の出力があります。

--- Solution #1 ---
Fixed score: 0 0 0 0 0 0 0 0 0 1 0 0
Type variables:
  $T1 as Int @ locator@0x7fade80c2598 [IntegerLiteral@code01.swift:5:3]
  $T0 as (Any) -> () @ locator@0x7fade80c2400 [OverloadedDeclRef@code01.swift:5:1]
  $T2 as () @ locator@0x7fade80c2658 [Call@code01.swift:5:1 -> function result]

Overload choices:
  locator@0x7fade80c2400 [OverloadedDeclRef@code01.swift:5:1] with code01.(file).f@code01.swift:3:6 as f: (Any) -> ()


Constraint restrictions:
  Int to Any is [existential]

こちらは2つ目の解である解1番の詳細です。先と同様に$T0の割当を見ると(Any) -> ()になっているので、これはAnyを受けるオーバーロードの選択であるとわかります。そして、スコアは0 0 0 0 0 0 0 0 0 1 0 0です。

こちらは右から3番目が1になっています。3番目はSK_EmptyExistentialConversionで、Anyへの変換があると加算されるスコアです。

続きには下記の出力があります。

---Solution---
Fixed score: 0 0 0 0 0 0 0 0 0 1 0 0
Type variables:
  $T1 as Int @ locator@0x7fade80c2598 [IntegerLiteral@code01.swift:5:3]
  $T0 as (Any) -> () @ locator@0x7fade80c2400 [OverloadedDeclRef@code01.swift:5:1]
  $T2 as () @ locator@0x7fade80c2658 [Call@code01.swift:5:1 -> function result]

Overload choices:
  locator@0x7fade80c2400 [OverloadedDeclRef@code01.swift:5:1] with code01.(file).f@code01.swift:3:6 as f: (Any) -> ()


Constraint restrictions:
  Int to Any is [existential]

Disjunction choices:

ここは採用された解が出ている部分で、(Any) -> ()のオーバーロードが選ばれた事がわかります。

続きには下記のように、解となる型が割り当てられたASTの出力もあります。

---Type-checked expression---
(call_expr type='()' location=code01.swift:5:1 range=[code01.swift:5:1 - line:5:4] arg_labels=_:
  (declref_expr type='(Any) -> ()' location=code01.swift:5:1 range=[code01.swift:5:1 - line:5:1] decl=code01.(file).f@code01.swift:3:6 function_ref=single)
  (paren_expr type='(Any)' location=code01.swift:5:3 range=[code01.swift:5:2 - line:5:4]
    (erasure_expr implicit type='Any' location=code01.swift:5:3 range=[code01.swift:5:3 - line:5:3]
      (integer_literal_expr type='Int' location=code01.swift:5:3 range=[code01.swift:5:3 - line:5:3] value=3 builtin_initializer=Swift.(file).Int.init(_builtinIntegerLiteral:) initializer=**NULL**))))

overloaded_decl_ref_exprdeclref_exprに変更され、type='(Any) -> ()'になっていることから、Anyを受けるオーバーロードが選ばれたことがわかります。

この2つの比較の場合は、左の桁のほうが影響が大きいので、4桁目が1になっているInt?の方がスコアが高く、スコアが低いAnyの方が選択されます。

スコアの例

その他のスコアとして、6桁目にSK_NonDefaultLiteralがあります。これはリテラルがデフォルト型にならない場合に加算されるスコアです。

Swiftのリテラルは、リテラルの種類に応じて、割り当てることができる型が決まっているのですが、その中でも特別にデフォルトの型が決まっています。例えば、1のような整数リテラルの場合はInt1.0のような小数リテラルの場合はDoubleです。

例えば下記のコードを考えてみましょう。

// SK_EmptyExistentialConversion
func f(_ a: Any) { print("Any") }

// SK_ValueToOptional
func f(_ a: Int?) { print("Optional") }

// SK_NonDefaultLiteral
func f(_ a: Float) { print("Float") }

f(3)

コメントでそれぞれの呼び出しで加算されるスコアを書いてあります。それぞれ3, 4, 6桁目なので、下のものほどスコアが高く、選ばれにくいです。

実際に実行してみると、Anyと出力されます。

そして、1つ目のfをコメントアウトすると、Optionalと出力され、2つ目のfもコメントアウトして初めてFloatと出力されます。

呼び出しの行をf(3.0)に変更した場合も、Floatは小数リテラルのデフォルト型ではないので、Anyの方が優先されます。

もしかしたら「Anyの吸い込みは強い」といったような経験的な感覚を持っている方もいるかもしれませんが、この結果を意外に感じる方が多いのではないでしょうか。

Swiftコードを書いていて、オーバーロードの選択優先度が重要になる場面に出会ったら、正確な振る舞いを理解する上でこの記事を参考にしてもらえると幸いです。

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

UISwipeGestureRecognizerを使ったときにはまった(unrecognized selector sent to instance)

状況

UISwipeGestureRecognizerを使ったとき

unrecognized selector sent to instance

がで続けてハマったのでメモ。ちなみにハマったときに書いてたコードが以下。(一部簡略化してます)

private func setSwipe(girlsView: GirlsView) {
        let rightSwipe = UISwipeGestureRecognizer(target: self, action: "didSwipe:"))
        rightSwipe.direction = .right
        girlsView.addGestureRecognizer(rightSwipe)
}

private func didSwipe(sender: UISwipeGestureRecognizer) {
        if sender.direction.contains(.right) {
            print("Right!")
            getFrontView().removeFromSuperview()
            girlsViewArray.removeLast()
        }
}

対策

調べるとactionの関数名書くところに:をつけなきゃダメとかあったのですがうまくいかず
@objcにして#selectorで囲ったらうまく行きました(addTargetと同じやり方ですね)

private func setSwipe(girlsView: GirlsView) {
        let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(didSwipe(sender:)))
        rightSwipe.direction = .right
        girlsView.addGestureRecognizer(rightSwipe)
    }

@objc func didSwipe(sender: UISwipeGestureRecognizer) {
        if sender.direction.contains(.right) {
            print("Right!")
            getFrontView().removeFromSuperview()
            girlsViewArray.removeLast()
        }
}

参考

ios – SpriteKitとUISwipeGestureRecognizer - コードログ
ios - UITapGestureRecognizer unrecognized selector sent to instance - Stack Overflow
UISwipeGestureRecognizerの罠と左右スワイプの実装 - Qiita

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

構造体(Struct)のViewを配列にしてみる[Swift] - vol.2(データ挿入・表示まで)

データ型Structの用意

  • Tinderなどのプロフィールにあるデータをプロパティとして用意
  • initで初期化
Girls.swift
import UIKit

struct Girl {
    var name: String
    var age: String
    var type: String //職業など(ex. 学生, OLなど)
    var distance: String
    var image: UIImage

    //初期化 //Girl(name: String, age: String, type: String, distance: String, image: UIImage)という感じでインスタンスにして使う
    init(name: String, age: String, type: String, distance: String, image: UIImage) {
        self.name = name
        self.age = age
        self.type = type
        self.distance = distance
        self.image = image
    }
}

Viewに代入

配列を用意

var girlsData: [Girl] = [] //初期化してからの配列

private func setGirlsInfo() {
        //データをセット
        girlsData.append(Girl(name: "あや", age: "18", type: "学生", distance: "6 km", image: UIImage(named: "aya.png")!))
        girlsData.append(Girl(name: "マイカ", age: "25", type: "OL", distance: "2 km", image: UIImage(named: "maika.png")!))
        girlsData.append(Girl(name: "emi", age: "40", type: "パート", distance: "56 km", image: UIImage(named: "emi.png")!))
        girlsData.append(Girl(name: "みゆ", age: "21", type: "大学生", distance: "10 km", image: UIImage(named: "miyu.png")!))
        girlsData.append(Girl(name: "リンゴ", age: "29", type: "事務", distance: "16 km", image: UIImage(named: "ringo.png")!))
}

override func viewDidLoad() {
        super.viewDidLoad()
        setGirlsInfo()
}

Viewに配列の要素を代入

//girlsViewの要素にGirlのインスタンスの各要素を代入
private func setDataToGirlsView(girlsView: GirlsView, index: Int) {
        girlsView.nameLabel.text = girlsData[index].name
        girlsView.ageLabel.text = girlsData[index].age
        girlsView.typeLabel.text = girlsData[index].type
        girlsView.distanceLabel.text = girlsData[index].distance
        girlsView.girlsImageView.image = girlsData[index].image
}

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        Array(0..<girlsViewArray.count).forEach {
            //省略
            view.addSubview(girlsViewArray[$0]) //$0は0 ~ girlsViewArray-1
            //省略
//girlsViewArrayの各要素にGirlのインスタンスを代入
            setDataToGirlsView(girlsView: girlsViewArray[$0], index: $0)
            //省略
        }
}

完成図

girlsView-ex.jpeg

完成したらgithubに後ほどリンク載せます

参考

Swiftで構造体を配列で扱う方法 | Swift4 Web入門書

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

フォントサイズの異なるUILabelの下を揃える[Swift]

状況

UILabelにおいて、フォントサイズが異なると、上下の余白が異なるため
制約でbottomを揃えても文字の下が揃わないことがあります。

対策

  • 揃えたいUILabel高さの制約を0以上にする heightを0にし、Greater Than Or Equalにする
  • 制約でBaselineを揃える(FirstでもLastでも良い) bottomではなくBaselineを揃える

完成図

リンゴと29がきちんと下揃えされている。

girlsView-ex.jpeg

参考

iPad - 違うフォントサイズのUILabelを複数、横に並べて下揃えしたい|teratail

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

構造体(Struct)のViewを配列にしてみる[Swift] - vol.1(設置まで)

Youtubeの素材用にTinderの模擬アプリを作成する際、
構造体(Struct)のViewを配列を使ったのでメモがわりに。(vol.1とvol.2があります)

Viewを用意

この辺はあとで追記します

GirlsView.swift
import UIKit

class GirlsView: UIView {
    @IBOutlet weak var shadowView: UIView! //could not insertは@IBOutletを先に書いて解決
    @IBOutlet weak var girlsImageView: UIImageView!
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var ageLabel: UILabel!
    @IBOutlet weak var typeLabel: UILabel!
    @IBOutlet weak var distanceLabel: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()
        //viewのautolayout取得前
        //VCの制約決定前
        //setAppearance()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        //viewのautolayout取得前後
        //VCの制約決定前
        //setAppearance()
    }

    //コードで設置した時
    //以降3つを書いてないとVC側で//girlsView.frame = self.view.frame書かないとViewが表示されない?
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
        //configure()
        setAppearance()

    }

    //storyboardで設置した時
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
    }

    func loadNib() {
        //どっちでもViewのUI表示してくれる
        if let view = Bundle(for: type(of: self)).loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)?.first as? UIView {
            view.frame = self.bounds
            self.addSubview(view)
        }

//        let view = Bundle.main.loadNibNamed("GirlsView", owner: self, options: nil)?.first as! UIView
//        view.frame = self.bounds
//        self.addSubview(view)
    }

    private func configure() {
        let nib = UINib(nibName: "GirlsView", bundle: nil)
        //nibは取得できたが、各要素の取得がダメになるらしい
        //Viewの各要素はfile's ownerに紐付けないとダメ?
        //File's OwnerのみにGirlsViewを紐付けた状態で繋ぐ->各要素が勝手に親のUIViewではなくFile's Ownerに紐づく
        //sportipはUIViewのextensionでロードのやり方を分けている?
        guard let view = nib.instantiate(withOwner: self, options: nil).first as? UIView else { return }
        addSubview(view)
    }

    func setAppearance() {
//imageViewに影つけたいので最下層に影専用のshadowViewを用意
        shadowView.layer.cornerRadius = 10
        shadowView.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
        shadowView.layer.shadowColor = UIColor.lightGray.cgColor
        shadowView.layer.shadowOpacity = 0.3
        shadowView.layer.shadowRadius = 4

        girlsImageView.contentMode = .scaleAspectFill
        girlsImageView.layer.cornerRadius = 10
        girlsImageView.layer.masksToBounds = true
    }
}

Viewを設置

customViewのインスタンスを5つ用意

var girlsViewArray = [GirlsView(), GirlsView(), GirlsView(), GirlsView(), GirlsView()]

ダメな例

let girlsView = GirlsView()
var girlsViewArray = [girlsView, girlsView, girlsView, girlsView, girlsView]
//これだと同じgirlsViewを5つ入れるだけになるので、インスタンス(新規View)が5つにならない
Array(0..<girlsViewArray.count).forEach {
            girlsViewArray[$0].translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(girlsViewArray[$0]) //$0は0 ~ girlsViewArray-1
            girlsViewArray[$0].layer.zPosition = CGFloat($0) + 1.0 //上に重ねていく
            girlsViewArray[$0].layer.cornerRadius = 20
            girlsViewArray[$0].layer.masksToBounds = true
            //制約つける //viewWillAppearでないと反映されない //addSubviewの後に書かないと反映されない
            girlsViewArray[$0].leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20.0).isActive = true
            girlsViewArray[$0].trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20.0).isActive = true
            girlsViewArray[$0].topAnchor.constraint(equalTo: self.view.topAnchor, constant: 84.0).isActive = true
            //84.0 = 44.0(navHeight) + 20.0(statusBarHeight) + 20(margin)
            girlsViewArray[$0].bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -20.0).isActive = true
}
  • forEachの他の書き方
girlsViewArray.forEach {
    print($0)  //こっちの$0は配列の中身そのもの
}

ポイント

  • xibを設置した時(VC以外で)はsafe Area消した方がいい(Safe Area Layout Guideのチェックを外す)
  • @IBOutletでUI追加時にCan not insert..と出る場合は、先にswiftファイルに@IBOutlet weak var...を書いてから繋げるとうまくいく(多分本当はFile's ownerからcustom classに参照を変えてもうまくいくと思われるがこっちの方が簡単なので)
  • UIImageViewに影つけたい時は影用のViewを別で用意し下に配置する(masksToBounds = trueにするのはUIImageViewのみ)->親Viewは背景をclearにしておけば特にいじる必要なし

参考

【Swift】for と forEach の書き方メモ
iPhone X対応 ~Safe Areaの外側~ - Qiita
Xcode6で「.xibファイル」+「自作UIViewクラス」で、汎用できるViewを作りたい - Qiita
UIImageView で cornerRadius と Shadow を同時に使いたい - Qiita

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

Facebook、Messangerで端末に保存されている画像をシェアする時に使用するattachment idの取得方法

この記事を書いたわけ

FacebookやMessangerに画像をシェアする時には基本的にはFacebook URLを使用して画像を選択します。ですが、この場合だとすでにFacebookに投稿されている画像しか選べないのです。なので端末に保存されている画像や、一般的な画像URLを指定してシェアしたい時は一度画像アップロードAPIを使用してからattachment idを取得し、その値を設定してやる必要があります。ですがこの画像アップロードAPIはSDKが用意されておらず、実装に大変苦労したのでこの記事を書いてみました。

Alamofire のインストール

今回APIの送信にはAlamofireを使います。cocoaPodsを使ってインストールしていきます。

cocoa Pods を利用して Alamofire をインストール

PodFileに以下を追加して、Alamofireをインストールしていきます。

# Alamofire
 pod 'Alamofire';

ターミナルで以下のコマンドを打って完了です。

pod install
pod update

Attachment IDの取得

ここらは実際に画像アップロードAPIを使ってattachment idを取得していきます。

Attachment upload api を使って Attachment IDを取得する

https://developers.facebook.com/docs/messenger-platform/reference/send-apiによるとFacebook URLを使用せずに画像をアップロードするためには、

curl  \
  -F 'message={"attachment":{"type":"image", "payload":{"is_reusable":true}}}' \
  -F 'filedata=@/tmp/shirt.png;type=image/png' \
  "https://graph.facebook.com/v5.0/me/message_attachments?access_token=<PAGE_ACCESS_TOKEN>"

のようにmultipart/form-data形式ででPOSTする必要があります。
そこでAlamofireを使って以下のようにAPI通信を行なっていきます。

    // multipart/form-data形式 で post request を実行する
    func postByMultipartFormData (
        requestUrl: String,
        params: [String : Any],
        image: UIImage,
        completion: @escaping (Result<DataResponse<Any>, APIManegerError>) -> ()) {

        // paramをJSONArray -> Dataに変換する
        let jsonParams: Data?
        do {
            jsonParams = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
        } catch {
            return completion(.failure(.parameterError(message: "有効なパラメータではありません")))
        }

        // imageをDataに変換する
        guard let dataImage : Data = image.jpegData(compressionQuality:1.0) else {
            return completion(.failure(.imageError(message: "有効な画像ではありません")))
        }

        // API送信
        Alamofire.upload(
            multipartFormData: { multipartFormData in
                // 送信する値の指定
                multipartFormData.append(dataImage,
                                         withName: "image",
                                         fileName: "hoge.jpeg",
                                         mimeType: "image/jpeg")
                multipartFormData.append(jsonParams!,
                                         withName: "message")
        },
            to: requestUrl, // 送信先URL
            // API送信の実行
            encodingCompletion: { encodingResult in
                switch encodingResult {
                case .success(let upload, _, _):
                    upload.responseJSON { response in
                        completion(.success(response))
                    }
                case .failure(let encodingError):
                    // 失敗
                    completion(.failure(.uploadError(error: encodingError)))
                }
        }
        )
    }

シェアする画像、リクエストURL、リクエストパラメータは以下のようにで指定します。

        // アクセストークン
        let accessToken = <自分のfacebookページのアクセストークン>
        // リクエストURL(attachment upload API)
        let requestUrl = "https://graph.facebook.com/v2.6/me/message_attachments?access_token=\(accessToken)"
        // リクエストパラメータ
        let params:[String:Any] = [
            "attachment":[
                "type":"image",
                "payload":[
                    "is_reusable": true
                ]
            ]
        ]     
        // シェアしたい画像を設定
        let image: UIImage = <投稿したい画像>

これらを使ってAttachment upload APIの実行、Attachment IDの取得をしていきます。

        // attachment upload APIのレスポンスを変換するためにCodableを定義
        struct AttachmentId: Codable {
           let attachment_id: String
        }

        // attachment upload API の実行
        postByMultipartFormData(requestUrl: requestUrl,
                                params: params,
                                image: image){ result in
                                                    switch result {

                                                    // Upload API成功時
                                                    case .success(let responce):
                                                        // attachment IDを取得する
                                                        if let data = responce.data {

                                                            // responceをAttachmentIdに変換する。
                                                            let responceDataJson: AttachmentId
                                                            do {
                                                                responceDataJson = try JSONDecoder().decode(AttachmentId.self, from: data)
                                                            } catch {
                                                                print("Json Decode error")
                                                                return
                                                            }

                                                            // attachmentIdを取得する
                                                            let attachmentID: String = responceDataJson.attachment_id
                                                            print(attachmentID) // 取得したattachment IDを表示してみる
        }
                                                    // Upload API失敗時
                                                    case .failure(let error):
                                                        print(error)
                                                    }
        }

これでやっとattachment IDを取得することができました。

491466764797083

attachment idを使って画像のシェアをしてみる

実際に作ったアプリではattachment idを使用してMessengerに画像シェアするところまで作成していたのですが、Messanger Platform Changelogによると「Messanger SDK は廃止するからネイティブ機能使ってね。」とのことです。チクショー!!
いまいちattachment idの使い道はないかもしれませんが、、、もし何かで必要な方がいらっしゃいましたら参考にしてください。

ちなみにソースは以下においてます。
https://github.com/mterada1228/FacebookCommon

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

サーバエンジニアから始める、Swiftロードマップ

こんにちは
taskey サーバエンジニアの田代です。

ふとSwiftを学ぶことになり、その時の軌跡をまとめておこうと思いnoteに残しました。

対象者: サーバエンジニアだけど、Clientのことをもっとわかってあげたい方、こまい修正位やっちゃいたい方

社内マガジンに載せる都合上noteにまとめました。
内容はこちらをご閲覧下さい。

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

Swiftにおける構造体(struct)vsクラス(class)問題に向き合ってみた

項目 構造体 クラス
タイプ 値型 参照型
格納型プロパティ
計算型プロパティ
スタティックプロパティ
クラスプロパティ ×
メソッド
スタティックメソッド
クラスメソッド ×
継承 ×
プロトコル準拠
拡張
メモリリーク回避
スレッドセーフ

Swiftにおける構造体とクラスの性質の違い、できる・できないことをまとめるとこんな感じになると思います。これを踏まえつつ、この記事ではアップルさんの見解をもとに構造体とクラスの使い分けの方針についてつづります。(結論を先に知りたい方はここ)

構造体とクラスの選択についてのアップルさんの見解

原文はこちらのリンクにあります。
https://developer.apple.com/documentation/swift/choosing_between_structures_and_classes

原文のみorその日本語直訳とかは眠すぎる内容になると思ったので、原文と筆者による日本語要約(要約でも眠さは残る)を載せます。大筋of大筋としてはSwiftでは構造体推し!ってことを言いたいのだと思います。

Overview

Structures and classes are good choices for storing data and modeling behavior in your apps, but their similarities can make it difficult to choose one over the other.

Consider the following recommendations to help choose which option makes sense when adding a new data type to your app.

  • Use structures by default.
  • Use classes when you need Objective-C interoperability.
  • Use classes when you need to control the identity of the data you're modeling.
  • Use structures along with protocols to adopt behavior by sharing implementations.

概要

アプリのデータや処理をまとめておくのに構造体やクラスは便利ですが、両者は似ているためどちらを使えばいいのか迷います。そんな時は判断基準として以下がおすすめです。

  • まずは構造体を使ってみる
  • Objective-Cやモデル化したデータの操作が必要ならクラスを使う
  • 実装を共有したいなら構造体はプロトコルと一緒に使う

Choose Structures by Default

Use structures to represent common kinds of data. Structures in Swift include many features that are limited to classes in other languages: They can include stored properties, computed properties, and methods. Moreover, Swift structures can adopt protocols to gain behavior through default implementations. The Swift standard library and Foundation use structures for types you use frequently, such as numbers, strings, arrays, and dictionaries.

Using structures makes it easier to reason about a portion of your code without needing to consider the whole state of your app. Because structures are value types—unlike classes—local changes to a structure aren't visible to the rest of your app unless you intentionally communicate those changes as part of the flow of your app. As a result, you can look at a section of code and be more confident that changes to instances in that section will be made explicitly, rather than being made invisibly from a tangentially related function call.

まずは構造体を使ってみる

Swiftの構造体は、他の言語ではクラスにしかできないような性質(プロパティとかメソッドを持ったりなど)を持っており、プロトコルを使って既定の実装に応じた振る舞いも可能です。

また、構造体は値型なので、ローカルで変更を加えても変更した部分以外には影響しません。アプリ全体の状態を気にすることなく、一部のコードを扱うことができるわけです。

Use Classes When You Need Objective-C Interoperability

If you use an Objective-C API that needs to process your data, or you need to fit your data model into an existing class hierarchy defined in an Objective-C framework, you might need to use classes and class inheritance to model your data. For example, many Objective-C frameworks expose classes that you are expected to subclass.

Objective-Cが必要ならクラスを使う

データ処理にObjective-CのAPIが必要だったり、Objective-Cのフレームワークで書かれたクラスにモデルを当てはめたいのであれば、クラスやクラス継承を使う必要があるかもしれません。

Use Classes When You Need to Control Identity

Classes in Swift come with a built-in notion of identity because they're reference types. This means that when two different class instances have the same value for each of their stored properties, they're still considered to be different by the identity operator (===). It also means that when you share a class instance across your app, changes you make to that instance are visible to every part of your code that holds a reference to that instance. Use classes when you need your instances to have this kind of identity. Common use cases are file handles, network connections, and shared hardware intermediaries like CBCentralManager.

For example, if you have a type that represents a local database connection, the code that manages access to that database needs full control over the state of the database as viewed from your app. It's appropriate to use a class in this case, but be sure to limit which parts of your app get access to the shared database object.

Important

Treat identity with care. Sharing class instances pervasively throughout an app makes logic errors more likely. You might not anticipate the consequences of changing a heavily shared instance, so it's more work to write such code correctly.

モデル化したデータを操作したいならクラスを使う

Swiftのクラスは参照型なので、クラスインスタンスに変更を加えれば、アプリ内のそのインスタンスの参照部分すべてに影響します。このような性質を求めるならクラスを使いましょう。

ただ、クラスインスタンスを共有することで、ロジックエラーなどの予期せぬ結果を招くことがあるのでクラスは慎重に扱いましょう。

Use Structures When You Don't Control Identity

Use structures when you're modeling data that contains information about an entity with an identity that you don't control.

In an app that consults a remote database, for example, an instance's identity may be fully owned by an external entity and communicated by an identifier. If the consistency of an app's models is stored on a server, you can model records as structures with identifiers. In the example below, jsonResponse contains an encoded PenPalRecord instance from a server:

struct PenPalRecord {
    let myID: Int
    var myNickname: String
    var recommendedPenPalID: Int
}

var myRecord = try JSONDecoder().decode(PenPalRecord.self, from: jsonResponse)

Local changes to model types like PenPalRecord are useful. For example, an app might recommend multiple different penpals in response to user feedback. Because the PenPalRecord structure doesn't control the identity of the underlying database records, there's no risk that the changes made to local PenPalRecord instances accidentally change values in the database.

If another part of the app changes myNickname and submits a change request back to the server, the most recently rejected penpal recommendation won't be mistakenly picked up by the change. Because the myID property is declared as a constant, it can't change locally. As a result, requests to the database won't accidentally change the wrong record.

モデル化したデータを操作したくないなら構造体を使う

変更したくないデータをモデル化するなら構造体を使いましょう。

リモートDBと接続するアプリでは、サーバー上でモデルの一貫性を保つなら、識別情報を持たせた構造体としてレコードをモデル化することができます。

コード例にあるPenPalRecordは構造体なので、ローカルで変更を加えてもDB内のレコードを操作することがなく、不意にDB内の値が変わってしまうといったリスクがありません。

Use Structures and Protocols to Model Inheritance and Share Behavior

Structures and classes both support a form of inheritance. Structures and protocols can only adopt protocols; they can't inherit from classes. However, the kinds of inheritance hierarchies you can build with class inheritance can be also modeled using protocol inheritance and structures.

If you're building an inheritance relationship from scratch, prefer protocol inheritance. Protocols permit classes, structures, and enumerations to participate in inheritance, while class inheritance is only compatible with other classes. When you're choosing how to model your data, try building the hierarchy of data types using protocol inheritance first, then adopt those protocols in your structures.

モデルを継承したり振る舞いを共有したいなら構造体とプロトコルを使う

構造体はクラスを継承することができませんが、プロトコル継承と構造体を使えばクラス継承のようなことが可能です。

データをモデル化する際は、プロトコル継承を使って型の階層を作り、それからそのプロトコルを構造体に準拠させるようにしましょう。

つまり、

筆者の知識不足でたどたどしい要約もありますが、以上がアップルさんの見解です。これを踏まえてまとめると、以下の2つの場合はクラスを使ってそれ以外は構造体を使いましょう、ということだと思います。

  • Objective-CのAPIを使ったり、そのAPIを含む標準フレームワークのクラス(UIViewControllerなど)を継承したりしたい
  • モデル化したデータをオブジェクト間で統一的に変更(参照渡しが適切)しやすくしたい

思えばSwiftを勉強したての頃もこの構造体vsクラス問題に悩み、実務経験を5ヶ月ほど積んだ今も悩んでます。多分これからも悩むんでしょうね。ただ、Swiftには構造体を扱いやすくするプロトコルなどの仕組みが多数用意されているので、構造体を使える場面がないか積極的に探していきたいなあと思った次第です。

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

【初学者向け】ややこしいSwiftの文字列切り出しの処理を理解する

Swiftで文字列切り出しを「何文字目から何文字目まで〜」みたいにやろうとした時、切り出し範囲の指定がめんどくさくて、ん????ってなりました。
Javaの文字列型のsubstringメソッドみたいにIntで指定できればいいのに。。。

今回は混乱したSwiftの文字列切り出しについて整理します。

目次

  1. 文字列切り出しの表記法
  2. 内部でなにがどうなってるのか
    1. Stringからサブスクリプトで文字にアクセスする
    2. 引数:範囲型(Range<String.Index>)
    3. 返り値:Substring
  3. まとめ

1.文字列切り出しの表記法

先頭から任意の文字数の切り出し

let str = "0123456789"
// 先頭から3文字切り出し
str.prefix(3) // -> "012"

ワカル。シンプル。

先頭から任意の文字数の切り出し

let str = "0123456789"
// 末尾から3文字切り出し
str.suffix(3) // -> "789"

ワカル。シンプル。

任意の位置から任意の文字数の切り出し

let str = "0123456789"
// 4文字目(先頭から前に3文字目ずらした文字)の位置を指定
let startIndex = str.index(str.startIndex, offsetBy: 3)
// 7文字目(末尾から後に4文字ずらした文字)の位置を指定
// endIndexは末尾+1文字目なので4文字ずらす
let endIndex = str.index(str.endIndex,offsetBy: -4)

str[startIndex]             // -> "3"
str[endIndex]               // -> "6"
str[startIndex...endIndex]  // -> "3456"

チョットマッテ。キュウニムズイ。

は?なんか最初の文字位置と最後の文字位置指定して最終的に添字みたいなやつでアクセスしてる?は?急になに?は?
雰囲気はふんわりわかる気がしますがこれ毎回ググるはめになるやつですね。。。
これを頑張ってしっかり理解しましょう。

※ここ以降は備忘録的な位置付けが強いので、書き方がわかればいい方は上記までを参照していただければ良いかと思います。

2. 内部でなにがどうなってるのか

最後の「任意の位置から任意の文字数の切り出し」で実際に文字列切り出しを行なっているのはこの部分。
str[startIndex...endIndex] // -> "3456"
この処理を紐解き、なにが起こっているのかを見ていきましょう。

2-1. Stringからサブスクリプトで文字にアクセスする

str[startIndex...endIndex] // -> "3456"
↓補足込みでかくと

image.png

これは文字列型に添字でアクセスして文字列を切り出しているように見えます。
コレを理解するために必要知識として、サブスクリプトという概念があります。
(参考:サブスクリプト | Swift言語を学ぶ - Tea Leaves

Swiftでは構造体やクラス、列挙型に添字式でアクセスするときにはsubscriptキーワードを使って定義することが決まっており、[]で定義されている処理はsubscriptメソッドの部分に書かれています。
ですので、String構造体についてsubscriptキーワードで記述されている処理を見れば、文字列切り出しで何をやっているのか理解できそうです。実際に定義が書かれている部分が以下です。

String構造体でのsubscript処理
@inlinable public subscript(r: Range<String.Index>) -> Substring { get }

これを見るに、str[startIndex...endIndex]で行なっている処理は、実のところsubscriptキーワードを使って定義されており、str文字列に対して、String.Indexの範囲型を引数にして、Substring型の返り値をgetする」という処理をしているようです。
おお。なんか理解した感が出てきました。

あとは、引数であるRange<String.Index>と、返り値であるSubstring型について何者なのか理解できれば、今後はいちいち理解が曖昧なままググらずに済みそうです。

2-2. 引数:範囲型(Range<String.Index>)

引数であるRange型は範囲型いう、その名の通り値のとりうる範囲を示す型となります。

そのRange型の<>内で示されている、String.Indexは文字列型の位置を示す型であり、String型から指定の位置のCharacter型の文字を取り出すときに使われます。

また、String.Indexを指定するときは、文字列の開始位置を示すstartIndexプロパティや、終了位置を示すendIndexプロパティ、特定の文字位置からのズレを指定することで任意の文字列の場所を取得するindex(_:,offsetBy:)プロパティで取得を行います。例えば、
let startIndex = str.index(str.startIndex, offsetBy: 3)
という処理では、「str文字列の最初の文字"0"を基準に3文字分ずらした文字位置を取ってきてくださいね」という処理をしているわけです。

つまり、Rangeは「文字列の位置(String.Index)を範囲で教えてください」ということで、最終的にstr[startIndex...endIndex]の引数として与えているstartIndex...endIndexの部分は切り出したい最初の文字列位置から最後の文字列位置までの範囲を示す必要があるというわけですね。

2-3. 返り値:Substring

よし、ここまでで文字列切り出し処理は、文字列型に対して文字位置範囲を引数に指定してあげることで取得してるのね、理解理解。ってなったと思ったらきました、(初学者に取っては)未知の型Substring。大人しくString型で返しとけや!!って感じですよね。

このSubstring型は部分文字列といい、切り出した部分だけでなく、元の文字列の全体への参照を保持します。そのため部分文字列を保存すると、文字列データの寿命が長くなり、メモリリークのように見える場合があるとのこと。長い文字列から切り出す場合は、String型にキャストしておいた方が無難なようです。

3. まとめ

subscript(r: Range<String.Index>) -> Substring { get }
で定義される文字列切り出しで行なっている処理は、
「文字列に対して、String.Indexで示された文字位置の範囲型を引数にして、Substring型の返り値をgetする」
ということをしてるんですね。

文字列切り出し完全に理解しました。

参考

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

C の配列を Swift が Tuple として扱うのが面倒なのを倒す

課題

Swift から C のライブラリを利用している際に、C の固定長配列を扱うのは少し煩わしいです。
なぜなら、Swift では Tuple として扱われるからです。
Tuple は Iteratable でないため、要素の参照に困ります。これをどうするか?

対応

方法1: reflection で倒す

https://qiita.com/codelynx/items/83f4b3829267d8d25b07 で述べられているように、reflection で Tuple を Array に変換する方法。
問題なくいけます。ただし、reflection は Compiler にとって負荷が高いため、要素が複雑だったりすると避けたくなります。

方法2: ポインタで倒す

var foo_list = func_with_c()
let ptr = UnsafeMutablePointer<Foo>(&foo_list.foo.0)

// "固定長"のサイズをベタに書く。例えば長さが 34 だと知っているとする。
for i in 0..<34 {
    let foo = ptr[i]
    print(foo)
}

var を使うことになるのが嬉しくないのですが、reflection を行わないので Compiler に優しいです。
特段の理由が無ければこう書くことが多いのかな..?と思っています。

参考: Swift Foundation UUID implement

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

GitHub Actions で 自作Cocoapodsライブラリを自動デプロイする

Treasure Advent Calendar 2019 7日目の記事です。

iOS向けライブラリを開発していたところ、バージョンを更新する際に毎回ローカルの環境で
pod trunk push Hoge.podspec をやっていたのですが、毎回やるのは面倒だということで自動でデプロイされるようにしました。
仕組みを作ろうとした際にハマったりしたので今後やろうとしている方の参考になると幸いです。

トークンを取得する

自作のCocoapodsライブラリをデプロイするためには環境変数にトークンを設定する必要があります。

ユーザー情報を登録する

pod trunk register コマンドを使用してユーザー情報を登録します。
コマンドを打つとメールが届くので、メール内にあるリンクに飛び、アクティベーションします。

pod trunk register ry-itto@example.com

トークンをファイルから取得する

ユーザー情報を登録したらローカル環境の ~/.netrc に以下のような情報が追加されていると思います。この情報の中の password がトークンに当たります。

machine trunk.cocoapods.org
  login ry-itto@example.com
  password jaeiwghwe83hlagw8

リポジトリに登録する

GitHubのリポジトリにアクセスし、設定画面を開きます。
スクリーンショット 2019-11-25 0.35.50.png

設定画面を開いたら、その中の"Secrets" を開きます。
スクリーンショット 2019-11-25 0.36.33.png

そしてその画面の中の"Add a new secret"を押し、
NameにCOCOAPODS_TRUNK_TOKEN, Valueに先ほど取得したトークンを入れます。

これでリポジトリへのトークンの登録は完了です。
スクリーンショット 2019-11-25 0.40.29.png
このようになっていればOKです。

ワークフローの設定ファイルを作成する

完成形は以下です。それぞれ部分ごとに説明をしていきます。
ワークフローの構文

name: pod-deploy

on:
  push:
    tags: v*

jobs:
  build:
    runs-on: macOS-latest
    steps:
    - uses: actions/checkout@v1
    - name: Lint
      run: pod spec lint
    - name: Deploy
      env:
        COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
      run: pod trunk push Hoge.podspec

処理を行うタイミングを設定する

v から始まるタグが作成された際に処理が行われるように設定。

on:
  push:
    tags: v*

行う処理を設定する

jobs:
  build: # 'build' という名前でジョブを作成
    runs-on: macOS-latest # GitHub Actionsで提供されているmacOSの最新版を使うように指定
    steps:
    - uses: actions/checkout@v1 # リポジトリの情報にアクセス
    - name: Lint
      run: pod spec lint # 作成したコードが正しいかチェック
    - name: Deploy
      env: # 環境変数に `COCOAPODS_TRUNK_TOKEN` を設定
        COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
      run: pod trunk push Hoge.podspec

自作したライブラリを公開するために COCOAPODS_TRUNK_TOKEN を環境変数に設定する必要があります。ここには序盤でリポジトリのSecretsに設定したトークンを設定します。
なお、リポジトリのSecretsに設定した値は secrets.設定したName で取得できます。

終わりに

思いの外簡単に自動デプロイの仕組みを作成することができました。
GitHub ActionsはPublicなリポジトリなら無料で利用できるため、ぜひ試してみてください。

おまけ

https://github.com/ry-itto/QiitaAPIKit
今回のワークフローを作成するにあたって自動化したライブラリです。フィードバックなどあればいただけるとありがたいです!

参考

Automated CocoaPod releases with CI

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