- 投稿日:2021-02-28T19:51:35+09:00
RxSwiftでTodoAPP入門[part1]
概要
最近RxSwiftを勉強し始めて現在理解していることを備忘録として残せたらいいなと思い記事にします。
そもそもRxSwiftのRxとはRx(Reactive X)とは、「オブザーバパターン」「イテレータパターン」「関数型プログラミング」の概念を実装している拡張ライブラリです。
Rxを導入するメリットは、「値の変化を検知できる」「非同期の処理を簡潔に書ける」ということに尽きると思います。 値の変化というのは変数値の変化やUIの変化も含まれます。 例えばボタンをタッチする、という動作もボタンのステータスが変わったと捉えることができRxを使って記述することができます。とのことです。
詳しくは以下のサイトを参照してください。
入門!RxSwift
RxSwiftについてようやく理解できてきたのでまとめることにした(1)今回使用するライブラリ
RxSwift
pod 'RxSwift'
--- ObservableなどのRx系の本領的なものを使うのに必要
RxCocoapod 'RxCocoa'
--- UIKitでRxを使うのに必要
RxDataSourcespod 'RxDataSources'
--- tableViewなどのdatasourceの扱いを楽にしてくれる
Firestorepod 'Firebase/Firestore'
--- todoの保存をするのに必要以上のライブラリを使用していきます。
認証機能は?と思われたかもしれないですが今回は省かせていただきます。
また、今回の記事はfirebaseやpodは導入済みのものとして話を進めさせていただきます。
StoreManager
まずはこのアプリのもっとも重要なTodoの追加や取得の処理を書いていきます。
StoreManager
という名前のファイルを作ってください。StoreManager.swiftimport Foundation import FirebaseFirestore import RxSwift class StoreManager { static let shared = StoreManager() private let store = Firestore.firestore() }こんな感じで定義してください。
ここは説明するまでもないと思うので説明を省略します。次に
Todo
を保存する関数を作ります。
extension
で拡張すると可読性が上がるので拡張します。StoreManager.swiftextension StoreManager { // firestore にtodoのデータを保存 func insertTodoToFireStore(title: String, detail: String) { let data: [String: Any] = [ "title": title, "detail": detail, "createdAt": Timestamp() ] store.collection("todos").addDocument(data: data) } }ここも特に説明はいらないと思いますは簡単に説明しておきます。
title
,detail
を受け取り[String: Any]
型のDictionary
にして各値を格納します。そして、todos
というコレクション名のコレクションにデータを保存します。次にデータを取得する関数を作ります。
StoreManager.swift// firestore からtodosのデータ取得 func fetchTodosFromFirestore() -> Single<[TodoModel]> { Single.create { [weak self](single) -> Disposable in self?.store.collection("todos").getDocuments { (snapshots, error) in guard let docs = snapshots?.documents, error == nil else { single(.failure(CustomError.error(message: "Failed To Fetch Todos From Firestore"))) return } let todos = docs.map { TodoModel(data: $0.data()) } single(.success(todos)) } return Disposables.create() } }ここがこのクラスのキモですね。
戻り値にSingle<[TodoModel]>
とありますね。
Single
はRxSwiftをインポートしていないと使えないので忘れないでください。
また、現時点ではTodoModel
というものが定義されていないのでエラーになると思いますが気になる人はとりあえずTodoModelを定義しておいてください。
Single
について説明していきます。
Single
はSuccessとErrorを流し、Completedを流さないものになります。
successとはnextを一回だけ流れるものでerrorはそのままですね。
ほーという感じだと思いますがcomplete処理がいらなかったらSingleを使うという感じでいいんじゃないでしょうか。Singleの他にもMaybe、Completableがあります。
詳しいことは以下の記事を参照してください。
RxSwift 3.3.0で追加された3つのUnit(Single, Maybe, Completable)次に
guard let文
の中でエラー処理を書いています。
ここは説明なしでもわかると思います。
そして、取得した値をTodoModel
に加工してsuccess
で流してあげます。最後に
Disposables.create()
ですが、これはストリームのライフサイクルを管理するためのものです。
disposeされたときに、ここに書いた処理が実行されます。
Disposable
についてTodoModel
先ほど飛ばした
TodoModel
はこんな感じで書きます。TodoModel.swiftimport Foundation import FirebaseFirestore struct TodoModel { let title: String let detail: String let createdAt: Timestamp init(data: [String: Any]) { title = data["title"] as? String ?? "" detail = data["detail"] as? String ?? "" createdAt = data["createdAt"] as? Timestamp ?? Timestamp() } }最後に
part1はここまでになります。
正直まだ理解しきれていないこともあり、調べながらになりますが理解できているところとあまり理解できていないとこがわかってoutputって大切だなと思いました。
また、あまり理解できずに書いてしまっているところや誤解釈してしまっているところがありましたらご指摘の方お願いいたします。[part2]はViewModelの作成をしていきます。
- 投稿日:2021-02-28T18:30:04+09:00
【SwiftUI】contextMenuで長押ししたときにメニューを出す
先日リリースしたアプリに使用した技術をひとつずつ解説しています。
私のアプリはこちら。
contextMenuとは
任意のビューに対し、長押ししたときにメニューを出す機能を追加するメソッド。
実際の動作
実際にアプリを触らないとわかりづらいかもしれませんが、長押しすることでメニューが表示されています。また、メニューの外側をタップするとメニューが閉じるようになっています。
iOSアプリでよく見かける機能ではないでしょうか。ソースコード
ContentView.Swiftimport SwiftUI struct ContentView: View { let evs = ["イーブイ", "ブースター", "シャワーズ", "サンダース", "エーフィ", "ブラッキー", "グレイシア", "リーフィア", "ニンフィア"] let colors1: [Color] = [Color(#colorLiteral(red: 0.7254902124, green: 0.4784313738, blue: 0.09803921729, alpha: 1)), .red, .blue, .yellow, Color(#colorLiteral(red: 0.8446564078, green: 0.5145705342, blue: 1, alpha: 0.5)), .black, Color(#colorLiteral(red: 0.6872052062, green: 0.9808971643, blue: 1, alpha: 1)), Color(#colorLiteral(red: 0.5843137503, green: 0.8235294223, blue: 0.4196078479, alpha: 1)), .pink] let colors2: [Color] = [.white, .white, .white, .black, .black, .yellow, .blue, Color(#colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1)), Color(#colorLiteral(red: 0.4745098054, green: 0.8392156959, blue: 0.9764705896, alpha: 1))] @State var evID = 0 var body: some View { ZStack{ Circle() .foregroundColor(colors1[evID]) .frame(width: 250) VStack{ Image(systemName: "sparkles") //このビューを長押しするとメニューが現れるようにする //ここからがコンテキストメニュー .contextMenu(menuItems: { ForEach(0..<9) { n in Button(action: { evID = n }, label: { Text(evs[n]) }) } }) //ここまでがコンテキストメニュー Text(evs[evID]) .padding() } .font(.title) .foregroundColor(colors2[evID]) } } }「長押しするとメニューが出る」という機能を追加したいビューに対して
.contextMenu
を書きます。
そして、contextMenuの引数であるmenuItems
の{}内が、表示されるメニューの内容になります。menuItemsには何を書く?
contextMenuで
Text
やSF Symbolsを使ったImage
を表示させることができます。しかし、書いたものを自動的にボタンにする、という機能はありません。上のサンプルアプリのようにcontextMenuにボタンを表示させたい場合は、menuItemsにはButtonビューを書くのが良いでしょう。まとめ
簡単なコードでおもしろくて便利な機能を実現できます。ぜひ、SwiftUIでcontextMenuを使ってみてください。
よかったらTwitterのフォローをお願いします。
https://twitter.com/masayoshi_tozan
- 投稿日:2021-02-28T17:17:46+09:00
[macOS][Swift5.3]マウスカーソルの画像をカスタマイズする方法
macアプリでプリセット以外のカーソルを表示する方法を見つけるのに時間がかかったため、誰かの役に立てばと思い書き記します。
1. 環境
macOS: 11.2.1 Big Sur
Swift: 5.3.2
Xcode: 12.42. 全コード
TestView.swiftimport Cocoa class TestView: NSView { private var trackingArea: NSTrackingArea? override func updateTrackingAreas() { super.updateTrackingAreas() if self.trackingArea != nil { self.removeTrackingArea(self.trackingArea!) self.trackingArea = nil } let options: NSTrackingArea.Options = [.mouseMoved, .mouseEnteredAndExited, .activeAlways] let rect = NSRect(origin: .zero, size: self.frame.size) self.trackingArea = NSTrackingArea(rect: rect, options: options, owner: self, userInfo: nil) self.addTrackingArea(self.trackingArea!) } override func mouseEntered(with event: NSEvent) { let image = NSImage(named: "image_name") let cursor = NSCursor(image: image!, hotSpot: .zero) addCursorRect(self.bounds, cursor: cursor) } override func mouseExited(with event: NSEvent) { NSCursor.arrow.set() } }3. 解説
対象のNSView自身と同じ大きさのトラッキング範囲を作成し、カーソルが出入りするとイベントを発火するようにします。
private var trackingArea: NSTrackingArea? override func updateTrackingAreas() { super.updateTrackingAreas() if self.trackingArea != nil { self.removeTrackingArea(self.trackingArea!) self.trackingArea = nil } let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways] let rect = NSRect(origin: .zero, size: self.frame.size) self.trackingArea = NSTrackingArea(rect: rect, options: options, owner: self, userInfo: nil) self.addTrackingArea(self.trackingArea!) }カーソルがトラッキング範囲に入ったタイミングでカスタムカーソルを表示します。プリセットのカーソルでは
set()
メソッドを使いますが、カスタムの場合addCursorRect()
を使う必要があります。override func mouseEntered(with event: NSEvent) { let image = NSImage(named: "image_name") let cursor = NSCursor(image: image!, hotSpot: .zero) addCursorRect(self.bounds, cursor: cursor) }上のコードの"hotSpot"は、カーソルの中でクリックを検知する座標を表します。矢印型のカーソルならばhotSpotを座標の左上に設定することで、見た目と動作を一致させることができます。
カーソルがトラッキング範囲を出たらデフォルトのカーソルに戻します。
override func mouseExited(with event: NSEvent) { NSCursor.arrow.set() }4. 参考資料
- 投稿日:2021-02-28T17:07:04+09:00
swift optional型 まとめ
はじめに
swiftのoptional型の基礎について学んだので,簡単にメモしました.
1. Force Unwrapping (optional!)
強制的にunwrapしてoptional型からvalueを取り出す.nilが絶対入らない場合に利用.
let myOptional: String? myOptional = "hello" let text: String = myOptional!ただし,仮にnilが入ってしまってエラーになった時,複数のコードやファイルが存在した場合,その原因を見つけるのが大変になり,安全でない. → 2. Check for nil Valueを利用.
2. Check for Nil Value (if optional != nil {optional!})
- Force Unwrappingより安全なアンラッピング.if文でnilか否か条件分岐.
let myOptional: String? myOptional = nil if myOptional != nil { let text: String = myOptional! //この部分が冗長 } else { print("myOptional was found to be nil.") }ただし,nil出ない時にも!を付けないといけないのは少し冗長. → 3. Optional Bindingを利用.
3. Optional Binding(if let safeOptional = optional {safeOptional})
以下のコードを例に,myOptionalがnilでなかったら,定数safeOptionalに代入.
これにより,if文内で!をつける必要がなくなる.let myOptional: String? myOptional = nil //myOptional = "hello" if let safeOptional = myOptional { let text: String = safeOptional print(text) } else { print("myOptional was found to be nil.") }ただし,myOptionalがnilの時にdefault値を与えたい場合 → 4. Nil Coalescing Operatorを利用.
4. Nil Coalescing Operator(optional ?? defaultValue)
optionalがnilでない場合,その値をそのまま用いる.
optionalがnilの場合,defaultValueを用いる.let myOptional: String? myOptional = "good bye" // output: "good bye" //myOptional = nil // output: "hello" let text: String = myOptional ?? "hello" print(text)5. Optional Chaining(optional?.property, optional?.method())
optional型のclass or structのオブジェクトを生成した際に,Optional Chainingを利用.
optional?.property, optional?.method()で,オブジェクトがnilでない場合,property, methodにアクセスできる.Optional Chainingなしとありの場合で比較.
Optional Chainingなしの場合(安全で無い)
オブジェクトを生成していない場合で,プロパティを取り出すために,オブジェクトをforce unwrappingするとエラー → 安全で無い
struct MyOptional { var property = 123 func method() { print("I am the struct's method.") } } let myOptional: MyOptional? //optional型のstructのオブジェクトを定義 myOptional = nil // output: error //myOptional = MyOptional() // output: 123 print(myOptional!.property)//optional型のオブジェクトからプロパティを取り出すためには,オブジェクトをunwrapする必要ありOptional Chainingありの場合(安全)
optional?.property, optional?.method()で,オブジェクトがnilでない場合,property, methodにアクセスできる.
struct MyOptional { var property = 123 func method() { print("I am the struct's method.") } } let myOptional: MyOptional? //optional型のstructのオブジェクトを定義 myOptional = nil // output: nil //myOptional = MyOptional() // output: Optional(123) print(myOptional?.property)// methodも同様のやり方まとめ
optional型の理解がかなり深まりました.何か間違っている点があれば,ご指摘いただけると幸いです.
- 投稿日:2021-02-28T14:12:21+09:00
アルバイト先のiOSアプリをリファクタリングした
アルバイト先の会社で、プロジェクトの整備、リファクタリングをしようということになりました。
アプリのプロジェクトはこれまでオフショアで海外に渡ったり、業務委託やアルバイトに渡ったりと、カオス状態でした。
今回は、まとまった時間を設けてこのレガシープロジェクトを少しでも改善しようということだったので、最近学んだ内容をアウトプットするいい機会だと思って色々やってみました。
「これまでそんな事もやってなかったのか」と思われるかもしれませんが、
誤りや改善点等ありましたら、ご指摘いただければ幸いです。GitLabからGitHubへの移行
これまでGitLabで管理されていたプロジェクトをGitHubに移行することになりました。
私はREADMEやWikiの整備を行いました。
具体的には以下を含めました。
README
- 開発環境
- ライブラリ管理
- セットアップ方法
- API仕様書
Wiki
- 簡単なディレクトリ構成
- 主な使用ライブラリ
- Gitやコードの命名規則
また、ブランチの運用も変更しました。
- Main(リリース用)
- Staging(テスト用)
- Mainからブランチを切って、作業用ブランチはStagingにマージ
- Stagingでテスト完了後にリリースバージョンのタグをつけてマージ
- Mainをリリース
rbenv で ruby のバージョンを指定
CocoaPods は ruby で作られたツールです。
チーム開発を行う場合、各々の PC の ruby のバージョンが異なるとライブラリのインストールに失敗したり、上手く動かないことがあるので rbenv を使って ruby のバージョンを統一しました。
bundler など他にもツールが出てきて一気に理解するのは大変でしたが、以下の記事では丁寧に解説されています。
Cocoapods と Carthage を共存させた iOS プロジェクトの始め方
SwiftLint の導入
SwiftLintを導入しました。
ルールに関しては、業務委託の方が普段使用されている.swiftlint.yml ファイルが導入されました。
それに少しルールを追加しただけで、かなり緩めにルールが設定されています。
下記の記事を参考にさせていただきました。
Swiftの静的解析ツール「SwiftLint」のセットアップ方法
IQKeyboardManager の導入
これに関しては、本当に入れるだけで障壁はほぼありませんでした。
リソース管理、R.swiftの導入
他言語対応されているはずなのに、NSLocalizedString が使用されずにハードコーディングされている箇所があったりと、こちらもカオス状態でした。
この機会なのでリソース管理のライブラリを入れようということで、導入がSwiftGenより簡単なR.swiftを導入しました。
注意点としては、生成された R.generated ファイルが、SwiftLint の制約エラーを起こすことがあるので、R.generatedファイルは .swiftlint.yml ファイルの excluded: に含める必要があります。
また、“キーワードが被っているという問題で、生成をスキップします“というような警告が出たので、リソース側の整理が必要でした。
現在は文字列しかほぼ書き換えられてないので、今後は画像、ストーリーボード、セルなども書き換える必要があります。
下記の記事を参考にさせていただきました。
リテラルが結構使用されていたのでこちらも。
R.Swiftでは文字リソースで「%@」を使うと、その部分を引数にとる関数を自動生成する
継承が想定されないクラスには final をつける
Swift ではクラスを継承することができますが、自作のクラスで他のクラスから継承しないものには final をつけるべきです。
自分が継承されない想定で作ったクラスを他の人が継承する実装にしてしまったというようなことがチーム開発ではあり得るので、それを防ぐためには final を適切に設定することが必要です。
アクセスレベルを適切に設定
変数やメソッドなどには private などのアクセスレベルを設定することができます。 例えばクラス内でのみ使用し、クラスの外から呼ばれることのないメソッドは private にすることができます。
final と同様に、アクセスレベルも適切に設定することが必要です。
他人がプロジェクトのコードを読むときに「このメソッドは private だから外から呼ばれることはないんだ」と判断できるので、迅速な理解に繋がるというメリットもあると思います。
下記の記事で、非常にわかりやすく説明されています。
delegate メソッドを extension に分ける
ViewController の定義時に全ての Delegate を継承せず適切に extension で分けました。
主に UITableViewDelegate や UICollectionViewDelegate
、 UITextFieldDelegate のメソッドがそのままクラス内に書かれており、定義されているメソッドが delegate メソッドなのか独自のメソッドなのか分かりづらい状態でした。こちらは自分で簡単にまとめたものを参考にしました。
不要なコメントアウト、 print デバッグの削除
使用されていないコードのコメントアウトや、 print 文がそのまま残されている状態でした。
print 文の中で可能なものは、下記の記事を参考に os.Logger で置き換えられました。
忙しい人向けの Explore logging in Swift - #WWDC20
例:
let logger = Logger() // 省略 do { // 処理 } catch { logger.error(error) }今後やりたいこと
可能なところは StackView を使用
UI のメンテナンスの際は、強力な StackView である方がメンテナンスがやりやすいので、
可能なところは StackView を使用したいです。可能なところStoryboard Reference を使用
Storyboard が入り組んでいると、メンテナンスがやりづらいと思います。
特にチーム開発ではコンフリクトの原因にもなるので、可能なところは StoryboardReference で Storyboard を分けるべきだと思います。
SwiftFormat の導入
機械的にコーディングルールを適用させれば楽になるかと思います。
ネストの解消
可読性の低いネストが散見されるので、可能な限り guard 文などで可読性を改善したいです。
まとめ
本来であれば、重複した処理を共通化したり、クラスの抽象化をしたりと、より高度なことをやりたいですが、現状の私のレベルで出来ることはこのくらいでした。
やるべき事は山積みですが、引き続きプロジェクトに貢献していきたいです。
- 投稿日:2021-02-28T11:42:19+09:00
CocoaPodsからSwiftyUserDefaultsを導入してみた
はじめに
SwiftyUserDefaultsの導入について備忘録としてまとめます。
導入の動機としては、SwiftのUserDefaultの使い勝手の悪さから、代替手段を模索していたところ
最終的にこのライブラリに辿り着いきました。目次
- プロジェクトへpodインストール
- 実装方法
- インポート
- 定義と初期化
- 値の保存方法
- 値の読み出し方法
- まとめ
プロジェクトへpodインストール
まずは、CocoaPodsをお使いのMacにインストールしておきます。
CLI//コマンドラインで実行 pod initPodfile//Podfileへ下記をコードを追加 pod 'SwiftyUserDefaults', '~> 5.0'CLI//コマンドラインで実行 pod installインポート
SwiftのUserDefaultを使うファイルに対し下記コードを挿入します。
.swiftimport SwiftyUserDefaults定義と初期化
まずは、プロジェクトへ定義する為のファイルを追加し、そこへ永続化したいキーを定義します。
SwiftyUserDefaultsでは定義と同時に初期値も指定できるので便利です。DefaultsKeys.swiftimport SwiftyUserDefaults extension DefaultsKeys { //初期値指定なし var test_str_nv: DefaultsKey<String?> { .init("Test_Str_NV") } var test_int_nv: DefaultsKey<Int?> { .init("Test_Int_NV") } //初期値指定あり var test_str_v: DefaultsKey<String> { .init("Test_Str_V", defaultValue: "初期値をここへ指定") } var test_int_v: DefaultsKey<Int> { .init("Test_Int_V", defaultValue: 777) } }値の保存方法
read.swiftDefaults[\.test_str_nv] = "文字列を代入" Defaults[\.test_int_nv] = 777値の読み出し方法
write.swiftlet read_str = keys[Defaults[\.test_str_nv]] let read_int = keys[Defaults[\.test_int_nv]]まとめ
SwiftyUserDefaultsを導入することにより、
* Swift純正のUserDefaultよりも実装するコード量が減らすことができる
* 定義及び初期化する部分を一つのファイル内に集約できて分散防止が可能
* 値の保存及び読み出しが従来の様に文字列指定ではなく、変数指定になることでタイピングミスによる予期せぬ不具合を未然に防げる。
* メンテナンスが楽になる
となります。参考
- 投稿日:2021-02-28T11:30:26+09:00
Admobで広告を表示するときのTips
中々調べても出てこなかったので記事にします。
未リリースのアプリにAdmobを実装していて、テスト広告は表示できるのに本番の広告が表示されないという減少が起こっていました。発生したエラー
その時のエラーは
No ad to show
というものが出ていました。注意
本番の広告のみ表示できないなら、まずAdUnitIDが合っているかを確かめましょう。
次に、Admobの広告は、アプリをリリースするまで表示されません。
Admobの設定画面には、リリース済みのアプリを紐付けるためappIDを入力する箇所がありますが、リリースしてから4,5日経過しないと表示されません。
私の場合は、リリース後2日経ってから、appIDの紐付けをしなくても自動的に広告が配信され始めました。
- 投稿日:2021-02-28T11:20:11+09:00
APIKitをCodableで使用する
https://github.com/ishkawa/APIKit ではJSONDataParserが提供されていますが、実装を見るとこのParserで返されるのはJSONをSwiftの世界の型に変換したものです。 (Dictionary, Array, String, etc...)
public func parse(data: Data) throws -> Any { guard data.count > 0 else { return [:] } return try JSONSerialization.jsonObject(with: data, options: readingOptions) }※JSONDataParserより抜粋
Codableに準拠した独自の型に変換するためには
JSONDecoder().decode(Decodable.Protocol, from: Data)
を使用しますが、そのためにはData型を渡す必要があります。
なので、JSONDataParserは使用できず、そのままのDataを返す空のDataParserが必要です。struct NopeDataParser: DataParser { var contentType: String? func parse(data: Data) throws -> Any { data } }これを用いると、以下のメソッドで取得できるobjectの型はDataになり、Codableに準拠した型に変換することができます。
func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response { guard let data = object as? Data else { throw DomainError.parseFailed(object) } return try JSONDecoder().decode(Response.self, from: data) }
- 投稿日:2021-02-28T10:53:13+09:00
すべてのひらがな・カタカナなどのリストをCharacterの配列で取得する
ひらがな一覧
static let hiraganaList: [Character] = { [UnicodeScalar("あ").value...UnicodeScalar("ん").value].joined() .compactMap { value in UnicodeScalar(value).map(Character.init) } }()カタカナ一覧
static let katakanaList: [Character] = { [UnicodeScalar("ア").value...UnicodeScalar("ン").value].joined() .compactMap { value in UnicodeScalar(value).map(Character.init) } }()アルファベット大文字一覧
static let upperAlphabetList: [Character] = { [UnicodeScalar("A").value...UnicodeScalar("Z").value].joined() .compactMap { value in UnicodeScalar(value).map(Character.init) } }()アルファベット小文字一覧
static let lowerAlphabetList: [Character] = { [UnicodeScalar("a").value...UnicodeScalar("z").value].joined() .compactMap { value in UnicodeScalar(value).map(Character.init) } }()数字一覧
static let numberList: [Character] = { [UnicodeScalar("0").value...UnicodeScalar("9").value].joined() .compactMap { value in UnicodeScalar(value).map(Character.init) } }()
- 投稿日:2021-02-28T10:49:49+09:00
【Swift】オプショナル型をアンラップする4つの方法
はじめに
オプショナル型を使用する度に、
xcodeさんに"!"や"?"についてエラーで怒られ、
毎回導かれるがままに「Fix」ボタンで解決していました。しかし、これではいけないと思い、
今回はあまり理解していなかった、オプショナル型のアンラップ方法について記します。開発環境
Swift (Version 5.3.2) xcode (Version 12.4) MacBook Air (13-inch, 2019) macOS Big Sur (Version 11.2.1)オプショナル型をざっくりと
オプショナル型とは、変数にnilの代入を許すとことです。
逆に非オプショナル型はnilの代入を許しません。
nilとはデータが無い、変数が空の状態を表します。オプショナル型と非オプショナル型// 非オプショナル型 var hoge: String print(hoge) // エラーが出て実行できない // オプショナル型 var hoge: String? print(hoge) // niliOSではnilに対して操作することでアプリケーションが落ちてしまうことがありました。
これを鑑みて、Swiftではnilを基本的には許容しません。
しかし、オプショナル型を使うことでnilを扱うことができるようになります。ところが、オプショナル型の変数を通常の変数と同様に扱おうとすると、おもわぬエラーが起きます。
通常の変数とオプショナル型変数との違い// 通常の変数 var hoge: Int = 1 var fuga: Int = 1 print(hoge + fuga) // 2 /*-------------------------------*/ // オプショナル型 var hoge: Int? = 1 // Optional(1) var fuga: Int? = 1 // Optional(1) print(hoge + fuga) // エラーが出て計算できないオプショナル型にすると、値1はラップされ「Optional(1)」のようになります。
そのため、オプショナル型の値を通常の値に変換するアンラップという作業が必要になります。
端的に換言すると「Optional(1)」を、ただの「1」にします。4つのアンラップ方法
オプショナル型をアンラップするには、4つの方法があります。
1, 強制アンラップ
2, オプショナルバインディング
3, オプショナルチェイニング
4, ??演算子1, 強制アンラップ
"!"を用いてアンラップします。
強制アンラップ// アンラップ前 var hoge: Int = 1 var fuga: Int = 1 print(hoge + fuga) // エラーが出て計算できない /*-------------------------------*/ // アンラップ後 var hoge: Int? = 1 var fuga: Int? = 1 print(hoge! + fuga!) // 2このように、"!"を使用することで、
「Optional(1)」を強制的に、ただの「1」に変換します。■暗黙的アンラップ
また、以下のように変数の宣言時に"!"を使用し、
アンラップすることも可能です。暗黙的アンラップvar hoge: Int! = 1 var fuga: Int! = 1 print(hoge + fuga) // 2■強制的アンラップの注意点
強制アンラップは"!"をつけるだけで簡単にアンラップできる反面、
変数の値がnilの場合に強制アンラップすると、アプリケーションが落ちてしまいます。強制アンラップの注意点var hoge: Int? print(hoge!) // エラー変数に必ず値が入っていると確信がある場合のみ、
強制アンラップを使用するようにしましょう。2, オプショナルバインディング
そんな強制アンラップの危険を回避するために使用するのが、
こちらのオプショナルバインディングです。if文を用いてnilかどうかを判断します。
if-letを用いたオプショナルバインディング// 変数「hoge」がnilで無い場合 var hoge: String? = "値" if let unwrapped = hoge { print(unwrapped) } else { print("nilです") } // 値 /*-------------------------------*/ // 変数「hoge」がnilの場合 var hoge: String? if let unwrapped = hoge { print(unwrapped) } else { print("nilです") } // nilです変数がnilでなければ、if文に続く処理が実行されます。
変数がnilであれば、if文に続く処理がスキップされ、上記の例ではelseが実行されています。強制アンラップと違うところは、nilを許容する点です。
変数がnilであっても、アプリケーションが落ちること無く安全に実行することができます。また、guard文を用いてアンラップすることも可能です。
guardを用いたオプショナルバインディング// 変数「hoge」がnilで無い場合 var hoge: String? = "値" func someFunction() { guard let unwrapped = hoge else { print("nilです") return } print(unwrapped) } someFunction() // 値 /*-------------------------------*/ // 変数「hoge」がnilの場合 var hoge: String? func someFunction() { guard let unwrapped = hoge else { print("nilです") return } print(unwrapped) } someFunction() // nilです3, オプショナルチェイニング
オプショナルチェイニングもオプショナル型の変数の中身がnilの際、
安全にプログラムを実行できます。使用方法は、オプショナル型の変数のあとに"?"をつけます。
オプショナル型の変数?.プロパティ
オプショナル型の変数?.メソッド()
オプショナルチェインニングvar hoge: String? = "値" print(hoge?.count) // Optional(1) // ※.countとは、この場合は文字数を取得するものですオプショナルチェイニングは上記のように、オプショナル型の変数に続けてプロパティを取得したり、メソッドを呼び出す場合に使用します。
また、オプショナルチェイニングを使って取得した値はすべてオプショナル型となるので、
その値を使うには再度アンラップが必要になります。ですので、アンラップする方法かと言えば怪しいところですが、
一応の紹介でした。4, ??演算子
??演算子は、オプショナル型に値が存在しない場合のデフォルト値を指定します。
これは以下のコードを見るのが早いかと思います。??演算子// 変数「hoge」がnilで無い場合 var hoge: String? = "値" print(hoge ?? "nilです") // 値 /*-------------------------------*/ // 変数「hoge」がnilの場合 var hoge: String? print(hoge ?? "nilです") // nilですnil出ない場合はオプショナル型の変数に格納されている値を、
nilの場合は、??の後に記載されている値を出力します。4つのアンラップ方法の使い分け
強制アンラップ
- 絶対にnilで無いと確信が持てる場合にのみ使用する
オプショナルバインディング
- オプショナル型の変数がnilかどうかで、処理を分たい場合に使用する
オプショナルチェインニング
- オプショナル型の変数に続けてプロパティを取得したり、メソッドを呼び出す場合に使用する
??演算子
- オプショナル型の変数がnilの場合に、デフォルトの値を設定したい場合に使用する
まとめ
便利でもあり危険でもあるオプショナル型。
使用頻度が高いので、しっかり使用できるようにしたいですね。参考文献
- 投稿日:2021-02-28T10:46:26+09:00
Stringをindex指定でCharacterを安全に取得する
環境
Swift 5.3.2StringはCollectionに準拠しているので元々
text[position]
でアクセスできますが、subscriptの引数に指定できるものはString.Indexという型なのでIntを指定する事はできません。
そのため以下のextensionを記述することでInt型でCharacterを取得することができます。extension StringProtocol { subscript(offset: Int) -> Character? { guard offset < count else { return nil } let index = self.index(startIndex, offsetBy: offset) return startIndex <= index && index < endIndex ? self[index] : nil } }