20210119のSwiftに関する記事は10件です。

[Swift]クロージャ

クロージャを理解しよう

クロージャは再利用可能なひとまとまりの処理です。
クロージャは、名前が不要であったり、型推論によって型の省略可能であったりと関数より手軽に定義ができます。

定義方法

引数と戻り値の型は関数と同様です。

{(引数名:)->戻り値の型 in
実行される文
必要に応じてreturn文
}

次の例では、Int型の引数を1つ受け取り2倍にして返すクロージャを定義しています.

let double={(x:Int)->Int in
    return x*2
}
double(2)//4

また、クロージャは変数や定数の型になったり、関数の引数の型として利用することも可能です。
(詳しくは後で説明します。)

型推論

クロージャの引数と戻り値の型はクロージャの代入先の型から推論することによって、省略するケースがあります。

//型を省略しない時
let double_1={(x:Int)->Int in
    return x*2
}
//型を省略する時
let double_2={ x in
    return x*2
}
double_1(2)//4
double_2(2)//4

ここで、クロージャdouble_1とdouble_2はletで宣言しているため、クロージャを変更することはできません。
しかし、以下のようにvar(変数)を使って宣言すれば、クロージャを変更しても型を変えなければエラーにはなりません.
以下のdouble_1の型は、(Int)->Int型です

//varで宣言
var double_1={(x:Int)->Int in
    return x*2
}
//letで宣言
let double_2={(x)in
    return x*2
}
double_1={x in
    return 3
}
double_2={x in
    return 3
}//エラー

double_1(2)//3
print(type(of: double_1))//(Int)->Int

クロージャの引数の特徴

外部引数名(外部から呼び出す時に、クロージャ内の引数と違う名前にすること)とデフォルト引数(クロージャを定義する時に、引数に値を代入すること)を使うことができません。

簡略引数名

クロージャの型が推論できるケースでは、型を省略できることは前述しました。
なんと、引数名も省略することができます!!が、可読性が落ちるので、使い分けをしましょう。

//簡略引数名なし
let isEqual_1:(Int,Int)->Bool={(x,y) in
    return x==y
}
//簡略引数名あり
let isEqual_2:(Int,Int)->Bool={
    return $0==$1
}
isEqual_1(2,2)//True
isEqual_2(2,2)//True

最後に引数としてのクロージャ

クロージャを関数や別のクロージャの引数として利用する場合の仕様として、属性とトレイリングクロージャがあります。
属性はクロージャの性質を追加する情報で、トレイリングクロージャは、クロージャを引数にとる関数の可読性を高めるための仕様です。

属性

属性は2種類あります。
escaping属性とautoclosure属性があります。
今回はescaping属性だけを説明します。

escaping属性

escaping属性は、関数の引数として渡されたクロージャを関数のスコープ外でも使用できることを表す属性です。
また、属性の前には@を追加します。

パターン1
var que=[()->Int]()
func number(operation: @escaping ()->Int){
     que.append(operation)
}
number(operation: {() in return 1})
number(operation: {() in return 2})

que[0]()//1
que[1]()//2


パターン2(簡略引数名)
var que=[()->Int]()
func number(operation: @escaping ()->Int){
     que.append(operation)
}
number(operation: { return 1})
number(operation: { return 2})

que[0]()//1
que[1]()//2


パターン3
var que=[()->Int]()
func number(operation: @escaping ()->Int){
     que.append(operation)
}
number{ return 1}
number{ return 2}

que[0]()//1
que[1]()//2


パターン1,2,3は同じ意味です。
number関数はqueの配列の中に()->Int型のクロージャを追加する関数です。
number{return 1}を実行するとque[0]に{ () in return 1}のクロージャを追加します。
次の,number{return 2}も同様に,que[1]に{() in return 2}を追加します。
また、上記のコードでescapingがないとエラーになります。

トレイリングクロージャ

パターン3は、まさにトレイリングクロージャそのものです。
関数の引数の最後がクロージャであった場合に、関数を呼び出す時、()を省略することができます。
以下の下二つがトレイリングクロージャを利用したパターンになります。

func execute (param:Int,operation:(String)->Void){
    operation("パラメータは\(param)です。")
}
//トレイリングクロージャを利用しない場合
execute(param:1,operation: { string in print(string)})//パラメータは1です。

//トレイリングクロージャを利用する場合
execute(param:2){ string in print(string) }//パラメータは2です
execute(param:3){  return print($0) }//パラメータは3です

以上で、クロージャの説明を終わります。

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

Swift 条件分岐文まとめ

はじめに

この記事には、Swiftで多用する条件分岐文についてまとめました。

参考文献

この記事は以下の情報を参考にして執筆しました。

目次

  1. if文 - 条件の成否による分岐
  2. guard文 - 条件不成立時に早期退出する分岐
  3. switch文 - 複数のパターンマッチによる分岐

1. if文 - 条件の成否による分岐

条件の成否に応じて実行する文を切り替える制御構文。
Bool型の値を返す条件式と、それに続く{}で囲まれた実行文から構成される。
条件式がtrueを返す場合は{}内の文が実行され、条件式がfalseを返す場合は{}内の文の実行がスキップされる。

if 条件式 {
条件式がtrueの場合に実行される文
}

-else節 - 条件不成立時の処理

条件が不成立の時、実行させたい文がある場合使用する。
if文の{}のあとにelseキーワードと{}に囲まれた文を続ける。

if 条件式 {
条件がtrueの場合に実行される文
} else {
条件式がfalseの場合に実行される文
}

また、else節は他のif文をつなげて書くこともできる。

if 条件式1 {
条件式1がtrueの場合に実行される文
} else if 条件式2 {
条件式1がfalseかつ条件式2がtrueの場合に実行される文
} else {
条件式1と条件式2の両方がfalseの場合に実行される文
}

-if-let文 - 値の有無による分岐

オプショナルバインディングを行う条件分岐文。
optional型の値の有無に応じて分岐を行い、値が存在する場合は値の取り出しも同時に行う。
以下のように、optional型の値を右辺に持つ定数定義を条件式に記述する。
右辺の値が存在する場合のみ{}内の文を実行し、nilの場合は{}内の文の実行をスキップする。
nilの場合のみ実行する文を追加するには、if文と同様にelse節を追加する。

if let 定数名 = optional<Wrapped>型の値 {
値が存在する場合に実行される文
} else {
値が存在しない場合に実行される文
}

また、if-let文では、複数のoptional型の値を同時に取り出すこともできる。
この場合、すべての右辺が値を持っていた場合のみ、if文の実行文が実行される。

2. guard文 - 条件不成立時に早期退出する分岐

条件不成立時に早期退出を行うための条件分岐文。
guardキーワード、条件式、elseキーワード、そして{}で囲まれた文から構成される。
条件式がfalseを返す場合のみ{}内の文を実行し、条件式がtrueを返す場合は{}内の文の実行をスキップする。
guard文のelse節では、guard文が含まれるスコープから退出しなければならない。
よって、guard文のelse節以降ではguard文の条件式が必ず成り立っていることがコンパイル時に保証される。
なお、guard文を含むスコープからの退出が含まれない場合、コンパイルエラーとなる。

guard 条件式 else {
条件式がfalseの場合に実行される文
guard文が記述されているスコープの外に退出する必要がある
}

また、guard文でもguard-let文として利用できる。
guard-let文とif-let文の違いは、guard-let文で宣言された変数や定数は、guard-let文以降で利用可能という点。
これは、条件式が満たされなかった場合にはスコープから出るため、guard文以降で変数や定数の存在が保証されるため。

3. switch文 - 複数のパターンマッチによる分岐

パターンを利用して制御式の値に応じて実行文を切り替える制御構文。
switch文の制御式にはどのような型の値でも指定できる。
switch文では制御式がパターンとマッチするかを上から順に評価し、マッチしたパターンの実行文が実行される。
一度マッチして実行するとマッチングは終了し、それ以降のパターンはスキップされる。
switch文の各パターンはcaseキーワードで定義し、どのパターンにもマッチしなかった場合の実行文はdefaultキーワードでデフォルトケースとして定義する。

switch 制御式 {
case パターン1:
制御式がパターン1にマッチした場合に実行される文
case パターン2:
制御式がパターン2にマッチした場合に実行される文
default:
制御式がいずれのパターンにもマッチしなかった場合に実行される文
}

また、ケースが網羅されていないとコンパイルエラーになる。
なので、制御式が取り得るすべての値をいずれかのケースにマッチさせる必要がある。

defaultキーワード - デフォルトケースによる網羅性の保証

他のいずれのケースにもマッチしない場合マッチするケースで、defaultキーワードで定義する。
デフォルトケースが存在すればマッチしないケースは存在しなくなるため、デフォルトケースは網羅性を保証する役割を担っている。
ただし、列挙型の制御式に対してデフォルトケースを用意することは極力避け、個々のケースを列挙する方が好ましい。

whereキーワード - ケースにマッチする条件の追加

ケースにマッチする条件を追加できる。

switch 制御式 {
case パターン where 条件式:
制御式がパターンにマッチし、かつ、条件式を満たす場合に実行される文
default:
制御式がいずれのパターンにもマッチしなかった場合に実行される文
}

break文 - ケースの実行の中断

switch文のケースの実行を中断する文。
breakキーワードのみか、breakキーワードと後述するラベルの組み合わせで構成される。

ラベル

break文の制御対象を指定するための仕組み。
break文の対象となるswitch文を明示する必要があるケースで利用する。
ラベルによってswitch文を参照可能にするには、break文の前にラベル名:を追加する。

ラベル名: switch文

breakキーワードに続けてラベル名を追加することで、対象のswitch文を明示。

break ラベル名

fallthrough文

switch文のケースの実行を終了し、次のケースを実行する制御構文。
fallthrough文は、fallthroughキーワードのみで構成される。

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

クラスと構造体の違いってなんだろう???

はじめに

今回は、クラスと構造体の違いを備忘録としてまとめようと思います。

クラスと構造体

クラスと構造体は値の受け渡し方法によって、値型参照型の2つに分けることができます。

ここでいう「型」というのはString型やInt型といったものではなくて
あるオブジェクトをどのように扱うべきかが事前に決まっている仕組みのことを表します。

そして、この2つの最大の違いは変更をほかの変数や定数と共有するかどうかにあります。

・値型は変更を共有しない (例: struct, enum)
・参照型は変更を共有する (例:class)

実は、値型は構造体だけではなく列挙型(enum)も該当します。

値型

インスタンスが値への参照ではなく値そのものを表す型

つまり、一度、代入したインスタンスは
再代入を行わない限り不変であり、その値が予測可能というメリットがあります。

コードで動きを見てみましょう。

色をRGB型で表すColor型という構造体のインスタンスの受け渡しがどうなるのかを見ていきましょう。

まず、struct Colorを定義して3つの変数を用意します。
そして、変数aに先ほど定義したColorを代入します。
その後、変数bにaを代入してaのredの値を変更します。

struct Color {
   var red: Int
   var blue: Int
   var green: Int
}

var a = Color(red: 255, blue: 0, green: 0) // 変数aに対して赤色を代入
var b = a // 変数bにaを代入

a.red = 0 // aのredを変更して黒色に変更する

// 結果↓

//aは黒になったが...
a.red // 0
a.blue // 0
b.green // 0

//bは赤のままである!
b.red // 255
b.blue // 0
b.green // 0

結果を見てみると、aを変更してもbに影響はありません
ここで分かることは、aをいくら変更してもbは影響を受けないということですね。

以上の例えから、値型は変数・定数への代入や関数への受け渡しのたびに値をコピーしていることが分かります。

参照型

インスタンスが値への参照を表す型

値型と違って変数・定数への代入時や関数への受け渡し時にはインスタンスのコピーが発生しないので、効率的なインスタンスへの受け渡しができるというメリットがあります。

例えとして、Int型の値をプロパティとして持つ参照型のクラスを見てみましょう。

変数aに対して定義したIntBoxクラスを代入し、変数bにaを代入します。

変数bにaを代入するという事は、aが参照しているインスタンスIntBox(value: 1)をbも参照している
という事になります。

つまり、変数aとbは同じ1つのインスタンスIntBox(value: 1)を持っているということになるんですね。

なので、下記のように変数aのvalueを変更すると、変数bの値も一緒に変わります。

class IntBox {
  var value: Int

  // 初期化
  init(value: Int) {
    self.value = value
  }
}

var a = IntBox(value: 1) // aはIntBox(value: 1)を参照する
var b = a // bはaと同じインスタンスを参照する

// 両方とも1である
a.value // 1
b.value // 1

// a.valueに対して2を代入すると...
a.value = 2

// b.valueの値も2に変わる!
a.value // 2
b.value // 2

値型と参照型はどう使い分ければいいのか?

じゃあ、実際に開発する際にこの2つをどうやって使い分けるのでしょうか?
私も詳しいことはよく分からないので調べてみました。

色々、調べてみると分かりやすい記事がいくつかあったので載せておきますね↓
Swiftで構造体とクラスを使い分ける方法(ポイント)
【Swift】構造体とクラスの使い分け
Swiftのクラスと構造体の使い分けについてのメモ

おわり

間違っている部分があれば、遠慮なくコメントして下さると有り難いです。

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

Swiftのカスタムセルの基本の基

Swiftのカスタムセルの基本の基

Swiftのカスタムセルをなんとなく利用していましたので、自己理解のために基本の基本をここで改めてまとめておこうと思います。

そもそもカスタムセルとは

UITableViewの一つ一つを表示するセル部分を表示するのが「UITableViewCellクラス」になります。

XCodeの方ではシンプルなUITableViewCellのデザイン・構成が元から準備されています。

しかし、元から準備されている構成では表現したい内容が異なる場合など、オリジナルの構成を作りたい場面が多くあります。
その様な際に作成できるのがカスタムセルで、UITableViewCellクラスを用いて自身の好きな様に見た目・内容をカスタムして利用することができます。

カスタムセルの時はxib化がおすすめ

UITableViewCellのカスタムをStoryboardのUITableView上に直接配置し利用することが可能です。

しかし、別画面で流用などができず、同様のセルを利用したいときは都度UITableViewCellのカスタムを作成しなけれないけなくなります。
DRY原則に従うためにも、流用できる形でセルを作成できた方が効率的です。

その、「流用できる形で作成できるカスタムセル」を作る方法が「xib化すること」です。

※DRY原則:Don't Repeat Yourselfの略。コード・情報を「繰り返しを避ける・重複を避ける」ことを意味します。

xib化とは

作成するUITableViewCell単体のxibファイルを作成し、それを利用することです。
※詳細の作成方法は割愛

xib化したUITableViewCellのメソッド

func awakeFromNib()

StoryboardまたはnibファイルなどでCellがロードされた直後に呼ばれるメソッド

func setSelected()

セルがタップされ、選択状態/通常状態が切り替わるときに呼ばれる処理メソッド

カスタムセルの高さを可変式にする際のポイント

初期設定ではセルの高さは固定されています。
Tableviewのメソッドで高さを指定しても、全てのセルが同一の固定値で指定されてしまいます。
そこで、以下の方法を使えばセルの内容に合わせてセルの高さを可変式に変更することができます!

・UITableView内のレイアウトで縦軸の全てにAutoLayoutを設定する
(CellのTopからBottomまで間にあるtextviewやbottonなど全て漏れなく上下AutoLayoutを設定してください。固定にしたいパーツは高さを固定値に、可変式にしたいパーツは高さを設定しないでください)

・TableViewに想定値を設定する
(必須ではない。事前に想定の高さがわかるこおでcellの処理が早くなるらしい)

override func viewDidLoad() {
      super.viewDidLoad()
      tableView.estimatedRowHeight = 50
      tableView.rowHeight = UITableViewAutomaticDimension
}

・tableviewでheightの指定をしない
以下のメソッドで高さを指定できますが、固定値になってしまうのでこのメソッドを利用しない様にしましょう

//これを設定しない!!
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 10
    }

まとめ

・xib化してカスタムセルを作成しましょう
・xib化した時は、最初から2つメソッドがあるので、必要に応じて利用しましょう。
・高さを可変式にしたい時は、可変式用の設定をしましょう。

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

Swift UITextFieldをコードで実装

UITextFieldをコードで実装してみる

swiftというかアプリ開発の入門書をみるとstoryboardにviewをぺたぺたドラッグ&ドロップするやり方ばっかですよね。
それでもいいんですがワンステップ上に上がりたい方のためにコード実装の仕方を記事にします。
まずは変数を定義しましょう。

let textField = UITextField()

この状態でビルドしても何も表示されません。
まあ、変数を宣言しただけなんで当たり前ですよね。
次に、ViewControllerviewに入れましょう。

view.addSubview(textField)

これで追加できました。ビルドしてみましょう。
Simulator Screen Shot - iPod touch (7th generation) - 2021-01-19 at 16.23.12.png
あれ?何も表示されませんね。
実はこれあるにはあるんですが幅も高さも0の状態なんです。
幅や高さを指定してあげましょう。

// .init(x軸方向の位置, y軸方向の位置, 幅, 高さ)
textField.frame = .init(x: 0, y: 0, width: 200, height: 40)
// 画面の真ん中に表示
textField.center = view.center

これで実行すると〜一見何もないように思えますが真ん中をタップすると編集できます。
placeholderも指定してあげましょう。

textField.placeholder = "ここに入力"

これでちゃんと表示されていることが確認できますね。
でも、枠も何もないのはわかりずらいですよね。てことで、枠を追加しましょう。

textField.layer.borderWidth = 1
textField.layer.borderColor = UIColor.black.cgColor

Simulator Screen Shot - iPod touch (7th generation) - 2021-01-19 at 16.33.47.png
できましたね!
あとはもう少し見栄えをよくしましょう。

// 角を丸くする
textField.layer.cornerRadius = 5
// border線と文字が近すぎるから少し離す
textField.leftView = UIView(frame: .init(x: 0, y: 0, width: 5, height: 0))
textField.leftViewMode = .always

これでビルド!
Simulator Screen Shot - iPod touch (7th generation) - 2021-01-19 at 16.37.43.png
いい感じに角も丸くなってボーダー線と文字の間にも隙間ができていますね。

textFieldのあれこれ

//勝手に英語のスペルを正しいのにする機能をoff
textField.autocorrectionType = .no
//勝手に英語の先頭の文字を大文字にするのをoff
textField.autocapitalizationType = .none
// fontサイズを変更
textField.font = .systemFont(ofSize: 18)
// textFieldを編集モードにする
textField.becomeFirstResponder()
// textFieldの編集モードを解除する
textField.resignFirstResponder()

やりたいことに応じてつかってみてください。
今回はここまでにします。
読んでくださりありがとうございます。

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

【Swift】PhotoKitで画像選択UIを作る

Custom UIs
LINEやTwitter、Pinterestなど、写真を扱う多くのアプリにはカスタマイズされた画像選択UIが実装されています。
今回はこのようなUIをPhotoKitを使って実装してみます。

デフォルトのPickerControllerはカスタマイズ性が低い

"Swift 画像 選択" などで検索すると、UIImagePickerControllerによる実装がいくつかヒットします。
何も考えずにパパッと実装したい場合には便利なのですが、UIや表示内容のカスタマイズ性が制限されるため、デザインの選択肢が狭まってしまいます。

PhotoKitでフォトライブラリにアクセスする

そこで、PhotoKitを用いて直接フォトライブラリからデータを取得してUIに設定するという方法を考えます。

ユーザにアクセスを許可してもらう

まずはUIImagePickerViewControllerと同様、Info.plistNSPhotoLibraryUsageDescriptionを追加します。
(ここで記述した文字列がアクセス要求の際に表示されます)
スクリーンショット 2021-01-19 11.27.00.png

次に、画像を読み込みたいタイミングでauthorizationStatus(for:)を呼び出し、フォトライブラリへのアクセスが許可されているかを確認します。

アプリの要求する許可が与えられていない場合は、requestAuthorization(for:handler:)でユーザに許可を促します。

// フォトライブラリへのアクセスを要求する
func requireAuthToPhotoLib(){
    let currentStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)

    // 許可されている?
    if [.authorized, .limited].contains(currentStatus){
        print("User has already granted access to the library.")
        return
    }

    // 許可をリクエスト
    PHPhotoLibrary.requestAuthorization(for: .readWrite) { (status) in
        switch status {
        case .authorized: // 「すべての写真へのアクセスを許可」
            print("Authorized!")

        case .denied: // 「許可しない」
            print("Denied")

        case .limited: // 「写真を選択」
            print("Limited access")

        case .notDetermined: // ユーザがどうするか決めてない
            print("Not determined")

        case .restricted: // ユーザはフォトライブラリへのアクセスを許可できない
            print("Restricted")

        @unknown default:
            fatalError("!?")
        }
    }
}

requestAuthorization(for:handler:)を呼び出すと、このようなプロンプトが表示されます。

ここで「写真を選択…」をタップすると .limited
すべての写真へのアクセスを許可」をタップすると.authorized
許可しない」をタップすると.deniedが返ります。

フォトライブラリからデータを取得

フォトライブラリの内容を取得するにはfetchAssets(with: PHFetchOptions)を使用します。
PHFetchOptionsには以下のように様々なオプションを設定できます。(引用: 公式ドキュメント)

スクリーンショット 2021-01-19 16.15.27.png

が、今回は単純に撮影日時の降順に10枚取得するように設定しました。

let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
fetchOptions.fetchLimit = 10
let fetchResult = PHAsset.fetchAssets(with: fetchOptions)

fetchAssetsの戻り値PHFetchResultは配列のように添字で参照でき、.countでサイズの取得も可能です。

fetchResult[0].mediaType == .image // true
fetchResult.count // 10

しかし公式ドキュメントによると、配列のように全ての値がその瞬間用意されているわけではないようです。

... Unlike an NSArray object, however, a PHFetchResult object dynamically loads its contents from the Photos library as needed, providing optimal performance even when handling a large number of results. ...

したがって、mapfilterで処理するといったことはできません

PHAssetからサムネイル画像を生成

次に、取得したPHFetchResultよりPHAssetを取り出し、サムネイルを生成します。
PHCachingImageManager.requestImage(for:)を使用することでUIImageに変換できます。

// (UICollectionViewにUIImageViewを載せて表示させる想定)
let cell: CollectionViewCell = .......

DispatchQueue.global().async {
    PHCachingImageManager().requestImage(
        for: asset,
        targetSize: self.layout.itemSize,
        contentMode: .aspectFill,
        options: nil
    ) { (image, nil) in

        // 画像の準備が完了したときに呼び出される
        DispatchQueue.main.async {
            cell.image = image
        }
    }
}

公式ドキュメントによると、handlerは複数回呼び出されることがあるようです。

For an asynchronous request, Photos may call your result handler block more than once.
Photos first calls the block to provide a low-quality image suitable for displaying temporarily while it prepares a high-quality image.
(If low-quality image data is immediately available, the first call may occur before the method returns.)
When the high-quality image is ready, Photos calls your result handler again to provide it.

画像の表示にやたら時間がかかるという状況を回避するために、handlerにはあらかじめキャッシュしておいたデータを先に渡す仕組みになっています。
handlerの中でUIを更新することで、ユーザの待機時間を最小限に抑えることができます。

UIに反映

最後に、生成したUIImageをUIImageViewに反映して…

完成です!

(UIについては様々な実装方法があると思うので、ここでは割愛します。コードの詳細はGitHubを参照してください。)

最後に

ここまで読んでいただきありがとうございました。

PhotoKitはかなり古くからあるフレームワークらしいのですが、今回調べて初めて知りました。
なかでもrequestImage()の美しさには驚きました。handlerが複数回呼ばれるのに合わせてUIImageViewが更新されていくのは非常にそれっぽさがありますね(個人的に)。

WWDC2020にて PHPickerViewController が紹介されていたようなので、そちらもまた触ってみようと思います。

ソースコードはGitHubにて公開しておりますので、「この実装ダメだよ!」とか「こっちのクラス(メソッド)使ったほうがいいよ!」等ございましたらコメントまたはPR頂ければ幸いです。

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

[Swift5].gitignoreファイルの作成方法とデフォルトで記述しておくべきコード

.gitignoreとは

要約すると.gitignoreとは、APIKeyなどの他者に知られることで問題が発生する恐れのあるコードをgit(Github)にアップしないファイルを設定するものです。

セキュリティの一環として扱われています。

.gitignoreファイルの作成方法

ターミナルで対象プロジェクトのディレクトリに移動して以下コマンドでファイル作成

% touch .gitignore   

作成したファイルを以下コマンドで開く

% open .gitignore     

開いたファイルに以下コードを記述

# Xcode
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate

# CocoaPod
Pods/*

# others
*.swp
!.gitkeep
.DS_Store

また、開発の途中で(すでにgitにコミットしていた場合).gitignoreを作成する場合は、
以下コマンドで一度全てのファイルをgitの対象外に設定

 git rm -r --cached .

次に、改めてgitの対象に設定

git add . 

この二つのコマンドを実行することで開発途中からでも.gitignoreに登録したファイルはアップされなくなる。

もっとも、プロジェクト作成直後に.gitignoreファイルを作成しておくにこしたことはない。

備考

Macのデフォルト設定として.(ドット)から始まるファイルは閲覧できなくなっていおるので、.gitignoreファイルを作成してもFinderなどでは表示されない。(なのでターミナルから開く)

Finder等できっちり確認したい方はMac の Finder で隠しファイルの表示/非表示を切り替える方法を参考にしてください。

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

【SwiftUI】一旦動く物が作りたい私へ

はじめに

対象としない読者:ちゃんとswiftを勉強したい人、「正しく」swiftを書きたい人、スタイリッシュなUIのものを作りたい人
対象とする読者:「あ、うごいた」を体験したい人、私

環境

もの バージョン
Xcode 12.3
mac Catalina
iPhone iOS14

「初めましてswiftさん」と言う状態。
これからappチームの人たちと関わることになったのでお勉強してみましたレベル。

かの有名な 挫折しない iPhoneアプリ開発「超」入門 第8版 【Xcode 11 & iOS 13】 完全対応を買ってサンプルコードを打ってみました、レベル。

こちらはxcode11の本ですが、自身のiPhoneがiOS14でxcode12を使う必要があった(と思う)ので最終的にxcode12にしました。

作った物

天気のAPIとニュースのAPIから情報を取得するiOSアプリ
大好きな観光地のベトナム:ダナンの情報を取得する

イメージ

初期表示

画面の状態 初期表示 「ダナンの天気」ボタン押下 「ニュース」ボタン押下
画面イメージ
関係のあるモジュール ContentView.swift ContentView.swift OpenWeatherMap.swift ContentView.swift NewsView.swift NewsAPI.swift

セットアップ

xcodeを落としてくる。

最新(xcode12)がいいならmacのApp Storeから取得できます。

ライブラリはCocoaPodsで管理する。

導入方法はこちらを参考にさせていただきました。
iOSライブラリ管理ツール「CocoaPods」の使用方法

インストール先は自身の作業プロジェクト配下です。
私の場合は /Users/{小松菜}/Practice/arekoreVNでした。

ベトナムが好きすぎて、これからもっといろいろ足したいのであれこれベトナムにしました。
抽象的なのでシステムのネーミングとしてはよくないですね。

※セットアップの途中でエラーになったら適宜対応してください。
私はRuby関連でエラーになったので、メッセージをみながら都度対応しました。
基本的に足りていない物をインストールしたり、古い物をバージョンアップしたり、という作業が発生します。
キーワードでぐぐると結構出てくるので安心してエラーメッセージと向きあえました。

CocoaPodのインストールでpod installコマンドを実施したと思います。
するとプロジェクト配下にワークスペース(.xcworkspaceファイル)ができていますね?
それをひらけたらコーディング開始です。
感動ですね。はじめの一歩です。

が、開けたらもう一度閉じてください。
そしてPodfile を以下のように修正して再度pod installしてください。

# Uncomment the next line to define a global platform for your project
 platform :ios, '12.0'
use_frameworks!
install! 'cocoapods',
            :warn_for_unused_master_specs_repo => false

target '自身のプロジェクト名' do
 # Comment the next line if you don't want to use dynamic frameworks

 # Pods for 自身のプロジェクト名
  pod "SwiftyJSON"

end

これで準備は完了です。(多分)

初期表示画面を実装する

早速画面を作っていきます。

この画面ですね。
初期表示画面自体はContentView.swiftに実装しました、

以下、ソースにコメントアウトする形で説明を入れていきます。
※ 先に1つ下の 画像(アイコン)の登録の作業からしてもらってもOKです。

ContentView.swift
// 初期表示画面。

import SwiftUI

/**
 初期表示画面。
 ダナン の天気の表示とニュースのページに遷移するリンクがある
 */
struct ContentView: View {

    // お天気情報 // このあと、APIでとってきたデータをセットする。
    @State var result = ""
    @State var temp = ""
    @State var humidity = ""
    @State var weathericon = ""

    // 画面表示
    var body: some View {
        NavigationView {
            VStack() {
                ScrollView {  // ScrollViewでスクロールを可能にする
                    VStack() {
                        Text("今日のベトナム")
                            .font(.largeTitle)
                            .fontWeight(.bold).foregroundColor(Color.white)
                            .lineLimit(nil)
                            .padding(.all, 30)
                            .padding(.bottom,50)

                        HStack{
                            Button(action: {}) { // actionは今は空。このあとopenweathermapにアクセスする処理を書いて呼び出す。
                                Image("weather").resizable().frame(width: 90, height:90)
                                Text("ダナンの天気")
                                    .fontWeight(.bold).font(.title)
                                    .foregroundColor(Color.white)
                            }
                        }
                        // 天気情報を取得できたときのみ、天気情報のパーツを表示させる。
                        if (self.result.count != 0 ){
                            VStack{
                                Group{
                                    VStack{
                                        Image("WeatherIcons/\(weathericon)").resizable().frame(width: 200, height: 200).padding(.all,-40)
                                        Text("\(result)").padding(.top, 0)
                                    }
                                    HStack{
                                        Text("気温 ")
                                            .padding([.top, .leading])
                                        Text("\(temp)")
                                            .padding([.top, .leading])
                                    }
                                    HStack{
                                        Text("湿度 ")
                                            .padding([.top, .leading])
                                        Text("\(humidity)")
                                            .padding([.top, .leading])
                                    }
                                }.font(.title2)
                            }
                            .padding(50)
                            .background(Color.white.opacity(0.7))
                        }
                        HStack{
                                Image("news")
                                    .resizable()
                                    .frame(width: 90, height: 90)
                                Text("ニュース")
                                    .fontWeight(.bold).font(.title)
                                    .foregroundColor(Color.white)
                        }
                    }
                    //背景画像(bg)を全画面にセットしたかったので以下のようにしてい
                }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
                .background(Image("bg"))
            }
        }
    }
}

/**
 初期画面プレビューの表示
 */
// これがないとXcode上でプレビューを表示できないので忘れずに。
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

画像(アイコン)の登録

プロジェクト配下のAssets.xcassetsに格納します。
ネットでフリー配布されているものの色をいじって使いました。
image.png

OpenWeatherMap APIから天気情報を取得する

この画面部分の作成に入ります。

OpenWeatherMapへの登録

天気情報の取得はOpenWeatherMapWeather Maps 2.0を使いました。
会員登録すれば個人利用であれば無料で使えます。(アクセス数の上限はあり)

詳しい使い方は公式のドキュメントを読んでみてください。
シンプルな英語で書かれているので比較的読みやすいかと思います。

appidが発行できたら、天気を取得したい地域自身のappidをセットしてアクセスしてきましょう。
今回はベトナムのTuranの地域(q=Turan,vn)にしました

https://api.openweathermap.org/data/2.5/weather?q=Turan,vn&appid={自身のappid}&lang=ja&units=metric

するとjsonが返却されます。

json → swift に変換する

返却されたjsonの値をquicktype に突っ込んでswift化しました。

APIの実装

こちらもソースにコメントを追加する形で説明します

OpenWeatherMap.swift
// APIからお天気情報取得して格納する処理たち

/** json -> swiftに直す処理をする
 https://app.quicktype.io/ で自動生成したもの
 使わないものもあるので適宜削除してよし。
 */
import SwiftUI

// MARK: - Templatures
struct Templatures: Codable {
    let coord: Coord
    let weather: [Weather]
    let base: String
    let main: Main
    let visibility: Int
    let wind: Wind
    let clouds: Clouds
    let dt: Int
    let sys: Sys
    let timezone, id: Int
    let name: String
    let cod: Int
}

// MARK: - Clouds
struct Clouds: Codable {
    let all: Int
}

// MARK: - Coord
struct Coord: Codable {
    let lon, lat: Double
}

// MARK: - Main
struct Main: Codable {
    let temp, feelsLike, tempMin: Double
    let tempMax, pressure, humidity: Int

    enum CodingKeys: String, CodingKey {
        case temp
        case feelsLike = "feels_like"
        case tempMin = "temp_min"
        case tempMax = "temp_max"
        case pressure, humidity
    }
}

// MARK: - Sys
struct Sys: Codable {
    let type, id: Int
    let country: String
    let sunrise, sunset: Int
}

// MARK: - Weather
struct Weather: Codable {
    let id: Int
    let main, weatherDescription, icon: String

    enum CodingKeys: String, CodingKey {
        case id, main
        case weatherDescription = "description"
        case icon
    }
}

// MARK: - Wind
struct Wind: Codable {
    let speed: Double
    let deg: Int
}


/**
 OpenWeatherMap からお天気情報を取得する。
 */
//結果を返す関数の定義
typealias RecvFunc = ((_ item:Templatures) -> Void)?

//OpenWeatherMapお天気情報を取得するクラス
class OpenWeatherMap {

    // 天気を取得する地域の指定
    let locale = "Turan,vn"
    // 本当はソースに直打ちではなく、設定ファイルなどで管理するべきだけど、いったん置いておく。
    let appid = "自身のappid"

    var _action: ((_ item:Templatures) -> Void)?

    // 結果受け取り
    func SetAction(action :((_ item:Templatures) -> Void)?){
        self._action = action
    }

    // 天気情報の取得
    func getWeather(action:RecvFunc) -> Void {
        self.SetAction(action: action)

        // APIのコール
        let VNWeatherURL="https://api.openweathermap.org/data/2.5/weather?q=\(locale)&appid=\(appid)&lang=ja&units=metric"
        guard let url = URL(string: VNWeatherURL)
        else
        {
            print("URLがおかしいので処理を続けられませんでした。")
            return
        }

        let request = URLRequest(url: url)
        URLSession.shared.dataTask(with: request){
            data, request, error in
            // 取得データをデコード
            if let data = data {
                if let decodedResponse = try? JSONDecoder().decode(Templatures.self, from: data){
                    DispatchQueue.main.async {
                        self._action!(decodedResponse)
                    }
                }
            }
        }
        // タスクの実行
        .resume()
    }
}

これでAPIに接続する部分はOKです。

天気のアイコンはOpenWeatherMapから取得出来ます。
私はダウンロードして./Assets.xcassets/WeatherIconsに格納しました。
xcodeはファイル名からよしなに画像を引っ張ってきてくれます。
私はなんか嫌だったので以下の設定をしました。
これ以降のソースもこの設定をしていることが前提です
【Xcode】画像をフォルダ管理したい

次は画面のボタンを押下してこの処理を呼び出せるようにします。

ContentView.swift
// 初期表示画面。
import SwiftUI

/**
 初期表示画面。
 ダナン の天気の表示とニュースのページに遷移するリンクがある
 */
struct ContentView: View {

    // お天気情報
    @State var result = ""
    @State var temp = ""
    @State var humidity = ""
    @State var weathericon = ""

    var weatherObj = OpenWeatherMap()

    //取得したお天気情報の編集
    func editData(data : Templatures) {
        self.result = ""
        self.temp = "\(data.main.temp)℃"
        self.humidity =  "\(data.main.humidity)%"

        // 天気データを複数取ることもあるため、1列で表示させる処理を追加)
        data.weather.forEach{
            item in
            if (self.result.count == 0 )
            {
                self.result = item.weatherDescription
            }else {
                self.result = self.result + "、" + item.weatherDescription
            }
        }
        // アイコンの設定
        let icon = data.weather[0].icon
        weathericon = icon
    }


    // 画面表示
    var body: some View {
        NavigationView {
            VStack() {
                ScrollView {
                    VStack() {
                        Text("今日のベトナム")
                            .font(.largeTitle)
                            .fontWeight(.bold).foregroundColor(Color.white)
                            .lineLimit(nil)
                            .padding(.all, 30)
                            .padding(.bottom,50)

                        HStack{
                            Button(action: {weatherObj.getWeather(action: self.editData) }) {
                                Image("weather").resizable().frame(width: 90, height:90)
                                Text("ダナンの天気")
                                    .fontWeight(.bold).font(.title)
                                    .foregroundColor(Color.white)
                            }
                        }
                        // 天気情報を取得できたときのみ、天気情報のパーツを表示させる。
                        if (self.result.count != 0 ){
                            VStack{
                                Group{
                                    VStack{
                                        Image("WeatherIcons/\(weathericon)").resizable().frame(width: 200, height: 200).padding(.all,-40)
                                        Text("\(result)").padding(.top, 0)
                                    }
                                    HStack{
                                        Text("気温 ")
                                            .padding([.top, .leading])
                                        Text("\(temp)")
                                            .padding([.top, .leading])
                                    }
                                    HStack{
                                        Text("湿度 ")
                                            .padding([.top, .leading])
                                        Text("\(humidity)")
                                            .padding([.top, .leading])
                                    }
                                }.font(.title2)
                            }
                            .padding(50)
                            .background(Color.white.opacity(0.7))
                        }
                        HStack{
                                Image("news")
                                    .resizable()
                                    .frame(width: 90, height: 90)
                                Text("ニュース")
                                    .fontWeight(.bold).font(.title)
                                    .foregroundColor(Color.white)
                        }
                    }.padding().frame(width: nil)
                }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
                .background(Image("bg"))
            }
        }
    }
}

/**
 初期画面プレビューの表示
 */
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

これで「ダナンの天気」ボタンを押すとAPIでデータを取得して表示が出来ます。
このAPIの呼び出しはこの部分ですね。
Buttonに呼び出すactionを設定します。以下です。
Button(action: {weatherObj.getWeather(action: self.editData) }) { ... }

最後にニュース画面の作成、画面遷移、画面遷移時のAPIの呼び出しを実装します。

ニュース画面の実装

この部分ですね。行き詰まりすぎて投げ出したくなりました。

ニュース画面への遷移はNavigationLinkを使いました

ContentView.swiftのニュースのパーツをNavigationLinkで囲みます。これは画面遷移などに使われるパーツです。
NewsViewは遷移先のニュース画面です。

ContentView.swift
HStack{
    NavigationLink(destination: NewsView()) {
        Image("news")
            .resizable()
            .frame(width: 90, height: 90)
        Text("ニュース")
            .fontWeight(.bold).font(.title)
            .foregroundColor(Color.white)
    }
}

ニュース画面の実装

画面表示時にニュースを取得するようにonAppearを使用しました。

NewsView.swift
import SwiftUI

struct NewsView: View {

    var newsMap = NewsApiMap()
    @State var errorMsg = ""
    @State var newsList:[(id: Int, title:String, url: String, urlToImage: String )] = []

    // ニュースを取得して編集
    func getData(data : News) {
        newsList = []
        errorMsg = ""
        if(data.articles.count != 0){
            newsList = []

            for i:Int in (0 ... (data.articles.count - 1)){
                self.newsList.append((i,data.articles[i].title, data.articles[i].url, data.articles[i].urlToImage))
            }
        } else {
            errorMsg = "ベトナムの関連のニュースがありませんでした。\nごめんなさい。"
        }
    }

    var body: some View{
        VStack  {
            VStack{
                Text("ニュース").font(.largeTitle).fontWeight(.bold).foregroundColor(.white)
            }
            .padding(.all, -5)
            Spacer()
            VStack{
                if (self.errorMsg == ""){
                    List {
                        ForEach(newsList.indices,id: \.self) { index in
                            HStack{
                                Link("\(self.newsList[index].title)", destination: URL(string: "\(self.newsList[index].url)")!)
                            }
                        }
                    }
                } else {
                    VStack{
                        Text("\(self.errorMsg)").font(.title).fontWeight(.bold).foregroundColor(.black)
                    }.frame(width: 300, height: 400, alignment: .center)
                }
            }.padding()
            .background(Color.white.opacity(0.7))
            Spacer()
        }.onAppear{newsMap.getNews(action: self.getData)}
       .background(Image("bg"))
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
    }
}

struct NewsView_Previews: PreviewProvider {
    static var previews: some View {
        NewsView()
    }
}

 ニュースAPIから取得する処理を実装する

基本的には天気のAPIと同じです。
ニュースはNewsAPIのものを使いました。こちらも個人利用であれば無料で利用可能です。(アクセス数の制限はあります)
今回は「ベトナム」のワードを含むニュースをとってきます。

ここでは天気のAPIの実装の差分を紹介してから全文の紹介をします。

まずは天気APIとの実装の差分

①日本語を含むキーワードをURLに指定したい場合はaddingPercentEncodingでエンコーディングしたあげる必要がある

まとめたのでよかったらどうぞ。
【Swift】日本語を含むURLを使いたい時はaddingPercentEncodingせよ

② quicktypeの出力結果をそのまんまは使えなかった。

よーくよーく見ればわかる部分でした。ヒントは型。
答えはこちらに書いているのでよかったらどうぞ。
【Swift】NewsAPIで取得したjsonのデコードで失敗して半日潰した話
当時はデバッグしても「デコードのあたりで落ちているや:ghost:」としかわかりませんでした。
 
 
ソースの全量はこちらです。

NewsView.swift
//  NewsAPIから日本語のベトナムニュースをとってくる

/** json -> swiftに直す処理をする
 https://app.quicktype.io/ で自動生成したものから使うものだけ残した
 */

import SwiftUI

// MARK: - News
struct News: Codable {
    let status: String
    let totalResults: Int
    let articles: [Article]
}

// MARK: - Article
struct Article: Codable {
    let title: String
    let url: String
    let urlToImage: String
}
/**
 NewAPI から"ベトナム"のワードを持つニュースを取得する
 */
//結果を返す関数の定義
typealias getNewsFunc = ((_ item:News) -> Void)?

//NewsApiからデータを取得するクラス
class NewsApiMap : ObservableObject {
    @Published var articles: [Article] = []

    let keyword = "ベトナム"
    let apikey = "自身のapikey"
    var _action: ((_ item:News) -> Void)?

    // 結果受け取り
    func SetAction(action :((_ item:News) -> Void)?){
        self._action = action
    }

    // ニュースの取得
    func getNews(action:getNewsFunc) -> Void {

        self.SetAction(action: action)
        // APIのコール
        let NewsApiURL="https://newsapi.org/v2/everything?q=\(keyword)&num=1&sortBy=latest&apiKey=\(apikey)"
        let encodedURL = NewsApiURL.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)

        guard let url = NSURL(string: encodedURL!) else {
            print("無効なURL")
            return
        }

        let request = URLRequest(url: url as URL)

        URLSession.shared.dataTask(with: request){
            data, request, error in
            // 取得データをデコード
            if let data = data {
                if let decodedResponse = try? JSONDecoder().decode(News.self, from: data){
                    DispatchQueue.main.async {
                        self._action!(decodedResponse)
                    }
                }
            }
        }
        // タスクの実行
        .resume()
    }
}

以上、動いて楽しかったのでここから体系的に勉強していこうと思います。

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

【Xcode】画像をフォルダ管理したい

Xcodeさんは画像のファイル名が被っていなければ、よしなに対応してくれる

例えばAssets.xcassetsに以下のように画像を格納します
image.png

この場合、以下のようにソースを書けばよしなにとってくれます

Image("news")  // ./Assets.xcassets/news を表示
Image("01d")  // ./Assets.xcassets/WeatherIcons/01d を表示

よしなに読み込ませない

なんか嫌なので、ここのProvide namespaceにチェックをいれる
image.png

すると以下の書き方で画像をよびだせるようになる。

Image("news")  // ./Assets.xcassets/news を表示
Image("WeatherIcons/01d")  // ./Assets.xcassets/WeatherIcons/01d を表示

「嫌」以外の理由をつけるならば同じファイル名の画像を使いたい時などでしょうか。

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

Collection Groupでサブコレクションをまとめてクエリ[Firestore]

Collection Groupを使うと、異なるドキュメント配下の同名のサブコレクションをまとめてクエリできます。

たとえば、

preference collection:県
 /document(京都、大阪、名古屋、岡山)
  /specialty collection:名物
   /document([name:八ツ橋, type:sweet] [name: たこ焼き, type: meal] [name:ひつまぶし, type: meal] [name:きびだんご, type: sweet])

という階層があったときに、

specialty collectionは別々のpreferenceドキュメントに属していますが、specialtyのくくりでまとめてクエリできます。

手順

1,セキュリティルールを追加

グループコレクションでクエリを行うには、途中までのパスがワイルドカードになったruleをFirestoreに追加します。

match /{path=**}/specialty/{document} {
  allow read: if true;
}

2,グループコレクションでクエリしてみる

Swiftの例は以下。他の言語は公式ドキュメント参照。

let db = Firestore.firestore()

db.collectionGroup("specialty").whereField("type", isEqualTo: "sweet").getDocuments { (snapshot, error) in
    // ...
}

このクエリは失敗します。
なぜなら、Collection Group queryをサポートする Index をFirestore に追加する必要があるからです。

3,Index を追加

上記クエリを実行すると、コンソールログにクエリに必要なインデックスを追加するためのリンクが表示されます。
ブラウザからそのリンクを開くと、必要なインデックスが自動で追加されます。

Firestoreコンソールから手動で追加することもできます。

3までの手順を実行して、改めて2,のクエリを実行すると、specialtyコレクションのスイーツタイプをもつドキュメントのスナップショットが得られます。

?


フリーランスエンジニアです。
お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium

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