どこでもDooor

参加した勉強会 / 読んだ技術書 / 見聞きした備忘録 を気ままに書いていきます

【iOS】UITableViewにプルリフレッシュ機能を付ける

UIRefreshControl

テーブルビューを引っ張って、データを更新する機能を付けたいと思って調べていたのですが、ライブラリを使わなくても簡単に実装できることが判明!
そこで見つけたのが、iOS6から使えるようになったUIRefreshControlでした!(知らなかった...)

UIRefreshControl Class Reference

実装自体はすぐにできたので、リファレンスは見なくてもいいかもしれない。

手順

UITableViewにUIRefreshControlをaddSubView
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self,action:"refreshControlAction:",forControlEvents:UIControlEvents.ValueChanged)
refreshControl.tintColor = UIColor.redColor()
myTableView.addSubview(refreshControl)
処理を実装
func refreshControlAction(refreshControl: UIRefreshControl) {
    /* なんらかの処理
     */
    //処理後にぐるぐる回っているのを消す
    refreshControl.endRefreshing()
}

これだけでできるんですね。感動しました...

【Rails】Wheneverを使ってバックグラウンド処理をしたい

github

https://github.com/javan/whenever

Wheneverでできること

Railsでcronのバッチを作成して、以下の4つのコマンドを定期的に走らせることができる。

  • command
    • bashコマンドの実行
  • rake
    • rakeコマンドの実行
  • runner
  • script
    • scriptの実行

ターミナルでよく使うコマンド一覧

/config/schedule.rbを作成

$ wheneverize .

cronの設定の確認

$ bundle exec whenever

cronに処理を登録

$ bundle exec whenever --update-crontab

cronに登録した処理の削除

$ bundle exec whenever --clear-cron

手順

Gemfile
gem 'whenever', :require => false
ターミナル
$ wheneverize .
> [add] writing `./config/schedule.rb'
> [done] wheneverized!
/config/application.rb

lib/tasks/***.rbを読み込む場合、lib配下をロードするように設定する。

module xxx # railsアプリ名
  class Application < Rails::Application
    config.autoload_paths += %W(#{config.root}/lib)
    config.autoload_paths += Dir["#{config.root}/lib/**/"]
  end
end
/config/schedule.rb
# 実行環境の設定
set :environment, :development

# 実行処理の内容
every 2.hours do
  command "/usr/bin/some_great_command"
  runner "MyModel.some_method"
  rake "some:great:rake:task"
end

every 4.days do
  runner "AnotherModel.prune_old_records"
end

【Rails】ActiveAdmin

ActiveAdminをインストール

手順

①Gemfileに記入

gem 'devise'
gem 'activeadmin', github: 'activeadmin'

②プロジェクトにインストール

$ bundle install
$ rails g active_admin:install

③インストール時に出た警告を修正

config/environments/development.rb
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
config/routes.rb
root to: "home#index
app/views/layouts/application.html.erb
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
db/migrate/201501222120_devise_create_admin_users.rb
## コメントアウトする

## Recoverable
# t.string   :reset_password_token
# t.datetime :reset_password_sent_at

...

add_index :admin_users, :email,                unique: true
# add_index :admin_users, :reset_password_token, unique: true

...

マイグレーション

$ rake db:migrate

⑤モデルを作成

$ rails g model Article title:string content:text
$ rake db:migrate

⑥ActiveAdminで管理する

このコマンドをやらなければ、adminには表示されません。

$ rails g active_admin:resource Article

⑦ストロングパラメータの許可

app/admin/article.rb
ActiveAdmin.register Article do
  controller do
    def permitted_params
      params.permit article: [:title, :content]
    end
  end
end

⑧articleと1対多のモデルを新たに作成

$ rails g model Comment commenter:string body:text article:references
$ rake db:migrate
$ rails g active_admin:resource Comment

⑨ストロングパラメータの許可

  controller do
    def permitted_params
      params.permit comment: [:commenter, :body, :article_id]
    end
  end

※例外的に必要な処理

今回、モデルにcommentを作成したため、ActiveAdminで元々使用されているcommentと被ってしまった。以下のようなエラーが出てしまいました...

`raise_if_mismatched!': You're trying to register ActiveAdmin::Comment as Comment, but the existing ActiveAdmin::Resource config was built for Comment! (ActiveAdmin::ResourceCollection::ConfigMismatch)
config/initializers/active_admin.rb
config.comments_registration_name = 'AdminComment'

ここまでやれば一通り動かせると思います。
管理画面はRailsAdminと比較してどっちを使うか決める感じになりそうです。
久しぶりの投稿、、これからは継続して投稿していきたい...
Cocos2d-xを2月から触っていくことになったのでそっちも書いていきたいです。

【iOS】デリゲート

デリゲートの設定

今回は完全に忘れないためのメモ。

Objective-C

Sample.h

#import <UIKit>

@class Sample;

@protocol SampleDelegate <NSObject>

- (void)sampleDelegateMethod;

@end

@interface Sample : NSObject

@property (nonatomic, weak) id<SampleDelegate>delegate;

@end

Sample.m

[self.delegate sampleDelegateMethod];

Swift

sample.swift

protocol SampleDelegate: class {
    func sampleDelegateMethod()
}

class Sample: NSObject {
    weak var delegate: SampleDelegate? = nil

    func callDelegateMethod() {
        delegate?.sampleDelegateMethod()
    }
}

最近のプロジェクトではSwiftを使ってるのですが、Swift好きになりますね。
まだ重かったりするみたいですけど、早くメインになってほしいかもしれないです。
Swiftの"?"とか"!"についてまだよくわかっていないので調べないとです...

【iOS8】PhotoKit

Photos Framework

iOS8から追加されたAssets Library Frameworkの代わりのフレームワーク
イメージピッカーを作る際に使用したのでまとめてみます。

アセットの編集など機能はたくさんあるようですが、今回はデータの取得と表示だけです。

主に使うクラス

・PHAsset
・PHAssetCollection
・PHCollection
・PHFetchResult
・PHImageManager

流れ

  1. カメラロール、アルバムの取得
  2. アルバム内の画像を表示
  3. 選択した画像のPHAssetを返す

これぐらいでイメージピッカーは作れるかな、と。

1. カメラロール、アルバムの取得

iOS7ではALAssetsLibraryを使って取得してたと思います。

  • enumerateGroupsWithTypes:usingBlock:failureBlock:

このメソッド使って取得してましたねー。
非同期なのが個人的には好きじゃなかったです...
Photos Frameworkではこんな感じでやりました。

NSMutableArray *results = @[].mutableCopy;

// Camera Roll
PHFetchOptions *options = [PHFetchOptions new];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
PHFetchResult *cameraRoll = [PHAsset fetchAssetsWithOptions:options];

[results addObject:cameraRoll];

// Other Albums
PHFetchResult *albums = [PHCollection fetchTopLevelUserCollectionsWithOptions:nil];
for (PHCollection *collection in albums) {
    PHAssetCollection *assetCollection = (PHAssetCollection *)collection;
    PHFetchResult *result = [PHAsset fetchAssetsInAssetCollection:assetCollection options:nil];
    [results addObject:result];
}

テーブルビューに表示したかったので配列に入れておきました。
カメラロールとアルバムを別々に取得しないといけないっぽいのが面倒です...
まとめて取ってくる方法がないかなー、と探しています。

アルバムの一覧を表示したいので、サムネイルとタイトルが必要。
-サムネイル取得

__block UIImage *image = nil;
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
// 同期処理にする場合にはYES (デフォルトはNO)
options.synchronous = YES;
[[PHImageManager defaultManager] requestImageForAsset:result.lastObject
                                           targetSize:CGSizeMake(THUMBNAIL_SIZE, THUMBNAIL_SIZE)
                                          contentMode:PHImageContentModeAspectFill
                                              options:options
                                        resultHandler:^(UIImage *result, NSDictionary *info) {
                                            image = result;
                                        }
 ];

-タイトル取得

// PHCollectionから取得
NSString *albumName = collection.localizedTitle;

この辺のを使って表示までできると思います。

2. アルバム内の画像を表示

-PHAssetの取得

NSMutableArray *assets = @[].mutableCopy
for (PHAsset *asset in result) {
    [assets addObject:asset];
}

-サムネイル表示

__block UIImage *image = nil;
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
// 同期処理にする場合にはYES (デフォルトはNO)
options.synchronous = YES;
[[PHImageManager defaultManager] requestImageForAsset:asset
                                           targetSize:CGSizeMake(THUMBNAIL_SIZE, THUMBNAIL_SIZE)
                                          contentMode:PHImageContentModeAspectFill
                                              options:options
                                        resultHandler:^(UIImage *result, NSDictionary *info) {
                                            image = result;
                                        }
 ];

3. 選択した画像のPHAssetを返す

3と書きましたが、UICollectionViewで選択されたPHAssetを返してあげれば終わりですね。

iOS7/8に対応したイメージピッカーを作っていたのでごちゃごちゃしていましたが、iOS8だけで見ればそんなに難しいことはやってないです。次回からは全体像がつかめるようにサンプルとか載せれるようにしたいです。

【iOS8】ドキュメントプロバイダ 実装

実装編

  1. Host appへUIDocumentMenuViewControllerを追加
  2. Containing appからDocument Providerにデータを渡す
  3. DocumentPickerViewControllerの編集

1. Host appへUIDocumentMenuViewControllerを追加

- デリゲート設定
<UIDocumentMenuDelegate, UIDocumentPickerDelegate>
- UIDocumentMenuViewController追加
// UIDocumentMenuViewControllerをImportモードで表示
- (IBAction)showImportMenu:(id)sender {
    NSArray *utis = @[(NSString *)kUTTypeRTF,(NSString *)kUTTypePNG,(NSString *)kUTTypeText,
                      (NSString *)kUTTypePlainText,(NSString *)kUTTypePDF, (NSString *)kUTTypeImage];
    UIDocumentMenuViewController *menu = [[UIDocumentMenuViewController alloc]
                                        initWithDocumentTypes:utis inMode:UIDocumentPickerModeImport];
    menu.delegate = self;
    [self presentViewController:menu animated:YES completion:nil];
}

// UIDocumentMenuViewControllerをExportモードで表示
- (IBAction)showExportMenu:(id)sender {
    NSURL *path = [[NSBundle mainBundle] URLForResource:@"2" withExtension:@"jpeg"];
    UIDocumentMenuViewController *menu = [[UIDocumentMenuViewController alloc]
                                          initWithURL:path inMode:UIDocumentPickerModeExportToService];
    menu.delegate = self;
    [self presentViewController:menu animated:YES completion:nil];
}

// メニューの中から選択されたUIDocumentPickerViewControllerを表示
- (void)documentMenu:(UIDocumentMenuViewController *)documentMenu didPickDocumentPicker:(UIDocumentPickerViewController *)documentPicker{
    documentPicker.delegate = self;
    [self presentViewController:documentPicker animated:YES completion:nil];
}

- (void)documentMenuWasCancelled:(UIDocumentMenuViewController *)documentMenu{
    NSLog(@"cancel");
}
- UIDocumentPickerViewControllerのデリゲートメソッド
// UIDocumentPickerViewControllerで選択したデータのURLが返ってくる
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url{
    NSLog(@"%@",url);
}

// 失敗した場合か、キャンセルされた時に呼ばれる
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller{
    NSLog(@"cancel");
}

2. Containing appからDocument Providerにデータを渡す

  • Document ProviderはContaining appとは異なるサンドボックスを持っているため、Document ProviderからContaining app内のデータへアクセスはできない。そのため、あらかじめデータを渡しておかなければならない。
- NSUserDefaultsでデータを渡す場合
NSUserDefaults *df = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.ContainingApp"];
[df setObject:@"sample" forKey:@"hello"];
- NSFileManagerからデータを渡す場合
NSFileManager *fm = [NSFileManager defaultManager];
NSURL *url = [fm containerURLForSecurityApplicationGroupIdentifier:@"group.com.ContainingApp"];
NSData *imgData = [[NSData alloc] initWithData:UIImagePNGRepresentation([UIImage imageNamed:@"1.jpg"])];
NSString *path = [[url path] stringByAppendingPathComponent:@"1.jpg"];
if (![fm fileExistsAtPath:path]) {
    BOOL success = [fm createFileAtPath:path contents:imgData attributes:nil];
    if (!success) {
        NSLog(@"error");
    }
}

3. DocumentPickerViewControllerの編集

- UIDocumentPickerMode毎のUIの設定など
- (void)prepareForPresentationInMode:(UIDocumentPickerMode)mode {
    if (mode == UIDocumentPickerModeImport) {
        self.exportButton.alpha = 0.0;
    } else if (mode == UIDocumentPickerModeExportToService) {
        self.importButton.alpha = 0.0;
    }
}
- Host appにURLを返すためのメソッド
// UIDocumentPickerModeImportの場合
- (IBAction)import:(id)sender {
    NSFileManager *fm = [NSFileManager defaultManager];
    NSURL *documentURL = [fm containerURLForSecurityApplicationGroupIdentifier:@"group.com.ContainingApp"];
    NSString *path = [[documentURL path] stringByAppendingPathComponent:@"1.jpg"];
    [self dismissGrantingAccessToURL:[NSURL fileURLWithPath:path]];
}

// UIDocumentPickerModeExportToServiceの場合
- (IBAction)export:(id)sender {
    [self dismissGrantingAccessToURL:self.originalURL];
}

4. HostAppで渡されたデータを確認する

・HostAppのNSTemporaryDirectoryにデータが保存されている
・保存先は指定されているので、指定されたディレクトリへのアクセスが必要

// HostApp内で行い、allFileNamesに保存されたデータ名が入っている
NSFileManager *fm = [NSFileManager defaultManager];
NSString *tmp = NSTemporaryDirectory();
NSString *path = [tmp stringByAppendingPathComponent:@"DocumentPickerIncoming"];
NSArray *allFileNames = [fm contentsOfDirectoryAtPath:path error:nil];

余裕があったのでexportについても少しですが調べられました。 設定は同じなので、導入編で確認してください。

【iOS8】ドキュメントプロバイダ 導入

導入編

iOS8から追加されたextensionの一つ、ドキュメントプロバイダについて調べる機会があったので忘れないように載せておきます。

【ドキュメントプロバイダの主な機能】
  • import
  • export
  • open
  • move

今回やりたかったことは、「アプリAからアプリBに写真選択のリクエストを送る、アプリBからアプリAに選択された写真データを渡す」という感じ。
これを import でやってみました。

アプリへの導入

いろいろと設定するものがあるので、長くなりますが載せてみます。
import、export、open、move、どれでも同じように設定するようです。
プロジェクトが二つ必要になるので、HostApp、ContainingAppを用意しておく。

  1. DocumentProviderの追加(Containing app)
  2. App Groupsの設定(Containing app)
  3. iCloudの設定(Host app)

1. DocumentProviderの追加(Containing app)

<ターゲットの追加>
・【File】→【New】→【Target】→【DocumentProvider】を選択

f:id:DOORS:20141114161847p:plain

【Include a File Provider extension】:open, moveを使用する際チェック

・新たにターゲットとDocumentPickerViewControllerクラスなどが追加される

2. App Groupsの設定(Containing app)

・【TARGETS】の【Capabilities】を選択

f:id:DOORS:20141117222744p:plain

使用するApp Groupsにチェックを入れる
ContainingApp,MyDocumentProvider共に設定が必要
※ App Groupsの管理はiOS Dev CenterのCertificates, Identifires内で可能

3. iCloudの設定(Host app)

・【TARGETS】の【Capabilities】を選択

f:id:DOORS:20141114162506p:plain

iCloudの設定をONに
iCloud Documentsにチェック

コードを書き始める前に必要な設定はこのぐらいかと思います。
また時間のある時に実装の方法も載せておきたいです。