脳汁portal

アメリカ在住(だった)新米エンジニアがその日学んだIT知識を書き綴るブログ

SwiftによるiPhoneアプリ開発入門(超初歩)その10 -データの保存(ユーザデフォルト)-

今回はデータの保存ができるアプリを開発します。
iOSではアプリのインストール時に、アプリ毎のサンドボックスが用意されます。各アプリは通常、サンドボックス内のファイルへのみアクセスが可能です。そのため、アプリ内の情報を他のアプリから盗まれたり、逆にシステムの重要なファイルを誤って破壊することを防ぐことができます。

イメージ

f:id:portaltan:20150727211936p:plain

コンテナ

サンドボックス内には用途により複数のコンテナが作成されます。

ディレクトリ


コンテナ ディレクトリ名 説明
Bundler Container アプリ名.app アプリ本体と、そこに含まれるリソース格納用ディレクトリ
Data Container Documents ユーザの作成したコンテンツ用ディレクトリ
iTunesのバックアップ対象
Documents/Inbox 他のアプリから受け取ったデータを保存するディレクトリ
iTunesのバックアップ対象
Library ユーザの作成したコンテンツ以外の格納用ディレクトリ
iTunesのバックアップ対象
Library/Cache キャッシュ用ディレクトリLibararu以下でもこのディレクトリはバックアップ対象にならない
tmp アプリ起動中のみデータを保持する一時ファイル用ディレクトリ

ユーザデフォルトの利用

  • アプリ内の設定情報を保存しておくためのもの
  • フォントサイズとか背景色とか、ユーザが選択した背景とか
  • 大量のデータを保存するには向かない
  • 利用するにはNSUserDefaultクラス
  • 格納先やファイル形式などを意識する必要はない
  • key と値で管理

実際に実装

1, Tabbed Applicationとしてアプリを作成

f:id:portaltan:20150727204201p:plain

2, ストーリーボード上に二つのシーンが作成されるので、SecondViewシーンにSegmented Controlを配置してプロパティを設定する

f:id:portaltan:20150727204602p:plain

  • segment0 => White
  • segment1 => Gray
  • segment2 => Brown
  • segment3 => Green
3, SecondViewControllerへsegmentからアウトレット接続(プログラムからの値の設定)とアクション接続(選択時に処理の呼び出し)の両方をしておく。

f:id:portaltan:20150727204944p:plain

f:id:portaltan:20150727204949p:plain

4, FirstViewControllerへ処理を実装
  • 背景色の設定
    //利用する背景色を定義
    enum BackgroundColorType: Int {
        case White = 0
        case Gray, Brown, Green
    }
  • データを取得して利用する処理を実装
    // 画面が表示される前に、毎回実行されるライフサイクルメソッド
    override func viewWillAppear(animated:Bool) {
        
        // NSUserDefaultsオブジェクトを取得
        let defaults = NSUserDefaults.standardUserDefault()
        
        // NSUserDefaultsに格納された値を取得
        let colorIndex = defaults.integerForKey("colorIndex")
        
        // 背景色にするためのUIColor
        let selectedColor: UIColor
        
        //取得した値を元に列挙型を生成し、たいおうする値がある場合のみ背景色を設定
        if let color = BackgroundColorType(rawValue: colorIndex) {
            
            //取得した設定値を元にビューの背景色を設定
            switch color {
            case .White:
                selectedColor = UIColor.whiteColor()
            case .Gray:
                selectedColor = UIColor.grayColor()
            case .Brown:
                selectedColor = UIColor.browncolor()
            case .Green:
                selectedColor = UIColor.greenColor()
            }
            
            // 背景色を設定
            self.view.backgroundColor = selectedColor
        }
    }
5, SecondViewControllerを実装
  • SegmentedControlの設定
    @IBAction func selectColorSegment(sender: UISegmentedControl) {
        
        //セグメントの選択値を取得
        let selectedIndex = sender.selectedSegmentIndex
        
        // NSUserDefaultsオブジェクトを取得
        let defaults = NSUserDefaults.standardUserDefaults()
        
        // NUUserDefaultsにセグメントの選択値を保存(キーは"clockIndex"を指定)
        defaults.setInteger(selectedIndex, forKey: "colorIndex")
        
        // 設定値をファイルシステムへ書き込み
        if defaults.synchronize() {
            println("設定値の保存成功")
        }
    }
  • 画面表示時に、前回終了時と同じ画面を表示させる実装
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 画面表示時にあらかじめ、以前の設定値を元にセグメントの項目を設定しておく
        let defaults = NSUserDefaults.standardUserDefaults()
        let colorIndex = default.integerForKey("colorIndex")
        colorSegment.selectedSegmentIndex = colorIndex
    }

これで一度アプリを落としても、前回の選択した値を利用するアプリができました。

SwiftによるiPhoneアプリ開発入門(超初歩)その9 -リストからの遷移と、編集と削除-

今回は前回作成したセクションに分けたリストに、以下の機能を付け加えます

  • 詳細画面への遷移
  • セルの編集(並び替え)機能
  • セルの削除機能

前回のリスト構造
SwiftによるiPhoneアプリ開発入門(超初歩)その8 -複数セクション(グループ分け)したリスト- - 脳汁portal

完成イメージ
f:id:portaltan:20150726193856p:plain

1、ストーリーボード上のシーンにナビゲーションコントローラを追加

f:id:portaltan:20150726184723p:plain

f:id:portaltan:20150726185310p:plain

2、シーンのNavigation Itemの設定をする

f:id:portaltan:20150726185440p:plain

3、セルのプロパティを編集し、詳細画面への遷移のアイコンを追加

f:id:portaltan:20150726185639p:plain

4、詳細画面のシーンとビューコントローラを作成して対応づける

f:id:portaltan:20150726193214p:plain

f:id:portaltan:20150726193219p:plain

5、詳細画面のシーンにUIImageViewを配置し、DetailViewControllerへアウトレット接続する

f:id:portaltan:20150726190024p:plain

6、詳細画面のビューコントローラを作成
    // 表示する何ファイル名の受けとり
    var imageFileName: String = ""
    
    @IBOutlet weak var imageView: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // 画像を表示
        imageView.image = UIImage(named: imageFileName)
    }
7、一覧画面から詳細画面のシーンへのセグエを接続

f:id:portaltan:20150726190448p:plain

f:id:portaltan:20150726193603p:plain

8、一覧画面のビューコントローラに実装を追加
    override func viewDidLoad() {
        super.viewDidLoad()

        // ナビゲーションバーの右側に編集ボタンを追加
        self.navigationItem.rightBarButtonItem = self.editButtonItem()
    }
9、セルの削除と追加機能を実装する
    // セルの削除と追加を行う
    override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        
        if editingStyle == .Delete {
            // 削除の場合の処理
            // テーブルビューから削除するだけではなく、必ず実際のデータを先に削除
            items[indexPath.section].removeAtIndex(indexPath.row)
            
            // テーブルビューから、指定した行のデータを削除
            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        } else if editingStyle == .Insert {
            // 挿入の場合の処理
        }
    }
10、セルの移動を行う
    // セルの移動を行う
    override func tableView(tableView: UITableView, moveRowAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) {
        
        // sourceIndexPathの位置にあるデータを削除
        let item = items[sourceIndexPath.section].removeAtIndex(sourceIndexPath.row)
        
        // destinationIndexPathの位置に、削除したデータを追加
        items[destinationIndexPath.section].insert(item, atIndex: destinationIndexPath.row)
    }
11、一覧画面から詳細画面へデータの代入
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        
        let dest = segue.destinationViewController as! DetailViewController
        
        // tableViewプロパティから、選択行のindexPathを取得
        let indexPath: NSIndexPath? = self.tableView.indexPathForSelectedRow()
        
        // 選択行のindexPathが取得できた場合、次の画面にファイル名を渡す
        if let section = indexPath?.section, row = indexPath?.row {
            dest.imageFileName = items[section][row].fileName
        }
    }

ちなみに、

  • as!は強制的に型をキャストして、できなければエラーを返す
  • as?はキャストできなければnilが帰る

SwiftによるiPhoneアプリ開発入門(超初歩)その8 -複数セクション(グループ分け)したリスト-

前回はセクションを分けない(グループ分けしない)でリストを一括表示しましたが、今回はリストをセクションに分けてみます。
f:id:portaltan:20150726183225p:plain

1、リスト上に表示させる画像をSupporting Filesディレクトリへ配置

2、ストーリーボードから既存のシーンを削除し、Table View Controllerをl配置して初期画面に設定
f:id:portaltan:20150726165606p:plain

3、セルのプロパティを編集し、セルのスタイルとidentifierを設定f:id:portaltan:20150726165813p:plain

4、ドキュメントアウトラインから、Table View を選択し、右の設定画面でStyleをGroupedに設定
f:id:portaltan:20150726170050p:plain

5、既存ビューコントローラを削除し、UITableViewControllerのサブクラスを作成して実装
f:id:portaltan:20150726180409p:plain

6、セクションのヘッダに表示するデータとテーブルに表示するデータを実装

    let sectionHeaderTexts = ["女性向け商品", "男性向け商品"]
    
    var items: [[(text: String, detail: String, fileName: String)]] = [
        [
            ("メガネ", "知的な印象を与えます", "glasses1.jpeg"),
            ("帽子", "日差しを避けて涼しさを。", "hat1.jpeg"),
            ("ドレス", "街中で視線を集めます", "suit1.jpeg"),
            ("シューズ", "歩きやすさを重視", "shoes1.jpeg"),
        ],
        [
            ("メガネ", "個性が光ります", "glasses2.jpeg"),
            ("帽子", "夏にぴったりです。", "hat2.jpeg"),
            ("ドレス", "機能性が非常に高いです。", "suit2.jpeg"),
            ("シューズ", "山歩きにどうぞ", "shoes2.jpeg"),
        ]
    ]

7、セクションの設定を実装

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        // 作成するセクションの数を返す
        return sectionHeaderTexts.count
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // セクション毎の行数を返す
        return items[section].count
    }

    // セクションのヘッダー文字列を返す
    override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return sectionHeaderTexts[section]
    }
    

8、セクションを作成して返す

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        
            // 再利用できるセルがあれば取得する
            // 第一引数には、ストーリーボードで設定したセルのidentifierを指定
            let cell = tableView.dequeueReusableCellWithIdentifier("myCell", forIndexPath: indexPath) as! UITableViewCell
        
            // セクション番号
            let section = indexPath.section
        
            // セクション内での番号
            let rowInSection = indexPath.row
        
            // 表示するUIImageのファイル名
            let fileName = items[section][rowInSection].fileName
        
            // セルの内容を設定
            cell.textLabel?.text = items[section][rowInSection].text
            cell.detailTextLabel?.text = items[section][rowInSection].detail
            cell.imageView?.image = UIImage(named: fileName)
        
        return cell
    }

9、作成したTableViewControlをストーリーボード上のシーンと対応付け
f:id:portaltan:20150726182911p:plain

SwiftとRubyのnilとif文の扱い

nil

Swiftでのnilの扱い

JavaでいうNullPointerExceptionを防ぐため、nilは代入自体ができなくなっている
var foo = nil

//===>
//Playground execution failed: nil_if.playground:5:11: error: expression does not conform to type //'NilLiteralConvertible'
//var foo = nil

これはnilが返ってくる可能性があるだけでダメで、もし返ってくる場合は値を受け取る変数をoptional型という型にする必要がある。
型宣言の際に後ろに?とつけるとoptionalという型になり、nilを受け取れるようになる

var foo: String  = nil  // => NG
var bar: String? = nil // => OK

このoptional型は通常の型ではないため、他の型の変数へは普通に代入できない。
(nilが含まれる可能性があるため)
なので、もし代入をしたい場合にはif-let文を使う

var bar: String? = "test"

// NG
var bar2: String = bar

// OK
if let bar3: String = bar {
    println(bar3)
} else {
    println("bar is nil")
}

もしくは、以下のような構文を使うこともできる

// var \(変数名) = \(代入したい変数) ?? \(代入したい変数がnilの場合に代わりに代入する値)

var foo: String? = "test"
var foo2: String = foo ?? "bar"  # => "test"

var hoge: String? = nil
var hoge2: String = hoge ?? "fuga" # => "fuga"

Rubyでのnilの扱い

Rubyは全てを受け入れる・・・
  • Rubyではnilだろうがなんだろうが代入できる。
foo = nil
puts foo  # => nil
puts foo.class # => NilClass

その代わりに値が入っていないことに関連したエラーには気を配らなければならない

foo.each{|i|
  puts i
}
# ===> 
#NoMethodError: undefined method `each' for nil:NilClass
#	from (irb):5
#	from /usr/bin/irb:12:in `<main>'




if文 (条件文)

Swiftでの条件文

Swiftでは厳密にboolean(が返り値のもの)しかif文には入れることができない
if true {
    println("TRUE")
}
// => "TRUE"

if false {
    println("TRUE")
} else {
    println("FALSE")
}
// => "FALSE"

if "true" {
    println("TRUE")
}
// => Playground execution failed: nil_if.playground:16:4: error: 'String' is not convertible to 'BooleanType'

if nil {
    println("TRUE")
}
// => Playground execution failed: nil_if.playground:16:4: error: expression does not conform to type 'NilLiteralConvertible'

Rubyでの条件文

Rubyは全てを平等に判断してくれる・・・
  • Rubyでは条件文にどんな型の値でも入れることができる
  • false, nilはfalseとして判断され、それ以外は全てtrueとして判断される
  • ""false"という文字列や、空文字("")でもtrueとして判断されるので注意が必要である
if true
  puts "TRUE"
enb
# => "TRUE"

if false
  puts "TRUE"
else
  puts "FALSE"
end
# => "FALSE"

if nil
  puts "TRUE"
else
  puts "FALSE"
end
# => "FALSE"

### ""false"という文字列や、空白でもtrueとなるので注意が必要
foo = "false"
if foo
  puts "TRUE"
end
# => "TRUE"

bar = ""
if foo
  puts "TRUE"
end
# => "TRUE"

[Swift] SwiftとRubyの変数(定数)宣言

Swiftの超初歩的なことをRubyの文法と比較して書き記します。

テスト環境

Swiftの勉強には、Xcodeに組み込まれているplaygroundという機能がとても便利です。
Xcodeの起動画面で一番上の項目を選べば使用できます。
f:id:portaltan:20150708215239p:plain

エディタが開きます。
f:id:portaltan:20150708215359p:plain

変数宣言

Swift

// 定数
let numA = 10

// 変数
var numB = 20

// 型宣言のみ
var numC: Int
  • この時にOptionを押しながら、変数(定数)をクリックすると、その変数(定数)の情報が見れます。

f:id:portaltan:20150708215830p:plain

Ruby

// 定数
Nam_A = 10

// 変数
num_B = 20
  • ちなみにですが、Rubyで型チェックをするときは#{変数名}.classメソッドを使えばわかります。

定数への再代入

Swift

// 定数
let numA = 10

// 定数へ再代入
numA = 100

Swiftでは定数に再代入しようとすると怒られてできません。
f:id:portaltan:20150708221023p:plain

Ruby

$ irb
irb(main):001:0> Nam_A = 10
=> 10
irb(main):003:0> Nam_A = 20
(irb):3: warning: already initialized constant Nam_A
(irb):1: warning: previous definition of Nam_A was here
=> 20
irb(main):004:0> puts Nam_A
20
=> nil
  • Rubyでは同じく怒られますが、警告されるだけで値は変更されます。