AWS S3(2)S3Sync:S3バックアップツール

S3Sync

以前、S3にバケットを作成してGlacierアーカイブを行う手順を確認したが、この仕組みを利用してMacの任意のディレクトリをS3 Glacierと自動的に同期するアプリケーション「S3Sync」を作ってみた。Macのスリープを検知すると同期を始めるので、寝ている間にラクラク同期できる。

といってもこのアプリ、単にNSTaskを使ってシステムコマンドを実行しているだけのアプリなので、任意のコマンドを自由に実行することができる。ステータスバーに常駐しているアプリなので、作業の邪魔にもならない。

S3Sync

スリープ検知

スリープ検知をするには、NSWorkspace ClassNSWorkspaceWillSleepNotification属性を使う。

    func applicationDidFinishLaunching(aNotification: NSNotification) {
        // スリープ検知
        NSWorkspace.sharedWorkspace().notificationCenter.addObserver(self, selector: #selector(self.receiveSleepNotification(_:)), name: NSWorkspaceWillSleepNotification, object: nil)
    }

    func receiveSleepNotification(notification: NSNotification){
        // スリープ実行時に行う処理
    }

システムコマンドの実行

システムコマンドは、NSTask Classから実行することが可能である。
NSTaskは、実行したシステムコマンドの出力結果を取り出すことも可能だが、readDataToEndOfFile()を使うとブロッキング処理が発生してしまうので、dispatch_async()を使って非同期に順次出力処理していく必要がある。

let task = NSTask() 
// 実行コマンドをフルパスで指定
task.launchPath = "/usr/local/bin/aws "
// パラメータを配列形式で指定
task.arguments = ["-h", "hogehoge"]
// 標準出力をパイプに渡す
let pipe: NSPipe = NSPipe()
task.standardOutput = pipe
let stdoutHundle = pipe.fileHandleForReading     
// 非同期処理
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), {
var dataRead = stdoutHundle.availableData
while(dataRead.length > 0){
	let stringRead = NSString(data: dataRead, encoding: NSUTF8StringEncoding)
        if let output = stringRead {
		// 出力結果処理
        }
        dataRead = stdoutHundle.availableData
}
// コマンドの実行
task.launch()

NSTaskは、suspend()terminate()を使って、途中で処理を停止したり、完全に終了してしまったりすることができる。suspend()で中断した処理は、resume()で再開することができる。また、タスクが実行中にも関わらず再度launch()を実行してしまうと、下記の実行エラーが発生してしまう。

task already launched

通知の送信

本アプリは、コマンド実行毎にMacの通知センターに実行状況を通知する。

アプリ通知

Macの通知センターに通知を送信するには、NSUserNotificationCenter Classを使う。送信する通知には、「タイトル」「サブタイトル」の他に様々な項目を設定することが可能である。

// NSUserNotificationCenterDelegateが必要
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {

    func deliverNotification(title : String, subtitle : String, informativeText: String){
        // AppDelegate Classにデリゲードを指定
        NSUserNotificationCenter.defaultUserNotificationCenter().delegate = self
        let notification = NSUserNotification()
        notification.title = title
        notification.subtitle = subtitle
        notification.informativeText = informativeText
        notification.contentImage =  NSImage(named: "MainIcon")
        notification.userInfo = ["title" : "タイトル"]
        NSUserNotificationCenter.defaultUserNotificationCenter().deliverNotification(notification)
    }

}

ログを保存するディレクトリの指定

本アプリは、実行ログをファイルに保存することができる。

NSOpenPanel

Macでディレクトリやファイルを開くする際は、NSOpenPanel Classを利用する。
また、保存の際はNSSavePanel Classというクラスも用意されている。

// MARK: ディレクトリ選択画面
let panel = NSOpenPanel()
// ファイル選択の可否
panel.canChooseFiles = false
// ディレクトリ選択の可否
panel.canChooseDirectories = true
// 複数選択の可否
panel.allowsMultipleSelection = false
panel.beginWithCompletionHandler({(num) -> Void in
      if num == NSModalResponseOK {
           // ディレクトリ・ファイル決定時の処理
      }
})

常駐アプリ

ステータスバーに常駐するアプリを作成するためには、Project > TARGET > Info > Custom OS X Application Target Propertiesから、Application is agent (UIElement)YESに設定する。

Application is agent

StoryBoardのTips

  • 常駐アプリであっても、Main Menu > Edit がないと、TextFieldの Shortcut Keyが使えない

Main Menu : Edit

WKWebViewの使い方

WKWebView

WKWebViewはiOS8から新たに使用できるようになったWebViewで、WebKitFrameworkの中に含まれている。これまでのUIWebViewと比べると速くて高機能とのこと。使用方法は以下の通り。

WKWebViewの初期化

最初に初期化しておく。

private var webView = WKWebView()

各種設定

AutoLayout、Delegateなどの設定をviewDidLoad()で行う。デフォルトではジェスチャーが無効なので、有効にするためにはコードで明示する必要があることに注意が必要である。

    override func viewDidLoad() {
        // Autolayoutを設定
        webView.translatesAutoresizingMaskIntoConstraints = false
        // 親ViewにWKWebViewを追加
        self.view.addSubview(webView)
        // Delegateの設定
        self.webView.UIDelegate = self
        self.webView.navigationDelegate = self
        // WKWebViewを最背面に移動
        self.view.sendSubviewToBack(webView)
        // レイアウトを設定(後述)
        setWebViewLayoutWithConstant(0.0)
        // ジェスチャーを許可
        webView.allowsBackForwardNavigationGestures = true
        // ページのロード
        self.webView.loadRequest(NSURLRequest(URL: NSURL(string: url)!))
    }

終了時

親ビューから削除する。Autolayoutの値もリセットされる。

    override func viewDidDisappear(animated: Bool) {
        webView.removeFromSuperview()
    }

レイアウト設定

WKWebViewは、StoryBoard上で割り付けることが出来ない。他のパーツをStoryBoard上でAutolayout設定している場合は、Swift上でWKWebViewにAutolayout設定を行う。ConstraintはWebViewに対して指定するのではなく、親Viewに対して指定することに注意が必要である。

    func setWebViewLayoutWithConstant(constant: CGFloat){
        // Constraintsを一度削除する
        for constraint in self.view.constraints {
            let secondItem: WKWebView? = constraint.secondItem as? WKWebView
            if secondItem == self.webView {
                self.view.removeConstraint(constraint)
            }
        }
        // Constraintsを追加
        self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: webView, attribute: NSLayoutAttribute.Width, multiplier: 1.0, constant: 0.0))
        self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: webView, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0.0))
        self.view.addConstraint(NSLayoutConstraint(item: self.topLayoutGuide, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: webView, attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant: 0.0))
        self.view.addConstraint(NSLayoutConstraint(item: self.bottomLayoutGuide, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: webView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: constant))
    }

以上により、ViewController上でWKWebViewを表示させることができる。goBack()やreload()はUIWebViewと同様に使用できる。

戻るボタンの実装

前のページに戻るためには、goForward()ではなぜかうまく動かなかった。以下のようにするとうまく動く。

let url = webView.backForwardList.forwardItem?.URL
webView.loadRequest(NSURLRequest(URL: url!))

プログレスビューの実装

ページ読み込みの進捗状況を表示するプログレスビューを表示することもできる。プログレスビュー自体のレイアウトや表示位置は、StoryBoard上で設定し、表示処理をSwiftで記述すると簡単である。

    // プログレスビュー描画間隔
    private let timerDur = 0.1

    override func viewDidAppear(animated: Bool) {
        // プログレスビューの描画
        progressView.setProgress(0.0, animated: false)
        progressView.hidden = false
        timer = NSTimer.scheduledTimerWithTimeInterval(timerDur, target: self, selector: "updateProgressView", userInfo: nil, repeats: true)
    }

    override func viewDidDisappear(animated: Bool) {
        if timer?.valid == true {
            timer?.invalidate()
        }
    }

    /**
        プログレスビューを更新する
    */
    func updateProgressView() {
        progressView.setProgress(Float(webView.estimatedProgress), animated: true)
        if webView.estimatedProgress == 1.0 && timer?.valid == true {
            timer?.invalidate()
            progressView.hidden = true
        }
    }

新規タブで開く

WKWebviewでURL読み込み時に任意の処理を実行したい場合には、decidePolicyForNavigationAction関数内に処理を記述する。新規タブで開く(target = _blank)の指定があった場合にデフォルトでは動作しないので、以下の記述が必要である。

if navigationAction.navigationType == WKNavigationType.LinkActivated{
                if targetFrame == nil {
                    webView.loadRequest(NSURLRequest(URL: url))
                }
            }

UIActivityの使い方

UIActivityとは

UIActivityは、画面下からModal(CoverVertical)で出現する、FacebookやTwitterなどのSNS共有やメール添付の機能をアイコン一覧で表示するActivityである。任意の機能やアイコンを追加する場合は、UIActivityを継承したクラスを作成する必要がある。

UIActivity

カスタムUIActivityの作成

UIActivityを継承したクラスには、「表示するタイトル」や「アイコンイメージ」、「ボタンを押したときの動作」などを記述する。下の例は、「Safariで開く」というアイコンと機能をUIActivityに追加した場合のコードである。

import UIKit

class SafariActivity: UIActivity {
    
    var url: NSURL? = nil
    
    // 表示するタイトル
    override func activityTitle() -> String? {
        return "Safariで開く"
    }

    // 表示するアイコン    
    override func activityImage() -> UIImage? {
        return nil
    }
    
    // どのようなアイテムが投入された場合に機能が働くかを定義する
    // 機能が動作する条件の場合には true を返す
    override func canPerformWithActivityItems(activityItems: [AnyObject]) -> Bool {
        for activityItem in activityItems {
            // NSURLかつURLを開くことが出来る場合は動作する
            if activityItem.isKindOfClass(NSURL) && UIApplication.sharedApplication().canOpenURL(activityItem as! NSURL) {
                    return true
            }
        }
        return false
    }

    // 機能が動作する直前の処理を記す    
    override func prepareWithActivityItems(activityItems: [AnyObject]) {
        for activityItem in activityItems {
            if activityItem.isKindOfClass(NSURL){
                url = activityItem as? NSURL
            }
        }
    }
    
    // 機能のふるまいを記述する
    override func performActivity() {
        // SafariでURLを開く
        UIApplication.sharedApplication().openURL(url!)
        self.activityDidFinish(true)
    }
    
}

上記機能を含んだUIActivityを表示するには、以下のように定義したクラスを呼び出す。

// Data Item
let items = [NSURL(string: webView.URL!.description)!]
// Application Activity
let activities = [SafariActivity()]
// 上記2つを引数に指定しUIActivityViewControllerを生成
let activityController :UIActivityViewController = UIActivityViewController(activityItems: items, applicationActivities: activities)
UIActivityViewControllerを表示
self.presentViewController(activityController, animated: true, completion: nil)

以上でカスタムのUIActivityを表示することが可能となった。

UITableViewの使い方

テーブルデータの再読み込み

データの更新などを行った後、スレッド内で単に

tableView.reloadData()
[(UITableView)tableView reloadData]

とやると落ちてしまう。そこで以下のようなコードを記述することによって、データの再読み込み処理をメインスレッドに戻して処理させるようにする。

dispatch_async(dispatch_get_main_queue(), { () -> Void in
    self.tableView.reloadData()
})
[(UITableView)tableView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO];

また、下記のようにUserDefaultのデータをCellにデータを反映させる場合に、

@IBOutlet weak var label: UILabel!

override func viewWillAppear(animated: Bool) {
        let defaults = NSUserDefaults.standardUserDefaults()
        label.text = defaults.valueForKey("label")! as? String
}

viewWillAppear()でreloadData()しただけでは、うまくテーブルが更新されないので、

override func viewWillAppear(animated: Bool) {
        let defaults = NSUserDefaults.standardUserDefaults()
        label.text = defaults.valueForKey("label")! as? String
        tableView.reloadData()
}

viewDidAppear()でもreloadData()する必要がある。

    override func viewDidAppear(animated: Bool) {
        tableView.reloadData()
    }

セルのハイライトを解除する

UITableViewControllerを使用しない場合、View遷移時のセルハイライトの解除を自分で記述する必要がある

tableView.deselectRowAtIndexPath(tableView.indexPathForSelectedRow, animated: true)
[(UITableView)tableView deselectRowAtIndexPath:[(UITableView)tableView indexPathForSelectedRow] animated:YES];

セル選択時にハイライト表示しない

セルを選択した場合もハイライト表示としない

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
        cell.selectionStyle = UITableViewCellSelectionStyle.None
        return cell
    }

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        tableView.deselectRowAtIndexPath(indexPath, animated: true)
    }
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	// セル選択時のスタイルを無表示スタイルに設定する
	cell.selectionStyle = UITableViewCellSelectionStyleNone;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	// セル選択時も非選択状態とする
	[tableView deselectRowAtIndexPath:indexPath animated:YES];
}

セル選択不可

UITableView内全てのセルを選択不可にする

tableView.allowsSelection = false
[(UITableView)tableview setAllowsSelection:NO];

ヘッダビューを追加する

UITableView上部にヘッダビューを追加する

tableView.tableHeaderView = view
(UITableView)tableView.tableHeaderView = (UIView)view;

追加するViewは、ViewcontrollerのサブViewにしない。
文字数に合わせたセルの高さを設定する

セルの高さを設定する

  • テーブル内のUILabelの文字数に合わせてセルの高さを指定する
    - (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath  
    {
    	// セルの取得
    	UITableViewCell *cell = [self tableView:_tableView cellForRowAtIndexPath:indexPath];  
    	// 最大サイズ
    	CGSize bounds = CGSizeMake(_tableView.frame.size.width, _tableView.frame.size.height);
    	// ラベルサイズ
    	CGSize size = [cell.textLabel.text sizeWithFont: cell.textLabel.font
    	constrainedToSize: bounds
    	lineBreakMode: UILineBreakModeCharacterWrap];
    	return size.height;
    }
    

  • セルの高さを自動設定する

    // とりあえず高さを指定
    tableView.estimatedRowHeight = 100
    // 高さを自動指定
    tableView.rowHeight = UITableViewAutomaticDimension
    

プロトタイプセルに含まれる子ビューを指定する

Storyboard上で設定したPrototypeCellに含まれるUIImageViewやUILabelをコード上から指定する場合、それぞれのViewにラベルを付与しておくことで、ラベル番号を指定してViewを取得することが可能である。

Prototype Cells

プロトタイプセル上にUIImageViewを作成し、Tag=1を指定している。

Prototype Cells Property

上記の例で、UIImageViewにSwift上からアクセスする場合は、

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
        let imageView: UIImageView = cell.viewWithTag(1) as! UIImageView
        return cell
    }

とすれば良い。