Passion make things more better

Ruby on Rails / React.js / Swift / AWS / Docker

Railsでseedデータを分割して実行できるようにする

個人的にRailsのseed運用は以外と悩みがちです。seed-fu使ったりと色々やりましたが、これから説明する方法に落ち着きました。

  • ファイルを指定してseedを実行できるようにrake taskを作成する
  • db/seeds以下のディレクトリを作成し、以下にxxxx.rbといった実行したい処理を書いたファイルを用意
  • bundle exec rake db:seed:xxxx(作成したファイル名)で実行

rake taskの作成

コマンドでファイルを指定して実行できるようにrake taskを作成します。 ※参考に記載したサイトのコードをお借りしております。

# lib/tasks/seed.rakeとして以下を作成する

Dir.glob(File.join(Rails.root, 'db', 'seeds', '*.rb')).each do |file|
  desc "Load the seed data from db/seeds/#{File.basename(file)}."
  task "db:seed:#{File.basename(file).gsub(/\..+$/, '')}" => :environment do
    load(file)
  end
end

seedファイルの用意

db/seedsというディレクトリを作成し、以下に行いたい処理を記載したファイルを作成します。 今回は例でCategory(nameカラムを持つ)モデルに対してseedデータを挿入するファイルを作成します。

# db/seeds/category.rb

Category.create(name: 'サンプル')

seedファイルの実行

db/seeds/category.rbを以下のコマンドにて実行します。

~ bundle exec rake  db:seed:category

これでseedデータが挿入できたかと思います。 個人的にはこのやり方が一番しっくりきているので、しばらくはこちらで続けたいと思います。

参考

Code Sign error: No code signing identities found: No valid signing identities (i.e. certificate and private key pair) matching the team ID "xxx" were found. の解決方法

チームで開発をしていたとき通常のbuildは通ったのに、archiveをした際にエラーが起きてしまいました。 エラーメッセージを見ると以下が表示されていました。

Code Sign error: No code signing identities found: No valid signing identities (i.e. certificate and private key pair) matching the team ID "xxx" were found.

原因

本来とは違うteam IDでbuildしたため。

解決方法

エラーメーセージに表示されているteam IDで検索をかけて、ひかかったところを元のteam IDに書き換える。 ※私の場合は通常のProjectの方は大丈夫でしたが、Podの方のteam IDが書き換わっていました。

carrierwaveの設定ファイル

carrierwaveを使う時にいつも設定ファイルを調べているので、備忘録としてよく使うベースの物を。

  • ファイル名をuniqueにしたい
  • original及びthumbのversionで、リサイズを行う
    • resize_to_fitとresize_to_limitどちらもありますが、今回はresize_to_fitを使用
# encoding: utf-8

class ImageUploader < CarrierWave::Uploader::Base

  # Include RMagick or MiniMagick support:
  include CarrierWave::RMagick
  # include CarrierWave::MiniMagick

  # Choose what kind of storage to use for this uploader:
  storage :file
  # storage :fog

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Provide a default URL as a default if there hasn't been a file uploaded:
  # def default_url
  #   # For Rails 3.1+ asset pipeline compatibility:
  #   # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
  #
  #   "/images/fallback/" + [version_name, "default.png"].compact.join('_')
  # end

  # Process files as they are uploaded:
  # process :scale => [200, 300]
  #
  # def scale(width, height)
  #   # do something
  # end

  process :resize_to_fit => [640, 640]
  # Create different versions of your uploaded files:
  version :thumb do
   process :resize_to_fit => [160, 160]
  end

  # Add a white list of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  def extension_white_list
    %w(jpg jpeg)
  end

  # Override the filename of the uploaded files:
  # Avoid using model.id or version_name here, see uploader/store.rb for details.
  def filename
    "#{secure_token}.#{file.extension}" if original_filename.present?
  end

  protected
  def secure_token
    var = :"@#{mounted_as}_secure_token"
    model.instance_variable_get(var) or model.instance_variable_set(var, SecureRandom.uuid)
  end

end

参考

SwiftでJSQMessagesViewControllerを使ってチャットUIを実装する

昨今、多くのアプリでチャットUIが見慣れてきたかと思います。以外と見慣れているUIだけど一から自分で実装するのは以外と面倒ですよね....そこで今回はJSQMessagesViewControllerというライブラリを使ってチャットに必要な最低限の機能を実装する方法を説明します。

仕様について

JSQMessagesViewControllerには多くの機能が備わっているのですが、今回は以下の機能を実装していきます(とりあえずこれがあれば成り立つだろうという要素)。

  • 1対1でユーザー同士がやりとりをできる
  • テキストが送信できる
  • 画像が送信できる
    • 画像タップが検知できる
  • 送信時刻を良い感じに表示する(ある一定の間隔が空いたら表示するなど)
  • チャット相手のユーザーのアイコンが表示される
    • 相手のユーザーのアイコンタップを検知することができる

初期導入方法

インストール

JSQMessagesViewControllerをCocoaPodsを使ってインストールします。

// Podfile
pod 'JSQMessagesViewController'

~ pod install

はじめの設定

以下のファイルを作成してください。するとチャット画面ができ、最低限の実装ができているかと思います。 処理としては、メッセージを送ると自動で返信をするというものです。

import UIKit
import SwiftyJSON
import JSQMessagesViewController

class MessagesViewController: JSQMessagesViewController {
    
    private var messages: [JSQMessage] = []
    private var incomingBubble: JSQMessagesBubbleImage!
    private var outgoingBubble: JSQMessagesBubbleImage!
    private var incomingAvatar: JSQMessagesAvatarImage!
    // テスト用
    private let targetUser: JSON = ["senderId": "targetUser", "displayName": "passion"]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        initialSettings()
    }
    
    private func initialSettings() {
        // 自分の情報入力
        self.senderId = "self"
        self.senderDisplayName = "自分の名前"
        // 吹き出しの色設定
        let bubbleFactory = JSQMessagesBubbleImageFactory()
        self.incomingBubble = bubbleFactory.incomingMessagesBubbleImageWithColor(UIColor.jsq_messageBubbleLightGrayColor())
        self.outgoingBubble = bubbleFactory.outgoingMessagesBubbleImageWithColor(UIColor.jsq_messageBubbleGreenColor())
        
        // 相手の画像設定
        self.incomingAvatar = JSQMessagesAvatarImageFactory.avatarImageWithImage(UIImage(named: "sample_user")!, diameter: 64)
    // 自分の画像を表示しない
        self.collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSizeZero
    }
    
    // 送信ボタンを押した時の挙動
    override func didPressSendButton(button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: NSDate!) {
        let message = JSQMessage(senderId: senderId, displayName: senderDisplayName, text: text)
        messages.append(message)
        // 更新
        finishSendingMessageAnimated(true)
        
        sendAutoMessage()
    }
    
    // 表示するメッセージの内容
    override func collectionView(collectionView: JSQMessagesCollectionView!, messageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageData! {
        return self.messages[indexPath.item]
    }
    
    // 表示するメッセージの背景を指定
    override func collectionView(collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageBubbleImageDataSource! {
        if messages[indexPath.item].senderId == senderId {
            return self.outgoingBubble
        }
        return self.incomingBubble
    }
    
    // 表示するユーザーアイコンを指定。nilを指定すると画像がでない
    override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
        if messages[indexPath.item].senderId != self.senderId {
            return incomingAvatar
        }
        return nil
    }
    
    // メッセージの件数を指定
    override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return messages.count
    }
    
    // テストでメッセージを送信するためのメソッド
    private func sendAutoMessage() {
        let message = JSQMessage(senderId: targetUser["senderId"].string, displayName: targetUser["displayName"].string, text: "返信するぞ")
        messages.append(message)
        finishReceivingMessageAnimated(true)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

機能実装

ここからはこの基本的なチャットを拡張していきます。

メッセージの上に時刻を表示

よくLINEなどで見かけるメッセージの上に送信時刻を表示する機能を実装していきます。 以下のリンクに書いてある内容が非常に良いので、今回はそちらを使わせて頂きました。Objective-Cで書かれていたのでSwift用に書き換えています。

Change a timestamp logic in JSQMessagesViewController

// 送信時刻を出すために高さを調整する
    override func collectionView(collectionView: JSQMessagesCollectionView!, attributedTextForCellTopLabelAtIndexPath indexPath: NSIndexPath!) -> NSAttributedString! {
        let message = messages[indexPath.item]
        if indexPath.item == 0 {
            return JSQMessagesTimestampFormatter.sharedFormatter().attributedTimestampForDate(message.date)
        }
        if indexPath.item - 1 > 0 {
            let previousMessage = messages[indexPath.item - 1]
            if message.date.timeIntervalSinceDate(previousMessage.date) / 60 > 1 {
                return JSQMessagesTimestampFormatter.sharedFormatter().attributedTimestampForDate(message.date)
            }
        }
        return nil
    }
    
    // 送信時刻を出すために高さを調整する
    override func collectionView(collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForCellTopLabelAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
        if indexPath.item == 0 {
            return kJSQMessagesCollectionViewCellLabelHeightDefault
        }
        if indexPath.item - 1 > 0 {
            let previousMessage = messages[indexPath.item - 1]
            let message = messages[indexPath.item]
            if message.date .timeIntervalSinceDate(previousMessage.date) / 60 > 1 {
                return kJSQMessagesCollectionViewCellLabelHeightDefault
            }
        }
        return 0.0
    }

これでメッセージを送信してみてください。上に時刻が表示されていることでしょう。

ユーザーアイコンのタッチイベントを検知する

ユーザーのアイコンタップのイベントをハンドルするメソッドはデフォルトで備わっていないので、collectionViewのメソッドをオーバーライドして検知する方法をとります。

公式FAQを見るとcellのカスタマイズ方法が書いてあるので、それに習っていきます。

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = super.collectionView(collectionView, cellForItemAtIndexPath: indexPath) as! JSQMessagesCollectionViewCell
        
        // ユーザーアイコンに対してジェスチャーをつける
        let avatarImageTap = UITapGestureRecognizer(target: self, action: "tappedAvatar")
        cell.avatarImageView?.userInteractionEnabled = true
        cell.avatarImageView?.addGestureRecognizer(avatarImageTap)

    // 文字色を変える
        if messages[indexPath.item].senderId != senderId {
            cell.textView?.textColor = UIColor.darkGrayColor()
        } else {
            cell.textView?.textColor = UIColor.whiteColor()
        }
        
        return cell
    }
    
    func tappedAvatar() {
        print("tapped user avatar")
    }

これでtappedAvatar()メソッドでアイコンタッチが拾えるようになっているかと思います。

画像を投稿する

画像を投稿する仕組みを実装します。キーボードの上にクリップのアイコンがあるかと思います。あのアイコンをタップすることで投稿する画像を選べるようにします。

クリップのタッチイベントはdidPressAccessoryButton(sender: UIButton!)で受け取ることができるので、処理をこの中に書いていきます。 今回はUIImagePickerControllerを使用するので、こちらをみて、selectImage()メソッドが呼べる状態にしておいてください。

override func didPressAccessoryButton(sender: UIButton!) {
        selectImage()
    }
private func selectImage() {
        let alertController = UIAlertController(title: "画像を選択", message: nil, preferredStyle: .ActionSheet)
        let cameraAction = UIAlertAction(title: "カメラを起動", style: .Default) { (UIAlertAction) -> Void in
            self.selectFromCamera()
        }
        let libraryAction = UIAlertAction(title: "カメラロールから選択", style: .Default) { (UIAlertAction) -> Void in
            self.selectFromLibrary()
        }
        let cancelAction = UIAlertAction(title: "キャンセル", style: .Cancel) { (UIAlertAction) -> Void in
            self.dismissViewControllerAnimated(true, completion: nil)
        }
        alertController.addAction(cameraAction)
        alertController.addAction(libraryAction)
        alertController.addAction(cancelAction)
    }
    
    private func selectFromCamera() {
        if UIImagePickerController.isSourceTypeAvailable(.Camera) {
            let imagePickerController = UIImagePickerController()
            imagePickerController.delegate = self
            imagePickerController.sourceType = UIImagePickerControllerSourceType.Camera
            imagePickerController.allowsEditing = true
            self.presentViewController(imagePickerController, animated: true, completion: nil)
        } else {
            print("カメラ許可をしていない時の処理")
        }
    }
    
    private func selectFromLibrary() {
        if UIImagePickerController.isSourceTypeAvailable(.PhotoLibrary) {
            let imagePickerController = UIImagePickerController()
            imagePickerController.delegate = self
            imagePickerController.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
            imagePickerController.allowsEditing = true
            self.presentViewController(imagePickerController, animated: true, completion: nil)
        } else {
            print("カメラロール許可をしていない時の処理")
        }
    }
    
    func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
        if let image = info[UIImagePickerControllerEditedImage] {
            sendImageMessage(image)
        }
        picker.dismissViewControllerAnimated(true, completion: nil)
    }
private func sendImageMessage(image: UIImage) {
        let photoItem = JSQPhotoMediaItem(image: image)
        let imageMessage = JSQMessage(senderId: senderId, displayName: senderDisplayName, media: photoItem)
        messages.append(imageMessage)
        finishSendingMessageAnimated(true)
    }

これで写真を投稿することができたかと思います。

メッセージで送った画像のタップイベントを受け取る

先ほど、メッセージで画像を送るところまではできているかと思います。続いてやるのは、よくチャットアプリである画像をタップして拡大を行うために、画像のタップを検知する処理を書くことです。

JSQMessagesViewControllerにはfunc collectionView(collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAtIndexPath indexPath: NSIndexPath!)というメソッドがあります。このメソッドでbubble(吹き出し)のタッチを検知し、さらに画像だった場合に別のメソッドを呼ぶという処理を書いていきます。

override func collectionView(collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAtIndexPath indexPath: NSIndexPath!) {
        if messages[indexPath.item].isMediaMessage {
            let media = messages[indexPath.item].media
            if media .isKindOfClass(JSQPhotoMediaItem) {
               print("tapped Image")
            }
        }
    }

これで画像を送信してタップをすることでprintで書いた文章が呼ばれるかと思います。

最後に

以上が、SwiftでJSQMessagesViewControllerを使用した際のチャットUIの実装方法になります。今回紹介したのはあくまで最低限の実装方法なので、興味ある方は、他にも詳しく調べて是非実装してみてください。

間違え等ありましたらコメントなりで連絡を頂けると嬉しいです。

参考

SwiftでUIImagePickerControllerを使用したテンプレ

import UIKit

class UIPickerViewSampleViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        selectImage()
    }
    
    private func selectImage() {
        let alertController = UIAlertController(title: "画像を選択", message: nil, preferredStyle: .ActionSheet)
        let cameraAction = UIAlertAction(title: "カメラを起動", style: .Default) { (UIAlertAction) -> Void in
            self.selectFromCamera()
        }
        let libraryAction = UIAlertAction(title: "カメラロールから選択", style: .Default) { (UIAlertAction) -> Void in
            self.selectFromLibrary()
        }
        let cancelAction = UIAlertAction(title: "キャンセル", style: .Cancel) { (UIAlertAction) -> Void in
            self.dismissViewControllerAnimated(true, completion: nil)
        }
        alertController.addAction(cameraAction)
        alertController.addAction(libraryAction)
        alertController.addAction(cancelAction)
        presentViewController(alertController, animated: true, completion: nil
    }
    
    private func selectFromCamera() {
        if UIImagePickerController.isSourceTypeAvailable(.Camera) {
            let imagePickerController = UIImagePickerController()
            imagePickerController.delegate = self
            imagePickerController.sourceType = UIImagePickerControllerSourceType.Camera
            imagePickerController.allowsEditing = true
            self.presentViewController(imagePickerController, animated: true, completion: nil)
        } else {
            print("カメラ許可をしていない時の処理")
        }
    }
    
    private func selectFromLibrary() {
        if UIImagePickerController.isSourceTypeAvailable(.PhotoLibrary) {
            let imagePickerController = UIImagePickerController()
            imagePickerController.delegate = self
            imagePickerController.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
            imagePickerController.allowsEditing = true
            self.presentViewController(imagePickerController, animated: true, completion: nil)
        } else {
            print("カメラロール許可をしていない時の処理")
        }
    }
    
    func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
        if let image = info[UIImagePickerControllerEditedImage] {
            print(image)
        }
        picker.dismissViewControllerAnimated(true, completion: nil)
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}