iOSアプリ(Swift)再入門雑記

業務でiOSアプリ、今どきReactNativeじゃないの?? なんて思われるわけなんですが、ともかく業務でSwiftでアプリを試しに作ってみるのを初めて1週間が立ちました。 諸先輩方これからどうぞよろしくお願いします。 なにかデカい新しいことを仕事で始めるのはRailsRubyに入門して以来約3〜4年ぶりくらいで新鮮な気持ちなので、 入門したての新鮮な気持ちを懐かしむために箇条書きで適当に感想書いておこうかなと思っています。

言語など

  • 今までのバックグラウンドが大体動的な言語がほとんどなので、型が便利だと思う半面、書いているときはちょっと面倒さを感じる。
    • Rubyに比べてコードは冗長なんだけど、最初にザクザク書いていけばIDEなのであとから補完されていくので、トータル書いてる時間がすごく増えたとかはないと思う。
    • 型推論が便利
  • 今はSwiftが持っているポテンシャルの10%くらいしか引き出せていないので、Swiftという言語にちゃんと入門する必要がある。

  • やっぱりUIKitとかのドキュメントがわかりにくくて、UITableViewControllerでいつも必要になる三種の神器(Cell個数指定、Cellの中身定義、Cell選択時のコールバック)をいつも忘れてググってQiita見て満足しちゃっている。

    • 翻訳しながらでもいいので、公式ドキュメントの方をちゃんと読んだほうがいいよな。。。という気持ちになってる。
    • しかしまぁ、情報量はRailsに負けないくらい多い。(きっとRails以上だよね。)
  • Storyboardにまだまだ慣れない感じはする。ただ、4年前にやったときよりも理解はできているので手応えは感じる。

ライブラリなどの雑感

  • CocoaPods使ってる。
  • JSのときの学びだけど、あんまりUIデザインにべったり依存する部分のライブラリは入れないようにしている。
  • QiitaでよくUIColorのhexを再実装したりしているけど、そういうことはしないでちゃんとライブラリから探してみるようにしている。

アーキテクチャ

  • 今の所ソロプレイの予定なので、アーキテクチャはどうしようか悩み中。
  • MVPかMVCか。
  • サーバーサイド開発(特にRails)と違ってレールがないので、自分でレールを作る必要がある。
  • 実際に4画面くらい書いた感想は、多分何も工夫しないで実装していくとViewControllerのコードが膨れ上がって死ぬな、これは。。。という気持ちになってる。
  • まず、アーキテクチャよりもSwift自体の気持ちになってコードを書く必要があるかも。それで適切な責務のMVCでも十分イケる可能性もある。
  • ただ、画面遷移を管理するRouterがほしいなという気持ちになってる。
    • たんにUIView -> UIViewへの移り変わりでなく、WebViewの特定のURL -> ネイティブアプリ起動 みたいなことをやりたかったりするので、ライブラリ導入前に捨てる実装前提で、簡易的なRouter書いてみようかなと思っています。
  • 画面遷移しても共有したいObject(ログイン済みユーザーの情報等)があったりするので、雑にXXXXManagerってSingleton作って使ったりしてしまったので、多分勉強が足りないと思っている。
    • せめてデータフローは1方向にしたいな…
  • アーキテクチャを選定するにはアプリケーションコードを全然書いていないので、どんどんプライベートでもアプリ作ったりコードを書いたりしないとヤバイ…と思ってる。
  • これは読んだ。いい本だと思うけど理解が浅い気がしていて、あと半月したらもう1週する peaks.cc

テスト

  • まだ数画面なので書いていない。
  • アーキテクチャをちゃんと決めてから出ないと、どうテスト書くかみたいな戦略が未熟な故作戦が立てれない。

Firebase

  • とにかくモバイル開発に必要な周辺ツールがほぼココに揃っていて便利。最高。
  • とりあえずAnalyticsとかCrashlyticsとかPerformance Monitoringとか突っ込んでおいた。

まとめ

  • ここから半月から約1ヶ月間、プライベートでも含めてコードを書きまくってインプットしていかないとちゃんと書ける気がしないんで、ちゃんと頑張っていこう。。。

iOSアプリ再入門するためにUdemyで教材を買ったらとてもお得だった件

ステマではないです。

正直二重価格表示っぽいことをしているのは承知の上だけど、それはそれで…

あと個人情報がどうとかっていう話題には触れずに行きます。

とにかくiOSに再入門するために年末にUdemyで教材を買ってみてやったらとても良かったので、そのあたりのお話です。

筆者のSpec

  • 普段はサーバーサイドなエンジニア
    • フロントエンドもしっかりSPAを設計するのは難しいけど、それ以外そつなくこなせるかなーくらい。
  • iOSは4年ぶりくらい。
    • Swiftリリース直後くらいのときに社で電卓・出金伝票アプリ作っていてリリースした実績があります。
      • 現在はメンテしてなくて、新しいiOSのバージョンに対応しなくなったので公開されてないです。
    • 当時からCoreData、AutoLayoutを始めとしたStoryboard周りに置いて色々苦手意識があったのでした。
    • Swiftの文法について
      • 特に苦手意識はなかった。ただ1.0当時の知識なので、protocolやextensionもない。

筆者の課題

苦手意識があり、再入門に当たり克服・特に重点的に理解したかったところ

  • XCodeの基本的な使い方の復習
  • AutoLayout
  • CoreData
  • ライブラリ周り(Cocoapods)
  • Realm(おまけ)
  • WebAPI通信・JSONの扱い

これらについてはちゃんとiOSで開発したことがないとわからない分野だったり、 課題にそもそもぶち当たることがないので、検索だけでなく動画カリキュラムなどでドーピングしてスピードアップする必要があるところでした。

苦手意識も無いし、あとでもいいかって思ったところ

  • Swiftの文法
  • 新しく導入されたSwiftの文法や概念などなど

プログラミング初心者ではなかったのと、 この辺はググれば出るし、それこそ業務で書いていって追ってでも学習はできるかと思ったので、あまり重視しませんでした。

お得な教材はコレ

https://www.udemy.com/ios-12-app-development-bootcamp/

上記に書いた自分が再入門に当たり欲しかったカリキュラム内容がすべて盛り込まれている。 当然Swiftの基本的な文法についてどうとか、Git云々っていうのも当然あるけど、 Firebaseを使ったチャットアプリケーションを作ったりで初心者にとってはかなりもりもりな内容になっている。

おすすめできるかどうか

もちろんおすすめできるのでブログを書いているし、こうやっていいぞということを伝えている感じ。

プログラミング初心者向け…?

初心者を意識して作っているのは理解できるんだけど、本当の意味での初心者にはハードルが高い気がするので、ドットインストールとかもうちょっと簡単目の教材で物足りなくなったりしたときにコレをやればいいかなと。ただ、もともとサーバーサイドやWebフロントエンドをやっていて、英語にすごくハードルを感じなければなおすすめできる。

ただ、プログラミング初心者はテキストよりも動画などのほうがわかりやすいということはあるので、 動画のあるカリキュラムを基本的におすすめしたいです。特にXCodeの基本的な使いかたやAutoLayoutは動画カリキュラムだからこそ理解ができるところがあるからです。

カリキュラムの総時間

真面目に全部やろうとするとiOS周りのデザインの話もあって、全体で49時間程度ある。(うち3時間位は利用ライブラリのバージョンによって文法が異なるので、バージョン別でカリキュラムが存在しているため飛ばしてもいい。)デザイン周りはもともとそこまでやる予定はなかったのと、Swiftを使ってのアプリ開発の知識が得られればそれでOKだったので2x時間(1日7時間くらいやっててだいたい3〜4日目の前半とかくらい)くらいでドロップアウトした。時間も有限だし、人間にはプログラミング以外で大切なこともあるので、無理して全部続ける必要もないと思っている。

ちゃんと苦手意識のあったところ学習できた?

先にも書いたとおり、ほとんど学習できた。 特にXCodeの基本的な使いかたやAutoLayoutは動画カリキュラムだからこそ理解ができるところで大変よかった。 さらに、WebAPI通信やCoreData、Realmなどのよく使われるっぽいけど初心者にはちょっとレベルのお高めな分野についても触りはできたので触っていないときに比べて幾分スムーズに開発に入れるかなーと言う感じです。

Railsでアプリケーションコードで利用する生SQLを書くときにやっていること

TL;DR

  • SQLは別にそのSQLを実行するためのClassを作ってそこで書いたり色々やっている。
  • パラメータのバリデーションもそのClassでやってる。
    • 実装自体はActiveModelのValidateでサクッと実装。
  • select_allの結果は基本的にはHashなので、Structなどでオブジェクトっぽく振る舞えるようにしている。
  • BaseQueryみたいなコード書いておくと便利
    • ページネーションが必要なときも型はだいたい決まっているので、ベースClassに書いておくと便利そう。

具体例

複数のTableの内容LIKE検索したものをUnionで結合したクエリを発行するような Search クラスを実装してみることこんな感じ。 今回はMySQLを想定して実装してみました。

Controllerからはこんな感じで呼び出せるものとして実装しています。

※あくまで実装のご提案で流れがわかればいいと思ったので、 このコードを実際に動かして確認はしてないです。(業務コードではだいたい同じ感じで動いてますが。)

class SearchController < ApplicationController
    def index
        params[:page] ||= 1
        params[:limit] ||= 30
        @search = Search.execute(params)
        if @search.success?
            render 'index.json'
        else 
            render json: @search.errors, status: 422
        end
    end
end
class Search
  include ActiveModel::Model
  include ActiveRecord::Sanitization::ClassMethods

  attr_reader :result, :page, :limit, :order, :keyword, :context, :total_count

  validates :keyword, presence: true
  validates :context, inclusion: { in: %w(all staff user) }
  validates :order, inclusion: { in: %w(desc asc) }

  def self.execute(params)
    new(params).execute
  end

  def execute()
    if valid?
      con = ActiveRecord::Base.connection

      @search_keyword = sanitize_sql_like(keyword)
      @total_count = con.select_value(total_count_sql)
      @result = con.select_all(search_sql)
      assign_objects()
    end
    self
  end

  def initialize(params)
    @page = params[:page].to_i
    @limit = params[:limit].to_i
    @order = params[:order]
    @keyword = params[:keyword]
    @context = params[:context]

    @result = nil
    @total_count = 0
  end

  def success?
    result.present? && errors.empty?
  end

  def last_page?
    return true if total_count == 0
    (total_count.to_f / (limit * page)).ceil <= 1
  end

  private

  attr_reader :search_keyword

  def search_sql
sql_text =<<EOF
  SELECT s.* FROM (#{base_sql}) s #{order_sql} #{pagenate_sql}
EOF
  end

  def total_count_sql
sql_text =<<EOF
  SELECT COUNT(*) FROM (#{base_sql}) s #{order_sql}
EOF
  end

  def base_sql
    case context
    when "all"
      [staff_sql, user_sql].join("UNION\n ")
    when "staff", "user"
      send("#{context}_sql")
    else
      raise TypeError 'Not Found Context'
    end
  end

  def user_sql
sql_text = <<EOF
SELECT 
     user.id          AS id,
     user.name        AS name,
     user.image_url   AS image_url,
     user.created_at  AS created_at,
     user.updated_at  AS updated_at
FROM   users AS user
WHERE  user.name LIKE '%#{search_keyword}%'
EOF
    sql_text
  end

  def staff_sql
sql_text = <<EOF
SELECT 
     staff.id          AS id,
     staff.name        AS name,
     staff.image_url   AS image_url,
     staff.created_at  AS created_at,
     staff.updated_at  AS updated_at
FROM   staffs AS staff
WHERE  staff.name LIKE '%#{search_keyword}%'
EOF
    sql_text
  end

  def order_sql
    "ORDER BY s.created_at #{order.upcase}"
  end

  def pagenate_sql
    "LIMIT #{limit} OFFSET #{offset};"
  end

  def offset
    (page - 1) * limit
  end
 
  def assign_objects()
    SearchObj = Struct.new(:id, :name, :image_url, :created_at, :updated_at)
    @result = @result.map { |r| SearchObj.new(n['id'], n['name'], n['image_url'], n['created_at'], n['updated_at']) }
  end
end
#.jbuilder
json.result (@search.result) do |result|
  json.extract! result, :id, :image_url, :name, :created_at, :updated_at
end

json.page @search.page
json.limit @search.limit
json.order @search.order
json.keyword @search.keyword
json.context @search.context
json.total_count @search.total_count
json.is_last_page @search.last_page?

この設計自体の工夫点

呼び出しメソッドのルールを決める

  • これが一番なによりも肝だと思っていて、この生SQL用のクラスを作っておくことで、Controllerに生SQLを書くことを防げるし、テストも実装の置き換えを容易にしているところがこの実装の気に入ってるところです。
  • 自分のケースではそこまでこのクラスを多用することがないので、雑に modelsに放り込んでいます。
  • 呼び出しのメソッドについては、今回のところは#execute.executeにしてますが、#call#queryにしてもいいかもしれません。

ActiveModelでサクッとパラメータバリデーション

  • errosって変数が生えてバリデーションエラーの内容を詰めてくれるし、簡単にバリデーションの定義ができてとにかく便利。ここでは、バリデーションエラーの場合はクエリを実行しないようにしています。

ハッシュ地獄からの脱却

select_allの内容は厳密には異なりますがまぁHashです。

これはRubyを使っている以上なんかイケてないのと、要素のTypoに気づけない問題があるので、resultの内容は、Object(っぽいもの)にしたいですね。 ちょっと大きめのものであればDecoratorクラスを作っていいですが、比較的シンプルなので今回は簡単にStructで実装してみました。

Decoratorクラスについてはこの辺を参照するといいと思います。 morizyun.github.io

もうちょっと便利にしてみる

ここからはこの記事を書いていて思いついたのですが、 もうちょっとだけこのクラスをブラッシュアップできそうです。

ベースクラスを作る

ページネーション関連のメソッドは、特殊な要件がなければこの実装方法で基本的には問題にならないでしょう。 なので、ページネーションと必ず利用する(であろう)パラメータに関しては共通化してしまいましょう。

今回の場合は適当に、 SearchQueryBase とかにしておきましょうかね。 今回は継承をベースにしていますが、ModuleでどうにかするのがRubyっぽいかもしれません。ただ superとか使っているので、自明かと思って今回は継承にしてみました。

class SearchQueryBase

  include ActiveModel::Model
  include ActiveRecord::Sanitization::ClassMethods

  attr_reader :result, :page, :limit, :order, :total_count

  def self.execute(params)
    new(params).execute
  end

  def execute(params)
     raise NotImplementError
   end

  def initialize(params)
    @page = params[:page].to_i
    @limit = params[:limit].to_i
    @order = params[:order]
    @result = nil
    @total_count = 0
  end

  def success?
    result.present? && errors.empty?
  end

  def last_page?
    return true if total_count == 0
    (total_count.to_f / (limit * page)).ceil <= 1
  end

  private

  def pagenate_sql
    "LIMIT #{limit} OFFSET #{offset};"
  end

  def offset
    (page - 1) * limit
  end
end

class Search < SearchQueryBase

attr_reader :result, :page, :limit, :order, :keyword, :context, :total_count

  validates :keyword, presence: true
  validates :context, inclusion: { in: %w(all staff user) }
  validates :order, inclusion: { in: %w(desc asc) }

  def execute()
    if valid?
      con = ActiveRecord::Base.connection

      @search_keyword = sanitize_sql_like(keyword)
      @total_count = con.select_value(total_count_sql)
      @result = con.select_all(search_sql)
      assign_objects()
    end
    self
  end

  def initialize(params)
    super
    @keyword = params[:keyword]
    @context = params[:context]
  end

  private

  attr_reader :search_keyword

  def search_sql
sql_text =<<EOF
  SELECT s.* FROM (#{base_sql}) s #{order_sql} #{pagenate_sql}
EOF
  end

  def total_count_sql
sql_text =<<EOF
  SELECT COUNT(*) FROM (#{base_sql}) s #{order_sql}
EOF
  end

  def base_sql
    case context
    when "all"
      [staff_sql, user_sql].join("UNION\n ")
    when "staff", "user"
      send("#{context}_sql")
    else
      raise TypeError 'Not Found Context'
    end
  end

  def user_sql
sql_text = <<EOF
SELECT 
     user.id          AS id,
     user.name        AS name,
     user.image_url   AS image_url,
     user.created_at  AS created_at,
     user.updated_at  AS updated_at
FROM   users AS user
WHERE  user.name LIKE '%#{search_keyword}%'
EOF
    sql_text
  end

  def staff_sql
sql_text = <<EOF
SELECT 
     staff.id          AS id,
     staff.name        AS name,
     staff.image_url   AS image_url,
     staff.created_at  AS created_at,
     staff.updated_at  AS updated_at
FROM   staffs AS staff
WHERE  staff.name LIKE '%#{search_keyword}%'
EOF
    sql_text
  end

  def order_sql
    "ORDER BY s.created_at #{order.upcase}"
  end

  def assign_objects()
    SearchObj = Struct.new(:id, :name, :image_url, :created_at, :updated_at)
    @result = @result.map { |r| SearchObj.new(n['id'], n['name'], n['image_url'], n['created_at'], n['updated_at']) }
  end

end

少しだけスッキリしました。 継承したので、Rubyらしくはないですが#executeをInterfaceっぽい感じで、必ず実装しておくべき項目かを自明にしておきました。 これで似たような生SQLを書く際にルールを統一できて良さそうです。

ただ、SQLRubyのコードももうちょっと洗練の余地がありそうな気がします。 例だとSearchControllerでparamsの初期設定しているけど Searchクラス自体にも設定するべきかとか、resultのnilとempty状態でsuccess?の結果が異なるとかかなぁ… 誰かにレビューしてもらってもうちょっと良くしたいな…

そもそも分析用途でなく、幾分シンプルなら生SQLでなくArel使えばいいのでは?

  • ArelはあくまでRailsのプライベートAPIであり、Railsコミュニティから、アプリケーションコードで使うものではないとアナウンスされているから。

テストについて

  • そもそも生SQLを扱うときはテストがないと後からメンテができなくて本当に辛いので、なるべく書くようにしています。
  • 今回自分の使ったケースでは、規模が拡大した段階でElasticSearchなどへの置き換えを検討しているので、Searchクラスへのテストはあまり書かないで、使っているAPIのエンドポイントへのRequest Specに少し厚めにテストを書きました。
  • ElasitcSearchに置き換えした後も共通のインターフェイスで扱えると良いかなと思っています。

終わりに

自分はこんな感じでやっていますということを書いてみました。 みなさんどんな感じでやっているのでしょうか…? 機会があればおききしたいところです。

インフラエンジニアの教科書を読んだ

インフラエンジニアの教科書

インフラエンジニアの教科書

さらっと一度読んだので、インフラ方面のスキルを付けたいって思ったので再読することにした。

インフラエンジニアの教科書といっても紹介していることは、今どきなクラウドをゴリゴリ触ったりするようなクラウドのことだったり、 SRE寄りのことはあんまり書いていなくて、OSの種類だったり、CPUについての基礎知識、あとはソフトウェアのことよりもハードウェアについての基礎知識(例えばRAIDについてだったり、ディスクの種類)に多くページが割かれている印象があった。

自分は普段はサーバーサイドエンジニアの方面から必要なWebインフラの知識を入れていった感じなので、 とくにハードウェア周りの基礎知識だったりが欠落しているので、それを補完するのに大変良い書籍だった。

あとは、業務で活かす機会としては多分無いだろうけど、 「購買と商談」や「データセンター」という章があったり、この辺は超大規模な企業なんかのインフラエンジニアはこういったことをするんだなぁというので知れて良かった。

2018年振り返り

2018年の目標について

  • 前述しましたが、社の一員として、成果を上げること(メインは技術的負債の返済)
    • 一番大きめの問題については問題を解消する目処が立てた。後述します。
  • MHWに時間を吸われないように
    • 200時間以上吸われた…
  • 勉強会で積極的に登壇する
    • LTが1回でした。
    • もうちょっと増やしたいっすね。

書いたブログ

記事の内容はどうあれ半年くらい書くことを続けていたので、それなりに書いた気がする。 全体的にほぼ全部の記事で反響はなかったんだけど、個人的にはこのあたりの記事を気に入っていました。 暇なら見てください。

webuilder240.hatenablog.com

webuilder240.hatenablog.com もう毎週は書いていません。

webuilder240.hatenablog.com

技術的な振り返り

技術LT

webuilder240.hatenablog.com

一本だけでした。ただLTデビューはできました。

FirebaseというかFirestore

ちょうど去年の今頃はチャット機能を作るにあたって色々アーキテクチャを探したり試している最中で、 その中でベータリリースされたばかりのFirestoreとFirebaseを触っていた。 そんな後述するUIの全面的な改修の目玉機能として、チャット機能を作るのに最終的にFirestoreを利用することになった。 RealTimeDatabaseも試してみたけど、Firestoreのほうがやはり後発なだけありかなりサクサク作れたので、作り始めるタイミングとして本当にラッキーだった。

今年はまさにFirestore元年な年だと思った。

UIの全面的な改修

後述するシステム移行の第一弾的施策として、UIの全面的な改修を行った。 スケジュールに無理があったり、ちょっとデザイナーとのごたごたがあったりして、 スケジュール通りに行けるかどうかかなり不安だったけど、蓋を開けてみると当初のスケジュールからあまり変更することなくリリースできた。

システム移行について

これまでかなり非効率だったシステム構成についてもこのままではビジネス自体の拡大を妨げてしまうので、 幾分拡張性を捨ててでも、プラットフォームサービスとして運用するほうがビジネス的にも開発側でもメリットがあるという結論にいたり、 小規模の20個ほどあるシステムを1つのシステムにまとめるシステム移行を会社として決断して実行することにしました。 新アーキテクチャ自体は完成して、新しい案件をそちらで動かしているけど、既存の案件の移行はまだという感じです。 既存の案件の新アーキテクチャ移行は来年年始から早速始動していくという感じです。

もうちょっと具体的な話については、LTとか、知っている方は直接おはなしできればとか思っています。 とにかくご協力いただいた方々には感謝しています。

PR活動

送ったPRくらいは残しておいて振り返ってもいいかなと。

ngx_mruby

github.com

前述のシステム移行の技術検証の一環として試したときに出た(確かOpenSSLが古いとかだった気がする)問題があり、 単にOpenSSLをアップデートするより、Ubuntuのベースイメージを上げてそっちで動かすようにすればいいかなと思って対応した。

MergeされたあとにDockerイメージが自動ビルドされなかったので、松本さんにお願いしてDockerイメージがビルドされるよう設定し直して頂いたりしました。

github.com

こういうDockerイメージがないライブラリやプロダクトのDockerイメージを作る業みたいなのは地味に喜ばれるんじゃないかなと思った。*1

ただこの公式Dockerイメージ、あんまり使われてなくてどうしてなんだろうかという気持ちになっている。

lograge-sql

https://github.com/iMacTia/lograge-sql/pull/4github.com

そんなにStar無いんだけど、ちょっといいなと思ってPR投げた。 その後にもうちょっと便利なソリューションが生まれてて、そっちが最初からあればいいのかーってなった。

mock_redis

github.com

会社でたまたまペアプロしていたら見つけてその晩に対応コード書いてPRしたもの。 mgetでキーが存在しない場合の挙動が異なっていました。 こんな拙い英語でも伝わったので良かった。

意気込み

社にエンジニアが数名入ってきたりしたこともあって、自分の立ち位置も安泰ではないと思っている。 社のリードエンジニアとして技術では社の誰にも負けたくないと思っているし、技術って幅広いし、自分が負けている部分があるにしても少なくとも自分がどこかの分野で1番詳しいものがないといけないと思っている。そのためにはなんでもやるし努力は惜しみたくないと思っています。

2019年: 目標

これだと思える技術見つけて、エンジニアの特色を出したい。

  • どの技術も少しだけできるみたいな器用貧乏になっちゃってるのでなんとか脱したい。もうちょっとちゃんと極めたい。
    • デザインパターンとか、設計にも興味があってそれはどんな技術にも大抵は応用が効くので、やってみようかな…
  • 28でネイティブアプリを少しかじってみて、29までに決めて、30でとことん技術に投資するとかでもいいんですかね。
    • うーん、遅いかなぁ、なんか受験みたい…

毎年、システムを廃止したり、移行したりする仕事ばかりなのでいい加減年始でやるシステム移行で最後にしたい。

  • 2016年もやったり、2017年もやった。2018年もやってて、2019年こそはこういったシステムの移行案件はあんまり心臓に良くないので、できればないようにしたい。

ネイティブiOSAndroidやっていきたい。

  • 残り半年で、Androidはまぁ趣味とかで。
  • ReactNativeとかはネイティブある程度やってからでいいかなー。

おわりに

それでは皆様残りわずかですが良いお年をお過ごしください。

*1:単にDockerイメージを用意するのもそうだし、Alpine化してよりスリムなイメージを出すとか。