- 投稿日:2021-01-19T23:44:20+09:00
[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です以上で、クロージャの説明を終わります。
- 投稿日:2021-01-19T21:47:20+09:00
Swift 条件分岐文まとめ
はじめに
この記事には、Swiftで多用する条件分岐文についてまとめました。
参考文献
この記事は以下の情報を参考にして執筆しました。
目次
- if文 - 条件の成否による分岐
- guard文 - 条件不成立時に早期退出する分岐
- 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キーワードのみで構成される。
- 投稿日:2021-01-19T19:08:03+09:00
クラスと構造体の違いってなんだろう???
はじめに
今回は、クラスと構造体の違いを備忘録としてまとめようと思います。
クラスと構造体
クラスと構造体は
値の受け渡し方法
によって、値型
と参照型
の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のクラスと構造体の使い分けについてのメモおわり
間違っている部分があれば、遠慮なくコメントして下さると有り難いです。
- 投稿日:2021-01-19T17:38:21+09:00
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つメソッドがあるので、必要に応じて利用しましょう。
・高さを可変式にしたい時は、可変式用の設定をしましょう。
- 投稿日:2021-01-19T16:51:29+09:00
Swift UITextFieldをコードで実装
UITextFieldをコードで実装してみる
swift
というかアプリ開発の入門書をみるとstoryboard
にviewをぺたぺたドラッグ&ドロップするやり方ばっかですよね。
それでもいいんですがワンステップ上に上がりたい方のためにコード実装の仕方を記事にします。
まずは変数を定義しましょう。let textField = UITextField()この状態でビルドしても何も表示されません。
まあ、変数を宣言しただけなんで当たり前ですよね。
次に、ViewController
のview
に入れましょう。view.addSubview(textField)これで追加できました。ビルドしてみましょう。
あれ?何も表示されませんね。
実はこれあるにはあるんですが幅も高さも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// 角を丸くする textField.layer.cornerRadius = 5 // border線と文字が近すぎるから少し離す textField.leftView = UIView(frame: .init(x: 0, y: 0, width: 5, height: 0)) textField.leftViewMode = .alwaysこれでビルド!
いい感じに角も丸くなってボーダー線と文字の間にも隙間ができていますね。textFieldのあれこれ
//勝手に英語のスペルを正しいのにする機能をoff textField.autocorrectionType = .no //勝手に英語の先頭の文字を大文字にするのをoff textField.autocapitalizationType = .none // fontサイズを変更 textField.font = .systemFont(ofSize: 18) // textFieldを編集モードにする textField.becomeFirstResponder() // textFieldの編集モードを解除する textField.resignFirstResponder()やりたいことに応じてつかってみてください。
今回はここまでにします。
読んでくださりありがとうございます。
- 投稿日:2021-01-19T16:17:59+09:00
【Swift】PhotoKitで画像選択UIを作る
LINEやTwitter、Pinterestなど、写真を扱う多くのアプリにはカスタマイズされた画像選択UIが実装されています。
今回はこのようなUIをPhotoKit
を使って実装してみます。デフォルトの
PickerController
はカスタマイズ性が低い"Swift 画像 選択" などで検索すると、
UIImagePickerController
による実装がいくつかヒットします。
何も考えずにパパッと実装したい場合には便利なのですが、UIや表示内容のカスタマイズ性が制限されるため、デザインの選択肢が狭まってしまいます。PhotoKitでフォトライブラリにアクセスする
そこで、
PhotoKit
を用いて直接フォトライブラリからデータを取得してUIに設定するという方法を考えます。ユーザにアクセスを許可してもらう
まずはUIImagePickerViewControllerと同様、
Info.plist
にNSPhotoLibraryUsageDescription
を追加します。
(ここで記述した文字列がアクセス要求の際に表示されます)
次に、画像を読み込みたいタイミングで
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
には以下のように様々なオプションを設定できます。(引用: 公式ドキュメント)が、今回は単純に撮影日時の降順に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. ...
したがって、
map
やfilter
で処理するといったことはできません。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頂ければ幸いです。
- 投稿日:2021-01-19T07:28:33+09:00
[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 で隠しファイルの表示/非表示を切り替える方法を参考にしてください。
- 投稿日:2021-01-19T01:54:17+09:00
【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に格納します。
ネットでフリー配布されているものの色をいじって使いました。
OpenWeatherMap APIから天気情報を取得する
OpenWeatherMapへの登録
天気情報の取得はOpenWeatherMapの
Weather 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.swiftHStack{ NavigationLink(destination: NewsView()) { Image("news") .resizable() .frame(width: 90, height: 90) Text("ニュース") .fontWeight(.bold).font(.title) .foregroundColor(Color.white) } }ニュース画面の実装
画面表示時にニュースを取得するように
onAppear
を使用しました。NewsView.swiftimport 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のデコードで失敗して半日潰した話
当時はデバッグしても「デコードのあたりで落ちているや」としかわかりませんでした。
ソースの全量はこちらです。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() } }以上、動いて楽しかったのでここから体系的に勉強していこうと思います。
- 投稿日:2021-01-19T01:48:15+09:00
【Xcode】画像をフォルダ管理したい
Xcodeさんは画像のファイル名が被っていなければ、よしなに対応してくれる
例えば
Assets.xcassets
に以下のように画像を格納します
この場合、以下のようにソースを書けばよしなにとってくれます
Image("news") // ./Assets.xcassets/news を表示 Image("01d") // ./Assets.xcassets/WeatherIcons/01d を表示よしなに読み込ませない
なんか嫌なので、ここのProvide namespaceにチェックをいれる
すると以下の書き方で画像をよびだせるようになる。
Image("news") // ./Assets.xcassets/news を表示 Image("WeatherIcons/01d") // ./Assets.xcassets/WeatherIcons/01d を表示「嫌」以外の理由をつけるならば同じファイル名の画像を使いたい時などでしょうか。
- 投稿日:2021-01-19T01:01:44+09:00
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.comCore MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。