20210228のSwiftに関する記事は11件です。

RxSwiftでTodoAPP入門[part1]

概要

最近RxSwiftを勉強し始めて現在理解していることを備忘録として残せたらいいなと思い記事にします。
そもそもRxSwiftのRxとは

Rx(Reactive X)とは、「オブザーバパターン」「イテレータパターン」「関数型プログラミング」の概念を実装している拡張ライブラリです。
Rxを導入するメリットは、「値の変化を検知できる」「非同期の処理を簡潔に書ける」ということに尽きると思います。 値の変化というのは変数値の変化やUIの変化も含まれます。 例えばボタンをタッチする、という動作もボタンのステータスが変わったと捉えることができRxを使って記述することができます。

とのことです。
詳しくは以下のサイトを参照してください。
入門!RxSwift
RxSwiftについてようやく理解できてきたのでまとめることにした(1)

今回使用するライブラリ

RxSwift pod 'RxSwift'
--- ObservableなどのRx系の本領的なものを使うのに必要
RxCocoa pod 'RxCocoa'
--- UIKitでRxを使うのに必要
RxDataSources pod 'RxDataSources'
--- tableViewなどのdatasourceの扱いを楽にしてくれる
Firestore pod 'Firebase/Firestore'
--- todoの保存をするのに必要

以上のライブラリを使用していきます。

認証機能は?と思われたかもしれないですが今回は省かせていただきます。

また、今回の記事はfirebaseやpodは導入済みのものとして話を進めさせていただきます。

StoreManager

まずはこのアプリのもっとも重要なTodoの追加や取得の処理を書いていきます。
StoreManagerという名前のファイルを作ってください。

StoreManager.swift
import Foundation
import FirebaseFirestore
import RxSwift

class StoreManager {
    static let shared = StoreManager()
    private let store = Firestore.firestore()
}

こんな感じで定義してください。
ここは説明するまでもないと思うので説明を省略します。

次にTodoを保存する関数を作ります。
extensionで拡張すると可読性が上がるので拡張します。

StoreManager.swift
extension 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.swift
import 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の作成をしていきます。

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

【SwiftUI】contextMenuで長押ししたときにメニューを出す

先日リリースしたアプリに使用した技術をひとつずつ解説しています。
私のアプリはこちら。

contextMenuとは

任意のビューに対し、長押ししたときにメニューを出す機能を追加するメソッド。

実際の動作

実際にアプリを触らないとわかりづらいかもしれませんが、長押しすることでメニューが表示されています。また、メニューの外側をタップするとメニューが閉じるようになっています。
iOSアプリでよく見かける機能ではないでしょうか。

ソースコード

ContentView.Swift
import 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でTextSF Symbolsを使ったImageを表示させることができます。しかし、書いたものを自動的にボタンにする、という機能はありません。上のサンプルアプリのようにcontextMenuにボタンを表示させたい場合は、menuItemsにはButtonビューを書くのが良いでしょう。

まとめ

簡単なコードでおもしろくて便利な機能を実現できます。ぜひ、SwiftUIでcontextMenuを使ってみてください。

よかったらTwitterのフォローをお願いします。
https://twitter.com/masayoshi_tozan

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

[macOS][Swift5.3]マウスカーソルの画像をカスタマイズする方法

macアプリでプリセット以外のカーソルを表示する方法を見つけるのに時間がかかったため、誰かの役に立てばと思い書き記します。

1. 環境

macOS: 11.2.1 Big Sur
Swift: 5.3.2
Xcode: 12.4

2. 全コード

TestView.swift
import 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. 参考資料

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

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!})

  1. 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型の理解がかなり深まりました.何か間違っている点があれば,ご指摘いただけると幸いです.

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

アルバイト先のiOSアプリをリファクタリングした

アルバイト先の会社で、プロジェクトの整備、リファクタリングをしようということになりました。

アプリのプロジェクトはこれまでオフショアで海外に渡ったり、業務委託やアルバイトに渡ったりと、カオス状態でした。

今回は、まとまった時間を設けてこのレガシープロジェクトを少しでも改善しようということだったので、最近学んだ内容をアウトプットするいい機会だと思って色々やってみました。

「これまでそんな事もやってなかったのか」と思われるかもしれませんが、
誤りや改善点等ありましたら、ご指摘いただければ幸いです。

GitLabからGitHubへの移行

これまでGitLabで管理されていたプロジェクトをGitHubに移行することになりました。

私はREADMEやWikiの整備を行いました。

具体的には以下を含めました。

README
  • 開発環境
  • ライブラリ管理
  • セットアップ方法
  • API仕様書
Wiki
  • 簡単なディレクトリ構成
  • 主な使用ライブラリ
  • Gitやコードの命名規則

また、ブランチの運用も変更しました。

  • Main(リリース用)
  • Staging(テスト用)
  1. Mainからブランチを切って、作業用ブランチはStagingにマージ
  2. Stagingでテスト完了後にリリースバージョンのタグをつけてマージ
  3. Mainをリリース

rbenv で ruby のバージョンを指定

CocoaPods は ruby で作られたツールです。

チーム開発を行う場合、各々の PC の ruby のバージョンが異なるとライブラリのインストールに失敗したり、上手く動かないことがあるので rbenv を使って ruby のバージョンを統一しました。

bundler など他にもツールが出てきて一気に理解するのは大変でしたが、以下の記事では丁寧に解説されています。

Cocoapods と Carthage を共存させた iOS プロジェクトの始め方

SwiftLint の導入

SwiftLintを導入しました。

ルールに関しては、業務委託の方が普段使用されている.swiftlint.yml ファイルが導入されました。

それに少しルールを追加しただけで、かなり緩めにルールが設定されています。

下記の記事を参考にさせていただきました。

Swiftの静的解析ツール「SwiftLint」のセットアップ方法

IQKeyboardManager の導入

これに関しては、本当に入れるだけで障壁はほぼありませんでした。

IQKeyboardManager

リソース管理、R.swiftの導入

他言語対応されているはずなのに、NSLocalizedString が使用されずにハードコーディングされている箇所があったりと、こちらもカオス状態でした。

この機会なのでリソース管理のライブラリを入れようということで、導入がSwiftGenより簡単なR.swiftを導入しました。

注意点としては、生成された R.generated ファイルが、SwiftLint の制約エラーを起こすことがあるので、R.generatedファイルは .swiftlint.yml ファイルの excluded: に含める必要があります。

また、“キーワードが被っているという問題で、生成をスキップします“というような警告が出たので、リソース側の整理が必要でした。

現在は文字列しかほぼ書き換えられてないので、今後は画像、ストーリーボード、セルなども書き換える必要があります。

下記の記事を参考にさせていただきました。

R.swiftのセットアップ方法(Swift5)

リテラルが結構使用されていたのでこちらも。

R.Swiftでは文字リソースで「%@」を使うと、その部分を引数にとる関数を自動生成する

継承が想定されないクラスには final をつける

Swift ではクラスを継承することができますが、自作のクラスで他のクラスから継承しないものには final をつけるべきです。

自分が継承されない想定で作ったクラスを他の人が継承する実装にしてしまったというようなことがチーム開発ではあり得るので、それを防ぐためには final を適切に設定することが必要です。

アクセスレベルを適切に設定

変数やメソッドなどには private などのアクセスレベルを設定することができます。 例えばクラス内でのみ使用し、クラスの外から呼ばれることのないメソッドは private にすることができます。

final と同様に、アクセスレベルも適切に設定することが必要です。

他人がプロジェクトのコードを読むときに「このメソッドは private だから外から呼ばれることはないんだ」と判断できるので、迅速な理解に繋がるというメリットもあると思います。

下記の記事で、非常にわかりやすく説明されています。

アクセス修飾子を理解する

delegate メソッドを extension に分ける

ViewController の定義時に全ての Delegate を継承せず適切に extension で分けました。

主に UITableViewDelegate や UICollectionViewDelegate
、 UITextFieldDelegate のメソッドがそのままクラス内に書かれており、定義されているメソッドが delegate メソッドなのか独自のメソッドなのか分かりづらい状態でした。

こちらは自分で簡単にまとめたものを参考にしました。

extensionを用いて可読性を上げる

不要なコメントアウト、 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 文などで可読性を改善したいです。

まとめ

本来であれば、重複した処理を共通化したり、クラスの抽象化をしたりと、より高度なことをやりたいですが、現状の私のレベルで出来ることはこのくらいでした。

やるべき事は山積みですが、引き続きプロジェクトに貢献していきたいです。

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

CocoaPodsからSwiftyUserDefaultsを導入してみた

はじめに

SwiftyUserDefaultsの導入について備忘録としてまとめます。

導入の動機としては、SwiftのUserDefaultの使い勝手の悪さから、代替手段を模索していたところ
最終的にこのライブラリに辿り着いきました。

目次

  • プロジェクトへpodインストール
  • 実装方法
    • インポート
    • 定義と初期化
    • 値の保存方法
    • 値の読み出し方法
  • まとめ

プロジェクトへpodインストール

まずは、CocoaPodsをお使いのMacにインストールしておきます。

CLI
//コマンドラインで実行
pod init
Podfile
//Podfileへ下記をコードを追加
pod 'SwiftyUserDefaults', '~> 5.0'
CLI
//コマンドラインで実行
pod install

インポート

SwiftのUserDefaultを使うファイルに対し下記コードを挿入します。

.swift
import SwiftyUserDefaults

定義と初期化

まずは、プロジェクトへ定義する為のファイルを追加し、そこへ永続化したいキーを定義します。
SwiftyUserDefaultsでは定義と同時に初期値も指定できるので便利です。

DefaultsKeys.swift
import 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.swift
Defaults[\.test_str_nv] = "文字列を代入"
Defaults[\.test_int_nv] = 777

値の読み出し方法

write.swift
let read_str = keys[Defaults[\.test_str_nv]]
let read_int = keys[Defaults[\.test_int_nv]]

まとめ

SwiftyUserDefaultsを導入することにより、
*  Swift純正のUserDefaultよりも実装するコード量が減らすことができる
*  定義及び初期化する部分を一つのファイル内に集約できて分散防止が可能
*  値の保存及び読み出しが従来の様に文字列指定ではなく、変数指定になることでタイピングミスによる予期せぬ不具合を未然に防げる。
*  メンテナンスが楽になる
となります。

参考

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

Admobで広告を表示するときのTips

中々調べても出てこなかったので記事にします。
未リリースのアプリにAdmobを実装していて、テスト広告は表示できるのに本番の広告が表示されないという減少が起こっていました。

発生したエラー

その時のエラーは No ad to show というものが出ていました。

注意

本番の広告のみ表示できないなら、まずAdUnitIDが合っているかを確かめましょう。

次に、Admobの広告は、アプリをリリースするまで表示されません。
Admobの設定画面には、リリース済みのアプリを紐付けるためappIDを入力する箇所がありますが、リリースしてから4,5日経過しないと表示されません。
私の場合は、リリース後2日経ってから、appIDの紐付けをしなくても自動的に広告が配信され始めました。

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

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)
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

すべてのひらがな・カタカナなどのリストを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)
            }
    }()

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

【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) // nil

iOSでは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の場合に、デフォルトの値を設定したい場合に使用する

まとめ

便利でもあり危険でもあるオプショナル型。
使用頻度が高いので、しっかり使用できるようにしたいですね。

参考文献

どこよりも分かりやすいSwiftの"?"と"!"
[増補改訂第3版]Swift実践入門

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

Stringをindex指定でCharacterを安全に取得する

環境
Swift 5.3.2

Stringは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
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む