ラベル ファイル の投稿を表示しています。 すべての投稿を表示
ラベル ファイル の投稿を表示しています。 すべての投稿を表示

2018年4月18日水曜日

macOS/画像ファイルの保存

iOSもいいけど、macOSのアプリも作ってみたいというので勉強始めた。

CIFilterをかけた画像を保存するアプリ。
ファイル保存のウィンドウNSSavePanelを表示したいが、ネットで調べたコードじゃエラーが出て表示されない。

わかったのは、最近Sandboxという仕組みになり、そのせいでファイルの扱いがReadOnlyになっていたのだ。
TARGETSのCapabilities / App Sandbox / File Access / User Selected File のPermission & Accessを、Read Only → Read/Write に変更してやったら(App SandboxのボタンがOFFになる)NSSavePanelが表示され、画像の保存もできた。
めんどっちーね。

なお、User Selected FileがRead/Writeになっていれば、App SandboxのボタンをONにしても保存できるようだ。やっていいのかどうかしらんけど。

Sandboxとはなんぞや?

どうやら砂場を意味していて、子供をその中で遊ばせときゃ安全…という概念で、プログラムを保護された環境下で動作させるものだという。(マイナビニュースより)

公式には以下のように書かれている。
macOSのApp Sandboxは、アプリケーションが意図された動作だけを行うようにする機能です。アプリケーションのサンドボックス化は、アプリケーションをあなたのMacの重要なシステムコンポーネント、あなたのデータ、そしてほかのアプリケーションから分離させます。もしアプリケーションに悪意のあるソフトウェアが含まれていたとしても、サンドボックス化が自動的にそれをブロックするので、あなたのコンピュータと情報は守られます。macOSでは、内蔵されたPDFビューアや、Adobe Flash Player、Silverlight、QuickTime、Oracle Javaなどのプラグインをサンドボックス化することで、Safariにサンドボックス保護機能を持たせています。さらにmacOSは、メール、メッセージ、FaceTime、カレンダー、連絡先、写真、メモ、リマインダー、Photo Booth、クイックルックプレビュー、Game Center、辞書、Font Book、Mac App Storeなどのアプリケーションもサンドボックス化します。

コード(クロージャ内で保存する場合)

勉強のためにいろいろ余計なプロパティも設定してみたが、こんな感じ。
savePanelの表示と一緒に、保存処理するクロージャを設定する場合。

//画像保存
@IBAction func saveImage(_ sender: Any) {
    let saveImage = myImageView.image
    
    let savePanel = NSSavePanel()
    savePanel.title = "画像保存だってよ" //表示されない?
    savePanel.prompt = "保存" //デフォルトボタンのテキスト
    savePanel.nameFieldLabel = "名前:" //ファイル名入力欄の左に表示されるテキスト
    savePanel.nameFieldStringValue = "名称未設定" //デフォルトファイル名
    savePanel.message = "画像を保存します" //ウィンドウ上部に表示される
    savePanel.canCreateDirectories = true //フォルダ新規作成ボタン表示
    savePanel.showsHiddenFiles = false //隠しファイルの表示
    savePanel.showsTagField = true //タグ付のフィールド表示有無
    savePanel.tagNames = ["画像","旅行"] //デフォルト表示するタグ
    savePanel.isExtensionHidden = true //「拡張子を隠す」ボタン。Finder設定で表示になってると機能しない
    let downloadPath = NSHomeDirectory() + "/Downloads" //Download[s]なので注意
    savePanel.directoryURL = URL(fileURLWithPath: downloadPath) //デフォルト表示のフォルダ
    savePanel.canSelectHiddenExtension = false //falseだとウィンドウ中のファイルが薄く表示される。選択はできる
    savePanel.allowedFileTypes = ["jpg","gif","png"] //許可するファイルタイプ(先頭の拡張子がデフォルトファイル名に付く)
    savePanel.allowsOtherFileTypes = false //許可されたファイルタイプ以外の許可(デフォルト不許可)
    savePanel.treatsFilePackagesAsDirectoriesfalse //アプリみたいなファイルパッケージの中までディレクトリとして表示するか否か
    
    savePanel.begin { (result) in
        if result == NSApplication.ModalResponse.OK {
            guard let tiffData = saveImage?.tiffRepresentation, //画像をTIFFのDataにする
                let imageRep = NSBitmapImageRep(data: tiffData), //bitmapデータを描画?
                let imageData = imageRep.representation(using: .jpeg, properties: [.compressionFactor : NSNumber(floatLiteral: 0.7)]) //JPEGに変換
                else {
                    print("画像変換エラー")
                    return
            }
            //画像保存処理
            do {
                try imageData.write(to: savePanel.url!)
            } catch {
                print("保存エラー \(error)")
            }
        }
    }
}

コード(delegateメソッド内で保存処理する場合)

savePanelは表示だけさせて、「保存」ボタンが押されたらdelegateメソッドで処理する場合。

//画像保存
@IBAction func saveImage(_ sender: Any) {
    let saveImage = myImageView.image
    
    let savePanel = NSSavePanel()
    savePanel.title = "画像保存だってよ" //表示されない?
    savePanel.prompt = "保存" //デフォルトボタンのテキスト
    savePanel.nameFieldLabel = "名前:" //ファイル名入力欄の左に表示されるテキスト
    savePanel.nameFieldStringValue = "名称未設定" //デフォルトファイル名
    savePanel.message = "画像を保存します" //ウィンドウ上部に表示される
    savePanel.canCreateDirectories = true //フォルダ新規作成ボタン表示
    savePanel.showsHiddenFiles = false //隠しファイルの表示
    savePanel.delegate = self
    savePanel.showsTagField = true //タグ付のフィールド表示有無
    savePanel.tagNames = ["画像","旅行"] //デフォルト表示するタグ
    savePanel.isExtensionHidden = true //「拡張子を隠す」ボタン。Finder設定で表示になってると機能しない
    let downloadPath = NSHomeDirectory() + "/Downloads" //Download[s]なので注意
    savePanel.directoryURL = URL(fileURLWithPath: downloadPath) //デフォルト表示のフォルダ
    savePanel.canSelectHiddenExtension = false //falseだとウィンドウ中のファイルが薄く表示される。選択はできる
    savePanel.allowedFileTypes = ["jpg","gif","png"] //許可するファイルタイプ(先頭の拡張子がデフォルトファイル名に付く)
    savePanel.allowsOtherFileTypes = false //許可されたファイルタイプ以外の許可(デフォルト不許可)
    savePanel.treatsFilePackagesAsDirectoriesfalse //アプリみたいなファイルパッケージの中までディレクトリとして表示するか否か
    
    savePanel.runModal() //savePanelの表示
}

//savePanelで保存ボタンを押した際に呼ばれるDelegateメソッド
func panel(_ sender: Any, validate url: URL) throws {
    let saveImage = myImageView.image

    guard let data = saveImage?.tiffRepresentation,
        let imageRep = NSBitmapImageRep(data: data),
        let imageData = imageRep.representation(using: .jpeg, properties: [.compressionFactor : NSNumber(floatLiteral: 0.7)])
        else {
            print("なんかエラーっす")
            return
    }
    do {
        try imageData.write(to: url)
    } catch {
        print("保存エラー \(error)")
    }
}
プロパティはだいたいこんな感じ

問題点

savePanelのローカライズが不完全。プロパティで設定したところしか日本語化されない。
savePanelの下の方に保存するファイル形式(jpeg, gif, pngとか)を選ぶポップアップメニューを表示したいけど、やり方がわからん。

2017年11月3日金曜日

ファイルPathとファイルURL

ファイルを保存したり、ファイルの有無を確認したりするのに、ファイルPathを用いる方法と、ファイルURLを用いる方法があり、結構混乱するのでまとめ。

ファイルPathとファイルURLの違い

ファイルPath

/var/mobile/Containers/Data/Application/97C2495D-1A6B-4E5D-BCAE-39C64563328A/Documents/ファイル名

ファイルURL

file:///var/mobile/Containers/Data/Application/97C2495D-1A6B-4E5D-BCAE-39C64563328A/Documents/ファイル名

以上のように、頭に「file://」が付いてる方がURL。
WebのURLの頭に「http://」が付いてるのと一緒だな。

Documentディレクトリを得る

Path(String型)で得る

let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)last!
【結果】
/var/mobile/Containers/Data/Application/97C2495D-1A6B-4E5D-BCAE-39C64563328A/Documents

URL型で得る

let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last!

【結果】
file:///var/mobile/Containers/Data/Application/97C2495D-1A6B-4E5D-BCAE-39C64563328A/Documents

Documentsディレクトリにファイル名を追加

URLの場合はそのまま追加できる

let fileURL = documentsURL.appendingPathComponent(fileName)

Pathの場合は一度URLに変換しないといけない

let fileURL = URL(fileURLWithPath: documentsPath).appendingPathComponent(fileName)

SwiftにはPathにファイル名などを追加するメソッドがない。(たしかObjective-Cにはあった)
そのため一度URL化する必要がある。めんどくせえ。

URLからPathに変換

let filePath = fileURL.path

URLのpathプロパティで得られる。

PathからURLに変換

let url = URL(fileURLWithPath: filePath)


引数のfilePathがPathと判断されない場合(例えばファイル名の文字列とか、空白文字とか)、
123456.jpg -- file:///
という、末尾に -- file:///が付いた変な形式に変換されてエラーの原因になるので注意が必要。
その場合は
let urlStr = URL(string: "123456.jpg")
とやるといい。

2017年11月1日水曜日

enumをエンコード/デコードしようとするとエラーになる

地図のタイプ(MKMapType)をファイルに保存すべく、

func encode(with aCoder: NSCoder) {
    aCoder.encode(mapType, forKey: "mapType")
}

などと書いたのだが、以下のようなエラーが出て落ちてしまった。

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x1c4242e80'

有効じゃないselectorがインスタンスに送られた…ってことらしいが、さっぱりわからん。
調べたところ、要はMKMapTypeがenum(列挙型)のため、そのままじゃencode、decodeできんのだと。
中身(RawValue)はUIntなので、
aCoder.encode(mapType.rawValue, forKey: "mapType")
でうまくいった。

decode側も、以下のように対処。
mapType = MKMapType(rawValue: UInt(aDecoder.decodeInteger(forKey: "mapType")))!
decodeInteger(forKey:)でIntとしてデコードして、それをUIntにキャストしてる。

mapType = MKMapType(rawValue: aDecoder.decodeInteger(forKey: "mapType")) as! UInt)!
としたかったけど、このやり方はできなかったので、UInt()で囲んだ。

グチ

カスタムクラスでも構造体でも列挙型でも、そのまんまファイルやUserdefaultsに読み書きできるように作らないのかねえ? めんどうだよね。

2017年7月12日水曜日

Documentsフォルダ内の画像ファイル読み書き

アプリ内にあるDocumentsフォルダに対して、画像ファイルを読み書きする方法。

Swift3ではURL(Swift2.xまではたしかNSURL)型で保存先情報を持つのだが、そのURLをそのまま引数に書いてやってもうまくいかない。
myURLに入っているなら、myURL.pathというプロパティで指定してやる。
引数はString型のようなのでmyURL.descriptionとやってみたがダメだった。

実機で試した場合のpathとdescriptio

ファイルpath
 /var/mobile/Containers/Data/Application/47DCD124-F9A4-4953-9B81-696DA790CCED/Documents/20170711055328142.jpg

ファイルdescription
 file:///var/mobile/Containers/Data/Application/47DCD124-F9A4-4953-9B81-696DA790CCED/Documents/20170711055328142.jpg

頭のfile:///の有無だけのようだ。

書き込み

//Documentsディレクトリのpathを得る(返り値はArrayで、index0がそれ)
let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
//StringappendingPathComponentがないのでURLに変換
let fileURL = URL(fileURLWithPath: docPath).appendingPathComponent(fileName)
//JPGに変換
let imageData = UIImageJPEGRepresentation(savePhoto, 1.0)
//画像書き込み(URLのpathを引数に)
//write(to:)はエラーを投げる関数なので、do-catch文が必要
do {
   //do-catchを使ってるので書込みエラーが起きるとcatchに移ってくれる
     try imageData?.write(to: fileURL, options: .atomic)
   //書き込み成功時の処理
   (省略)
   } catch let error {
   //書き込み失敗時の処理
     print("画像保存失敗 \(error)")
   }

読み込み

エラー処理なんかはしてない。

//Documentsディレクトリのpathを得る
let filePath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
//その後にファイル名を追加
//(String型のfileWithPathのメソッドがSwiftにないので、一度URL型に変更)
let fileURL = URL(fileURLWithPath: filePath).appendingPathComponent("ファイル名")
//.pathプロパティを引数に画像読み込み
let uiImage = UIImage(contentsOfFile: fileURL.path)

2016年8月4日木曜日

保存先のpathが毎回変わる?

iOSのファイル構造

アプリ内にはデベロッパーやiOSがデータを読み書きできるディレクトリがある。
例えば以下のようなもの。

  • /Documents
    • アプリ固有のファイルを保存
    • アプリを消すまでiTunesでバックアップされる
  • /Library/Preferences
    • アプリ固有の環境設定ファイルを保存
    • バックアップされるが、デベロッパーは直接いじっちゃダメらしい
  • /Library/Caches
    • アプリ固有のサポートファイルを保存
    • バックアップされないし、アプリがアクティブ時でも削除される可能性がある
  • /tmp
    • 一時ファイルを保存
    • バックアップされないが、アプリがアクティブ時は保持される
一番使うのはDocumentsディレクトリで、アクセスにはpathを作って読み書きする。

pathが毎回変わる!?

そのpathなのだが、実機で確認したところ、Xcodeから起動するごとに絶対pathの途中のディレクトリ名が変更されていることがわかった。
具体的には、以下の赤の部分が大きく変わっている。

/var/mobile/Containers/Data/Application/42F81D0D-EDC1-4BB3-AC03-BBE7D6DC8C79/Documents/ファイル名
/var/mobile/Containers/Data/Application/2E9C5905-1A89-40BA-B727-3DDD8AE13BC5/Documents/ファイル名

そのため、絶対pathでファイルを書き込み、次の起動時に読もうとすると、ファイルが存在せずに読めなくなるのだ。
なぜこうなってるのかわからないが、iOS8からの仕様らしい。

対処法

対処法だが、NSSearchPathForDirectoriesInDomains という関数があるので、それを使ってやれば起動ごとに変化するpathを得ることができる。(返り値は配列なので、index 0を参照すること)
もちろんディレクトリ内のファイルにもアクセスできる。

Swift
let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first ?? ""

Objective-C
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];

このpathの後に「/ファイル名 」を加えてやれば読み書きできるのだ。

2015年11月19日木曜日

NSUserDefaultsにenumを保存

変数currentStageは、下のように定義されてるenum(列挙型)の値が入ってる。
これをUserDefaultsに保存しようと思ったのだが、直接保存できないようなので調べたよ。

enum stage:String {
    case forest
    case beach
    case room
    case wheatField

}


まずそのまま保存するのはどうにもこうにも無理らしいので、扱いやすいrawValueを以下のように設定してやる。
ここではStringにしてる(使い方によってはIntでもいい)。
そのrawValueをString値(または数値)として保存するわけだ。

enum stage:String {
    case forest = "forest"
    case beach = "beach"
    case room = "room"
    case wheatField = "wheatField"

}

保存時
    func saveData() {
        let ud = NSUserDefaults.standardUserDefaults()
        ud.setObject(currentStage.rawValue, forKey: "currentStage")
    }


読み込んだら今度はそれをenum型に戻すんだけど、これがだいぶ悩んだ。
enum型にはメソッドがあって、rawValueを引数にしてやればいいのだな。
んで、引数の型をas! Stringとして強制的にStringに変換して。
さらにXcode様の指示どおり、最後にも!を付けたよ。
多分これでうまくいってる。

読み込み時
    func loadData() {
        let ud = NSUserDefaults.standardUserDefaults()
        //Stringで保存されたcurrentStageを列挙型stageの値に戻す
        currentStage = stage(rawValue: ud.objectForKey("currentStage") as! String)!
    }


もう21世紀なんだから、言語内で扱える型は全て直接読み書きできるようにすべきだと思うんだけど、どうだろうか?
いちいちこんなことで頭悩ますなんてアホらしいよね。


なお、UserDefaultsにあらかじめデータが入っていない状態で読み出そうとするとnilが返るため、きちんとチェックしておかないと最悪アプリが落ちることになる。
下記のようにif文のオプショナルバインディングというのを使うとコードが少し楽。
ud.objectForKey("currentStage")の結果がnilじゃなかったら値をtestに代入し、nilだった場合はtestにはfalseが入るためにelse文が実行されるというSwiftの仕組み。

        if let test = ud.objectForKey("currentStage") {
            //Stringで保存されたcurrentStageを列挙型stageの値に戻す
            currentStage = stage(rawValue: test as! String)!
            print("保存済み")
        } else {
            print("保存されてない")
        }