SwiftでQuickDialogをpushViewControllerで使う(メモ)


QuickDialgはDailogと名付けられているのでモーダルでしか使えないのか?
UINavigationControllerでpushViewControllerしたい。

また、この時、なんとかQuickDialgViewControllerをサブクラス化して、その中でQRootElementを作れないかも試行錯誤してみたが、こっちはちょっと無理っぽい。

単純にQuickDialgのサンプルを参考に以下のようにした。
1.QRootElementはクラス・メソッドで別途作る。
2.上記をrootとして渡してQuickDialogControllerのインスタンスを作り、これをpushViewControllerする。

1.QRootElementはクラス・メソッドで別途作る

QuickDialgのサンプルにならって何たらBuilderのcreateメソッドという名前にする。
Swiftではクラス・メソッドは class func create() と書けばよいらしい。
ExampleQRootBuilder.swift

class ExampleQRootBuilder: NSObject {
    class func create() -> QRootElement {
        let root = QRootElement()
        root.title = "Hello"
        root.grouped = true
        let section = QSection(title: "Example")
        let label = QLabelElement(title: "Hello", value: "Hello!")
        root.addSection(section)
        section.addElement(label)
        return root
    }
}

2.QuickDialogControllerとpushViewController

let root = ExampleQRootBuilder.create()
let qdc = QuickDialogController(root: root)
self.navigationController.pushViewController(qdc, animated: true)

QuickDialogControllerはサブクラスにして戻るボタンなどカスタマイズしてもよい。

以上

SwiftでStoryBoardとJASidePanelsとCoreDataを使う(メモ)


SwiftでStoryBoardを使って、JASidePanels(Objective-C)とCoreDataを使うメモ。
JASidePanelsはcocoapods経由で組み込んだ。cocoapodsの説明は割愛。
JASidePanelsのcenterPanelはUINavigationContorollerにして、そこでCoreDataを使うようにコーディングする。

StoryBoardでSceneを作成

以下のようにした。
1.JASidePanelsの土台になるUIViewControllerをSBに追加 → ラベル: MainPanel
2.leftPanelになるUITableViewContorollerをSBに追加 → ラベル: SidePanelLeft
3.rightPanelになるUITableViewContorollerをSBに追加 → ラベル: SidePanelRight
4.centerPanelになるUINavigationContorollerをSBに追加 → ラベル: Master
5.そのNavigationContorollerのtopViewControllerになるUITableViewControllerをSBに追加
6.個々のデータの編集用にUIViewContorollerをSBに追加
fig01

 

各Sceneに「Class」と「Storyboard ID」を設定

fig021.MainPanel Scene
  Class: MainPanelViewController
2.SidePanelLeft Scene
  Class: (UITableViewContoroller)
  Storyboard ID: leftPanel
3.SidePanelRight Scene
  Class: (UITableViewContoroller)
  Storyboard ID: rightPanel
4.Master Scene
  Class: (UINavicationController)
  Storyboard ID: centerPanel
5.Master Scene – topViewController
  Class: DataListViewController
  ※topViewController設定→SB上でcontrolキーを押しながらMaster SceneからこのSceneへドラッグ

Is Initial View Controller設定
MainPanel SceneはInitial View Controllerとなるので、Attribute Inspectorパネルの「Is Initial View Controller 」にチェックを入れる。

Bridging-Header.h

Objective-C で書かれた JASidePanels を使うには xxxxx-Bridging-Header.h が必要
ここでは MyApplicaton-Bridging-Header.h に下記コードを追加

#import <JASidePanels/JASidePanelController.h>
#import <JASidePanels/UIViewController+JASidePanel.h>

AppDelegateの記述

AppDelegateのdidFinishLaunchingWithOptions、本来は何も書かなくてもJASidePanelsは表示されるが、今回はCoreDataを使うので、centerPanelであるUINavigationControllerのtopViewControllerにself.managedObjectContext を渡すための処理を書く。

    func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
        let mainPanel = self.window!.rootViewController as MainPanelViewController
        let navigationController = mainPanel.centerPanel as UINavigationController
        let controller = navigationController.topViewController as DataListViewController
        controller.managedObjectContext = self.managedObjectContext
        return true
    }

MainPanelViewController.swift

1.MainPanelViewControllerはJASidePanelControllerのサブクラス。
2.awakeFromNibの中身は swift風には下記でよいようだ。

class MainPanelViewController: JASidePanelController {
    override func awakeFromNib() {
        self.leftPanel = self.storyboard?.instantiateViewControllerWithIdentifier("leftPanel") as UIViewController
        self.rightPanel = self.storyboard?.instantiateViewControllerWithIdentifier("rightPanel") as UIViewController
        self.centerPanel = self.storyboard?.instantiateViewControllerWithIdentifier("centerPanel") as UIViewController
    }

SwiftでもStoryBoard経由でObjective-c版のJASidePanelsをCoreDataとともに使えることが分かった。メモはここまで。

Rails4: has_manyの保存自動化とタブでの表示分け— accepts_nested_attributes_for, Strong Parameters, Bootstrap


has_manyしている項目を fields_for で記述して、これをBootstrapのタブで分けて表示して、さらにその保存はassociation を介して自動化する(コントローラでhas_many項目の保存を意識しないでよいようにする)ということについてまとめて書きます。

今回のポイントは、 accepts_nested_attributes_for と、そのための strong parameters の書き方、fields_for を使ってタブ表示するためにobjectの値を取得する方法です。


1.前提:モデルの構造

人とその住所のアソシエーションを例とします。

personモデル

※今回の説明には以下は必要ないですが、とりあえず以下のようにしたとします。

person_name string 氏名
gender string 性別
birth_day string 誕生日
addressモデル
location string 場所の種類 「自宅」「会社」「出向先」など
zip string 郵便番号
prefecture string 都道府県
street text 住所
person_id integer belong_to する person

2.モデルの記述

person.rb

accepts_nested_attributes_for を使いますね。

  has_many :addresses, :dependent => :destroy
  accepts_nested_attributes_for :addresses

address.rb

  belong_to :person

3.コントローラの記述

person_controller.rb

# update は @person.update(person_params) だけでよい
def update
  if !@person.update(person_params)
    .... something to process ....
  end
  render ....
end

ポイントは person_params で使う Strong Parameter の記述です。
:addresses_attributes を使うのですね。

def person_params
  params.require(:person).permit( :id, :person_name, :gender, :birth_date, 
    :addresses_attributes => [:id, :location, :zip, :prefecture, :street])
end

4.ビューの記述

has_many を表示するのに fields_for を使いますね。
fields_for 自体が存在するaddressの数分ループもしてくれるのですごく便利です。

<%= form_for :person do |f| %>
  ..... person 用の要素の記述 .....
  <%= f.fields_for :addresses do |fa|%>
    <div class="form-group">
      <%= fa.text_field :place, :placeholder => "PLACE NAME", :class => "form-control" %>
      <%= fa.text_field :zip, :placeholder => "ZIP", :class => "form-control" %>
      <%= fa.select :prefecture, PREFECTURE_LIST,{:include_blank => true}, :class => "form-control" %>
      <%= fa.text_area :street, :row => 2, :placeholder => "STREET",  :class => "form-control" %>
    </div>
  <% end %>

5.ビューをタブ表示にする

上の形だと、住所が全てダラダラと表示されるのでBootstrapのタブを付け加えてみます。
タブのラベルには address.location を、 タブのidには address.id を使います。

form.html.erb—タブラベル部分

 ※最初のタブには class=”active” が必要

<% address_first_id = @pesson.addresses.first.id %>
<div class="nav nav-tabs">
  <ul class="nav nav-tabs">
    <% addresses.each do |address|%>
      <li<%= ' class="active"' if address.id == address_first_id %>><a href="#address_<%= address.id.to_s %>" data-toggle="tab"><%= address.location %></a></li>
    <% end %>
  </ul>
</div>

form.html.erb—タブの中身部分

 ※ここでのポイントは fa.object.id というようにobject記述でデータの内容を読み出せること。

<div id="addressesTabContent" class="tab-content">
  <%= f.fields_for :addresses do |fa|%>
    <div class="tab-pane <%= "active" if fa.object.id == address_first_id %> in" id="address_<%= fa.object.id.to_s %>">
      <div class="form-group">
        <!-- ここには address.location はいらない。ラベルに表示しているので -->
        <%= fa.text_field :zip, :placeholder => "ZIP", :class => "form-control" %>
        <%= fa.select :prefecture, PREFECTURE_LIST,{:include_blank => true}, :class => "form-control" %>
        <%= fa.text_area :street, :row => 2, :placeholder => "STREET",  :class => "form-control" %>
      </div>
    </div> <!-- /tab-pane -->
</div> <!-- /addressesTabContent -->

以上です。概念説明のメモとして簡略化してますので、個々に解決が必要な細かい問題はあるかもしれません。

補足:fields_forのchild_indexを使う方法

上記の例は address.id をタグのidとしていたが、単純に頭から0,1,2…と番号を振って、fields_for では、その child_index を使うという方法もある。fields_for の index は fa.options[:child_index] で取得できる。


参考にさせていただいたページ

Rails: send_data でダウンロードした後に表示更新する方法


データベースの集計を CSVファイルでダウンロードする処理が必要になった。

ファイルダウンロードには send_data を使う。

今回は、非同期処理するつもりはなかったが、集計の処理には10秒くらいかかるので、ボタンをクリックして10秒くらい待ってからダウンロードが始まるので、待ち時間に「しばらくお待ちください」を表示したい。

処理開始時にこれを表示して、ダウンロードが終わったらそれを消すという動きにしたかった。

ボタンをクリック→以下のJavaScript

 $("#message").html("しばらくお待ちください");
 window.location.href = "<%= totaling_path %>"; //←集計してsend_dataするアクションのURL


そして「集計してsend_dataするアクション」の最後にメッセージを消すためのレンダリングして完了!

ところが、上記はうまく行かない。

send_data はそれ自体がレンダリングをしているので、send_dataの後にレンダリングすると Double render のエラーになるからだ。どうする?

ここに解決のヒントがあった。↓
How to trigger download with Rails send_data from AJAX post

理屈としては、処理に時間がかかっているのだから、処理とsend_dataをアクション分けして、処理が終わってから send_data のある別アクションを起動すればいいということ。

この場合に、send_data のあるアクションの起動を partial を使って、javascriptをレンダリングする形で行うというのがミソのようだ。いろんな理由から send_dataのあるアクションは javascriptの window.location を使いたいということもあり、これが都合いい。

模式的に書くと以下のような構成
↓コントローラ

# GET /totaling
def totaling
  CSV.open tempfile_path do |csv| .... などで一時ファイルとして生成
   ....時間のかかる集計処理...
   csv << data_array
  end
  @download_url = download_path(:tempfile_path => tempfile_path, :file_name => "result.csv") # ← download_path は URL /download のヘルパー関数 tempfile_pathやfile_nameは?パラメータになる
  render :partial => "downloadfile"
end

# GET /download
def download
    File.open(params[:tempfile_path], 'r') do |f|
        send_data f.read, :type => 'text/csv',:disposition => 'attachment',:filename => params[:file_name]
    end
    File.delete(params[:tempfile_path]) if File.exist?(params[:tempfile_path])
end

↓ _downloadfile.js.erb (パーシャルファイル)

  window.location.href = "<%= @download_url %>";
  $("#message").html(""); // ←ここで「しばらくおまちください」のメッセージを消す

上記で send_data の代わりに send_file を使うことはしなかった。send_file を使うと、その次の行のFile.deleteでの一時ファイルの削除が先に行われて都合がわるいことがあるからだ。上記の方法なら、記述の順番通りになる。

またApacheの場合、send_fileに必要なX-sendfile を人によってはインストールできない場合もあるだろう。

以上です。何かあればコメントをください。

JQuery: $(‘tr[data-link]’).click で行クリックできるようにした場合に、その行内の別リンクを有効にする方法


テーブルで tr にリンク先のURLをdataで入れておいて、行のどこでもクリックしたら、そのリンクへジャンプするというのはよくやる。例えば以下

<tr data-link="http://mysite.com/jobs">
  <td>Steve Jobs</td>
  <td>Apple Inc.</td>
  <td>U.S.A</td>
  <td><a href="http://wiki.org/jobs">Wiki</a></td>
</tr>
$('tr[data-link]').click(function() {
   window.location = this.dataset.link;
});

しかしこのままだと、先にtr行が反応してしまいtr行内にある別のリンクがクリックできない。上の例だと行内のWikiへのリンクがクリックできないことになる。

以下のようにしました。

...略...
  <td><a href="http://wiki.org/jobs" class="btn">Wiki</a></td>
...略...
$('tr[data-link]').click(function(e) {
  if (!$(e.target).closest('a').is('.btn')) {
    window.location = this.dataset.link;
  }
});

class は is(‘.クラス名’) で判定できるのでリンクの方に適当なクラス名を付けて処理から除外する。

もっと簡単な方法があるのだろうか?

Rails4: ActiveRecord カラム演算してその合計を出す—例えば数量×単価の合計


テーブルのカラムをかけ算してその合計を出す。例えば検索されたデータ行の数量×単価の合計を出したい。

結論を言えば下記。
Eitem というモデルに数量(quantity)と単価(price)がある。これらは同じestimate_idという見積書データのidを持っているものとする。

    eitems = Eitem.where(:estimate_id => estimate_id).select("sum(eitems.quantity * eitems.price) as total, eitems.estimate_id").group(:estimate_id).order(nil)
    total = eitems.first.total

余談だが、order(nil)しておかないと eitems.id で order by してしまってエラーになる。

ActiveRecordネイティブぽい記述(sum()とか)でできると思ってググったり、modelにメソッドを追加したり、半日試行錯誤したが、結局 selectを使う方法しか考えられなかった。

もっとActiveRecordっぽいいい方法があるのだろうか。

参考:Rails 3.1 – find using count and select as

Rails4: includesで除外をする


Rails4 です。
Personモデルが複数の Emailモデルを持っているという has_many なアソシエーションで、ある特定の email を持っているいる人を除外したい。
とりあえず、除外でなく、持っている人を検索するなら……

@people_with_email = Person.all.includes(:emails).where("emails.email='xxx@xxx.com' or emails.email='yyy@yyy.com').references(:emails)

で良いようだ。
では、除外するには……

@people_except_email = Person.all.includes(:emails).where("emails.email != 'xxx@xxx.com' or emails.email != 'yyy@yyy.com'").references(:emails)

でよいか……どうも良くないみたいだ。
試行錯誤の結果以下のようにした。

ids = Person.connection.select_rows(Person.joins(:emails).where("emails.email = 'xxx@xxx.com' or emails.email = 'yyy@yyy.com'").select("people.id").to_sql)
@people_except_email = Person.all.includes(:emails).where.not(:id => ids).references(:emails)

参考にさせていただいたサイトは以下です。本当に助かりました。ありがとうございます。