ボクココ

個人開発に関するテックブログ

MacBook Pro M1 を買ってみた感想

ども、@kimihom です。

f:id:cevid_cpp:20210131164557j:plain

先日から MacBook Pro の M1 チップ搭載のものを使い始めたので、その感想をつらつらと書く。

購入の背景

まず、前まで使っていた Mac は、MacBoook Air 2013 年版のものだった。買う前までは問題なくこれからも使い続ける気が満々だった。

しかし、最近から開発で Xcode を使う必要が出てきた。残りのストレージをなんとか用意して、起動してみたんだけど、あまりにもスピードが遅く、さらに他のビデオ通話などにも影響が出てしまい、そろそろ買い換えるしかないかと判断したのであった。そして最近たまたま M1 ってのが出たらしいことを知って、それを買ってみたというわけである。

あんなに Xcode が重いのは、容量不足にさせて新しいものを買わせる Apple の戦略なのかもしれない。

Xcode が軽い

何もない状態で Xcode を入れたんだから当たり前ではあるけど、Xcode が軽い・・!同時にブラウザや他のソフトウェアを起動しても全く問題がない速度だ。

電池の持ちが長い

カフェでブログを書くことの多い私にとって、電池の持ちが長いのもありがたい。 16時間は持つってことなので、ケーブルをわざわざ持っていく必要もないし、電源のある席を探す必要も無くなった。逆を言えば、ずっとカフェに居続けてしまう原因ともなりそうだ。長居しすぎないように気をつけよう。

スピードが全く違う

あらゆるスピードがとにかく速い(Mac 2013年比較 笑)。ブラウザの起動やページの遷移も、同じネットワークを使ってるのに全く速さが違う。

とはいえ最終的には使い手である私たちがいかにスピード早く考え抜き、タイピングできるかにかかっている。個人的にはこのスピードは単なるおまけくらいかなと思う。

開発に支障をきたす

まさかではあったが、うまく Rails 環境構築ができていない(現状)。ffi の Ruby gems がうまく入れられず、Rails 開発がうまくいっていない。どうやら rbenv を利用するとうまくインストールができないようである。ひとまず Rails 開発は前の Mac で開発して、解決案が出てきたら対応してみようと思う。

1.14.2 arm64 / m1 issue: ffi_c.bundle, 9: no suitable image found. · Issue #878 · ffi/ffi · GitHub

ググると色々なやり方があって、方法の一つとしてMac デフォルトの Ruby だとインストールできるってのがあった。最低限これで、iOS 開発に必要な CocoaPods は、デフォルト Ruby を使わせることで動くようになった。

購入したもの

最近の MacBook は USB すらない構成となっている。実際に使うときには確実にケーブルが必要となるだろう。

私の場合は USB さえ使えればいいので USB の繋ぎを4つ得られるものを買った。

デュアルディスプレイにするために USB で画面を繋げられるようにした。

終わりに

今後は MacBook Pro M1 を使って爆速? で Xcode での開発をしていこう。

Swift の型と省略記法を学ぶ

ども、@kimihom です。

f:id:cevid_cpp:20210124175657j:plain

まだ Swift を学習中の状態だ。Swift で実際に書かれたコードを読むと、「!?なんだこの書き方は」っていう謎の文法のソースコードをよく見る。そこをスルーせず、ちゃんと理解することで、心地よく Swift コードを読むことができよう。

型の中でも、特徴的なのが Optional型 だ。値が"ある"か, "nil" かいずれかを表す型である。 実際に存在する場合の Optional型にアクセスするには、事前に検証するコードが必要となる。 検証方法1では if 文で代入して確認する方法。検証方法2では各 opt の書き方で nil の場合には ?? の右の値が使われることになる。検証方法3ではメソッド内の検証で通れば出力させる。

let opt1 = Optional.some(5)
let opt2: Int? = nil
let opt3 = Optional(5)

// 検証方法1
if let opt = opt1 { 
  print("opt1 is /(opt1)") // opt1 is 5
}
// 検証方法2
print("opt1=\(opt1 ?? 0), opt2=\(opt2 ?? 0), opt3=\(opt3 ?? 0)") // opt1=5, opt2=0, opt3=5

// 検証方法3
func test() {
  guard let optg = opt1 else {
    return
  }
  print("opt1 is \(optg)") // opt1 is 5
}
test()

省略記法

関数でクロージャを返す場合の書き方がいくつかある。Swift のコードを見ると、ほとんどが3つめの最も省略されたケースを使うことが多い。これは、引数がクロージャのみである場合にのみ利用できる。

func execute(handler: (String) -> Void) {
    handler("called")
}
execute(handler: { string in
    print("1 \(string)")  // 1 called
})
execute() { string in
    print("2 \(string)")  // 2 called
}
execute { string in // 引数が1つのクロージャのみの場合
    print("3 \(string)")  // 3 called
}


// クロージャ関数定義しない場合 $0 は map クロージャに渡ってくる1番目変数
[1,2,3].map { $0 * 2 }
[4,5,6].map { $0 * 2 }

// クロージャ関数: $0 * 2 を一つにまとめられる
// double と 命名できる
func double(_ x: Int) -> Int {  // double(3) => 6
  return x * 2
}
[1,2,3].map(double)
[4,5,6].map(double)

終わりに

Swift の基本的なところではあるが、特徴的な書き方をするものをピックアップして記事とした。

これらの記法は見るだけではパッと理解できないけど、理解すれば自分でもシンプルな書き方を率先して使うことになると思う。

以下の本を読んで Swift の言語に関して一通り理解できた。その他にプロトコルとジェネリクスが関門となるだろうが、他の言語を学んだ経験があったので概要を理解できた。 混乱したらまた読んで理解しよう。

Swift 学習中の個人的考察

ども、@kimihom です。

f:id:cevid_cpp:20210117114311j:plain

以下の本でまず Swift の言語を学んでいる。一通り読みきったので、Swift の感想を書こうかと思う。

コンパイルによる厳密性

最近、プログラミング言語全体でよく出てくる話が、コンパイルで事前にエラー把握し、エラーの起きづらいプログラムを書くことの有用性である。型を厳格に管理することで、実行時にエラーが起き辛いプログラムを書こうという流れだ。そんな中で、クラスの継承を使うのではなく、インタフェースを定義していく流れである。

Swift はこの流れを極めるに極めた言語だと感じた。一度定義した型に入れた変数を後で変えることはできない。

  • nil と int が入るもの
  • int だけが必ず入るもの

の2つにも違いがあって、nil が入る可能性のあるものは、Optional 型として定義される。Optional 型の Int(nil の入る可能性のあるint型) を利用する場合、そこに実際にある int 型へアクセスするには、if 文で存在していた場合~ って処理を必ず書かないとコンパイルエラーが出る。

プログラミングする人の対象

さて、私には2種類のプログラマーがいると考えている。

  • 個人でプログラムを書く人
  • チームでプログラムを書く人

Ruby(v2を想定して欲しい) とかの柔らかい言語は前者が対象で、Swift は後者を対象として設計されているように思う。Ruby も最近のv3では(おそらく)チームプログラミングの人を考えたアップデートがされているみたいで、どんどんチームでプログラミングすることが前提とされた言語が増えていっているように見える。

個人で Ruby を使う分には、全て自分の想像通りにすぐプログラムが書ける Ruby の柔軟性が快適で、個人でサービスを運営する時の Ruby の便利さを感じてしまう。個人だったら、"変数AはB処理で変数が入ってCクラスから使われる" みたいなのが自分の頭の中に入っている。もちろん忘れることもあるけど、個人開発ならコードを見ればさっと思い出せるので問題が起きづらい。自分の思ったことをそのまま実現できる、Ruby の"楽しく" "簡単に" 書けるプログラミング言語が好きだった。

「チームでプログラミングする」って時に、何が問題になるかっていうと、Ruby などの柔らかい言語だと、"他の人が書いた" 変数の管理で意図しない動作が発生して、エラーがすぐ出てしまうことになりやすい。もちろん、柔らかい言語でもそれらを防ぐためにテストコードがあるわけだけど、それでも限度があるというわけだ。

Swift がチームプログラミング前提?

Ruby とかに比べて、Swift 言語の方がなおさら個人開発者が多い(と思う)ので、柔らかい言語にしてくれたらいいのになーと思った。

個人開発者がそこまで型を厳しく管理する必要があるのか。Webサービスよりも、大量にクラスや変数があるような大規模なスマホアプリで作ることがあるのか?私はスマホアプリは Webサービスに比べて小規模のエンジニアで小規模の機能を作ることが多いんだから、むしろ Swift は柔らかい言語ですぐに作れた方がいいのになぁと思った。

もちろんそんなことを言っても Swift で実装しなきゃいけないことには変わりないので、ルールに従って Swift でアプリを書いていこう。Swift でしっかり動くアプリを作れば、アプリを他の方が担当しても安全になる予想はできたので、その良さを考えながらプログラミングしていこうと思う。

終わりに

色々なプログラミング言語を学んでいると、それぞれの違いについて特に考えることになる。 それぞれにメリットがあり、それぞれにデメリットも出てくる。

Web サービスでは自分の好きな言語で扱う機会が多い。ただスマホアプリってなると Swift / Kotlin が基本となる。最近はそれ以外の選択肢もメジャーになりつつあるけどね。

次は SwiftUI という、新しい iPhoneアプリUI の作り方について学んでいこう。

スマホアプリ開発に先駆けて

ども、@kimihom です。

2021 年の新しい挑戦の1つとして、スマホアプリ開発がある。 私自身の経験を伝えると、6,7年前ごろに Android アプリで Java で開発していたことがある。これは仕事でガッツリ実装してたので、だいぶ詳しくはなかったが、それでもだいぶ前なので技術として参考になることは少ないだろう。そもそもプログラミング言語自体 Kotlin に変わっている。

また、iPhone アプリも開発していたことがある。Swift v1.0 が出立ての頃に、個人の趣味で作った Android アプリの iPhone 版を作ってリリースした。そのアプリは Android マーケットより厳しい iPhone マーケットで受け入れられず、しばらくしたらマーケットから消えてしまっていた。それを改善するほどの個人のやる気もなかったので、そのままスルーで終わった形である。一応 Swift で書いていた程度である。

さて、そのくらいの開発経験で、今どうやってスマホアプリの実装をしていくか考えた。

今、Flutter と呼ばれるプラットフォームが流行っているようだ。開発元は Google。コードを Dartと呼ばれる独自プログラミング言語で書き、それを iPhone, Android アプリで動くように変換されるようだ。UI はプラットフォーム側のではなく、独自で作られたのを共通で使うとのことである。なるほど、Dart が選ばれる理由もよくわかる。確実に iPhone, Android アプリ両方作るなら、この選択肢をまず真っ先に考えるだろう。

f:id:cevid_cpp:20201217084925p:plain

もし私が、単純な API 呼び出しとシンプルな UI 構成でできたアプリを作るのであれば、確実に採用していただろう。ただ、これから作ろうとするものが何かという点でより詳しい検討が必要である。

来年以降開発をしようとしているアプリは、サードパーティが開発運用しているライブラリを利用予定である。そして、そのライブラリが、完全に Swift, Kotlin で書く前提のライブラリであった。もちろん、Dart で頑張って書くという選択肢もあるけど、ドキュメントが全て Swift, Kotlin である以上、明らかにそれぞれのプログラミング言語でドキュメントを読みながら作った方が安全で正しくアプリが作れる。また、私自身 Swift の経験があるし、あとは似た言語? のイメージである Kotlin を学ぶだけなのでそこまで苦戦しなさそうというイメージを持っている。

f:id:cevid_cpp:20201217090049p:plain

最初から複雑な UI や機能を実装する計画でもなく、まずはその サードパーティのライブラリを組み込んでアカウントログインさえできればと考えている。その程度であれば、Swiftと Kotlin それぞれで書くこともそこまで辛いことではなさそうだ。

1人が2つのアプリを作るとなるけども、幸いどちらも経験がある。 Android Studio と Xcode という重たいソフトウェアを2つ入れるのが億劫なくらいである。

開発のきっかけ

今や、スマホアプリは個人で使うだけのものではなく、仕事でも当たり前に使われるようになっている。少し前までは、会社の電話はスマホではなく古い感じの携帯電話を使っている人も多かったけど、最近はもうビジネスで使うサブの電話もスマホになっている。

そうすると、ビジネスで普段使うスマホに、ビジネス用の Android/iPhone アプリのニーズがどんどん高まってくる。以前の 私の Android/iPhone 開発は、完全に個人向けのものだったけど、来年はビジネス向けという全く違うターゲットで開発を初めていく予定だ。

終わりに

来年の大きな技術的挑戦の一つスマホアプリの現在の考えについて記した。

まだ現時点での検討ということもあるけど、既に Swift UI という新しくなった iPhone アプリ開発を調べ始めているところだ。既に来年の挑戦は始まっている。

Web がメインだったボクココの記事ではあるけど、今後はアプリ開発におけるネタも多く出てきそうだ。

年末年始にどれだけ勉強して開発に持っていけるか。その空いた時間に何もしないか学ぶかで大きな差が出てくるので頑張っていこう。

Firebase はアプリ開発者のヒーローとなるか

ども、@kimihom です。

f:id:cevid_cpp:20160713231835p:plain

今日 Firebase の勉強会に行った。あらかじめ動画とかで Firebase の予習はしてたんだけど、今回の勉強会で実際のユースケースとか具体的な使い方を知ったことでよりイメージが湧いた。そこで感じた Firebase について今回はちょろっと語ろうと思う。

アプリ開発におけるサーバー/サービス選択の歴史

基本的にネットワークを使わずにアプリだけで完結するアプリってのは単純なものが多く、ユーザーを獲得することは困難になりつつある。より良いアプリにするには、クラウドと連携して"つながる"とか"最新情報がある"アプリにすることが大事であることは自明なことかと思う。

そんな中でエンジニアがアプリ開発している時に大抵決まって同じような悩みを抱く。その典型が"サーバーどうするの"っていう話。サーバーがなければ画像を置くこともできないし、ユーザーを管理することもできず、情報を取得/更新することもできない。要するに何もできないってわけだ。

今までは自前で API サーバーを持つことが一般的だった。これは今までの Web 知識を用いれば多少時間がかかっても実装が可能だし、Webアプリとの連携もデータベースを共有することで可能になる。 だから大手とか小さいところでも自前で API サーバーを持つことが多かった。というよりそれくらいしか選択肢がなかった。

でもみんなやるよねって機能はやはり楽をしたいもの。 AWS をはじめとしたモバイルのプラットフォームがどんどん出てきて、アプリ開発者が自前でサーバー実装しなければならないことを極端に少なくしてくれた。アプリエンジニアはそれらを活用することでより早くより高品質なアプリを作り出すことができる。

しかし、次に問題になってくるのは色々なアプリ開発のモバイルプラットフォームを使うと、アカウント登録がその都度必要で、クレジットカードとか登録情報とかいちいち管理しなければならない問題が出てくる。またそれらの情報を統合的に管理することができず、それぞれがそれぞれのデータを持って分断された分析や機能の追加しかできなかったわけだ。AWS のモバイル系のサービスは各種機能を統合することを実現してはいるが、"アプリ開発に特化した"って意味では弱い部分があると個人的に思う。AWSをはじめとした総合的な開発プラットフォームでは、各種サービスが Webでもモバイルでも使える汎用的な部分が多く、アプリだけで欲しい特化したサービスが弱いと感じている。

そこでつい最近に進化した Firebase である。進化した Firebase は"モバイル開発において必要だよねー"ってところを基本的にすべて統括的に提供している。Firebase にまとめて情報を管理するようにすれば、断片的だったデータを一元管理できるようになり、使う SDK も最低限のものだけにすることができる。いろんなSDKを詰め込むようなことはしなくて良いし、余計なコードを書く必要もない。(Firebase の各機能は Gradle 経由で簡単にインストールできる)。 確かに機能的には他の専門ツール(例えば分析系)に劣る部分はあるかもしれない。ただそれも時間の問題で、ゆくゆくは Firebase だけですべてがまかなえるような、そんなプラットフォームになっていくだろう。

特に Firebase では Web とアプリの垣根をなくしていくという点で、Deep Link や App Indexing などの Google 検索などに親和性の高い機能も提供しており、これらは Firebase ならではという感じがしている。今までのFirebaseであるリアルタイムデータベースもそうだし、 Remote Config もまさにアプリだからこそ欲しい機能であり、AWS など汎用的なクラウドプラットフォームでは提供できていない部分である。

開発プラットフォームの選択

私はこのような思いからアプリ開発に特化した開発プロジェクトを始めるなら Firebase を使う。というか使い始める予定である。

しかしアプリに特化というよりも、 CGM(ユーザーがログインして投稿しあうようなサービス) のような従来の Web サービスをそのままアプリにするっていうのであれば、 Rails の Turbolinks などを活用したアプリ作成の方がいい選択になる場合もあると思っている。最近のHTML5の技術と、それ+@ 程度の機能の要件のサービスであれば、Webベースのアプリを検討すべきである。この場合は Firebase を使う必要はほとんどないだろう。詳細は以下の記事に書いた。

www.bokukoko.info

当たり前の話ではあるが大事なのは適材適所である。

私の Firebase に対しての唯一の要望

そんな私が絶賛する Firebase だが、あと一つ欲しいものがある。それがサーバーサイドでロジックを実行できる、 Cloud Functions と Firebase の連携だ。やはりどうしてもサーバーサイドで実行したいようなロジックがあるときに、Firebase で Cloud Functions と同等の機能があれば、もう他のアプリ開発サービスを使う必要はなくなってくると思う。わざわざちょっとしたロジックだけのために Google App Engine や Google Cloud Platform などは使いたくないってケースは大いにあるだろう。

この願いを持っている人は他にもいるみたいで、今 Google では検討中? な感じのようである。

Google グループ

終わりに

この記事を書いた背景としては、まず進化した Firebase がすごいと感動したからであり、その凄さをまだほとんどの人は知らないんじゃないかという思いで書いたというのがある。

一部の熱狂的なアプリ開発者だけが使われて、それらのアプリだけが差別化してどんどん成長していくってのはもったいない気がしたので、ぜひ経験の浅いアプリ開発エンジニアでも Firebase にチャレンジしてみて欲しいと思う。

ドキュメントはやはり英語だから辛い部分もあるかと思う。でも英語を読むだけで実装のほとんどをやらなくて済むようになるので、頑張ってみて欲しい。

私もこれからどんどん Firebase を使っていくが、そこで得た知見は適宜このブログで書いていきたいと思う。

iOS Swift プッシュ通知を受け取ってからViewControllerに情報を渡す

久々の iOS のトピック。今回はiOSで初めてプッシュ通知(Push Notification)を利用したので、それのメモ。

プッシュ通知について

サーバーの任意のタイミングで情報を端末に送信する仕組み。これぞアプリのできること、として欠かせない機能だ。何か最新ニュースが出たりとか、リアルタイムで情報を送りたいときによく使われる。てか人気アプリで使ってないところはないくらいな機能だ。

Webアプリでは任意のタイミングでブラウザに情報を渡す仕組みを実装するには、やや特殊なことをしないと難しい。(Web Socket や Polling などを利用するだろう)

設定手順

  1. Apple Developer にて 証明書を作成
  2. Amazon SNSで作成した証明書をアップロード
  3. サーバーにてAmazonSNSの通知を発行
  4. iOSアプリで通知を受け取る

Apple Developer で証明書作成

iOSはコードは簡単だけど、Apple Developer での登録がめんどくさい。まずはPush Notification を利用できるようにするために証明書を作成する必要がある。

AppIDでプッシュ通知したいアプリを作成し、 ここのPush Notifications の項目をオンにして作成。そんで作成したAppIDを選択し、Edit から CreateCertificate を選択。キーチェーンアクセスで作成したcertSigningRequest ファイルをアップロードし、証明書を作成する。

その証明書をキーチェーンアクセスへドラッグアンドドロップして、それをディスクに書き出す。これがp12形式にして書き出す。これに合わせてプロビジョニングファイルも作成しておこう。

Amazon SNS でのプッシュ

Amazon SNS を使えば、 iOS, Androidの区別をほとんどきにすることなく、プッシュ通知を送ることが可能だ。しかも何万通もプッシュ通知送っても料金は非常に低価格。これは使わない手はない。

http://d2wwfe3odivqm9.cloudfront.net/wp-content/uploads/2014/05/Amazon_SNS-320x320.png

Application で Apple Development を選択し、 p12 ファイルをアップロード。その後Load Credential from file を押すと作成ボタンが押せるようになる。作成したApp Arn をサーバーから参照できるようにしよう。今回はAWS_SNS_APP_ARNとして環境変数に保存する。

サーバーにてAmazon SNSの通知を発行

今回は Ruby を例に、発行方法を記述。bundler にて gem "aws-sdk"sdkをインストール。

config/initializers/aws.rb

Aws.config.update({
  region: ENV['AWS_REGION'],
  credentials: Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'])
})

app/controllers/samples_controller.rb

      sns = Aws::SNS::Client.new

    response = client.create_platform_endpoint(
      platform_application_arn: ENV['AWS_SNS_APP_ARN'],
      token: device_uuid
    )
    target_arn = response[:endpoint_arn]

      message = {
        "APNS" => {
          "aps" => { "content-available" => true },
          "other_data" => obj
        }.to_json,
        "APNS_SANDBOX" => {
          "aps" => { "content-available" => true },
          "other_data" => obj
        }.to_json
      }
      push_parameters = {
        target_arn: target_arn,
        message_structure: "json",
        message: message.to_json
      }
      resp = sns.publish(push_parameters)

objに送りたいデータ、target_arn に送り先のTargetArnを選択しよう。target arn は iOSアプリから送信されたトークンを元に作成する。今回の変数でいうdevice_uuidだ。

iOSアプリで通知を受け取る

これは簡単。 AppDelegate にて任意のメソッドを実装するだけだ。JSON解析のためにSwiftyJSONを利用している。

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        
        // 通知を許可
        var types: UIUserNotificationType = UIUserNotificationType.Badge |
            UIUserNotificationType.Alert |
            UIUserNotificationType.Sound
        var settings: UIUserNotificationSettings = UIUserNotificationSettings( forTypes: types, categories: nil )
        
        application.registerUserNotificationSettings( settings )
        application.registerForRemoteNotifications()

        return true
    }

    // Device Token を取得
    func application( application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData ) {
        var characterSet: NSCharacterSet = NSCharacterSet( charactersInString: "<>" )
        
        var deviceTokenString: String = ( deviceToken.description as NSString )
            .stringByTrimmingCharactersInSet( characterSet )
            .stringByReplacingOccurrencesOfString( " ", withString: "" ) as String
        
        println( deviceTokenString )
        // このトークンをサーバーに送って保存
    }

    // プッシュを受け取ったとき
  func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
        let json = JSON(userInfo)
        
            let notification = NSNotification(
                name:"my_push",
                object: nil,
                userInfo:[
                    "title": json["other_data", "title"].stringValue,
                ]

            NSNotificationCenter.defaultCenter().postNotification(notification)
        }

        completionHandler(.NoData)
    }
}

NSNotificationCenterからmy_pushという名の通知がアプリ内に飛ぶので、それをViewController側でキャッチしてあげます。

class ViewController: UIViewController {
    override func viewDidAppear(animated: Bool) {
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "handlePushNotification:", name: "my_push", object: nil)
        
    }

    
    override func viewDidDisappear(animated: Bool) {
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }
    
    // observer fired
    func handlePushNotification(notification: NSNotification) {
       // 通知を受け取ったときの処理
    }

}

終わりに

Androidは設定は簡単だけど、実装が面倒。 iOSは設定が面倒だけど実装が簡単。そんな印象。iOSではデフォルトでAndroidでいうEventBusのような仕組み(NSNotificationCenter)を提供してくれてるのがありがたい。乱用するとカオスになりかねないが、こういうときには役立つ機能だと思う。

ただ通知する中身が空だったり間違ったりすると通知が来なかったりするパターンがあるので、送るデータがちゃんと入っているかを確認したほうがいい。これにかなりつまずいた。

表参道で Swift 学習者向けの発表をしてきました

久々にスライド作ったなー。

今日は表参道で Swift 勉強会的なのがあったので、ゆるい感じで発表してきました。

最近 Swift 関連書籍は出てるけど入門者向けの本しかなくて、それで詰まっている人は多いと思う。そんな方のためのスライドを作ってみたので、よければ見てみてください。

Swift Style Guide が Swift 書く前に参考になる

最近は Swift を再勉強中! 一度アプリをがっつり作ったことがあって、それでもう一度Swiftを学んでいる。 そうすると開発中にモヤモヤしていたところが解決される時があって、いい感じ。

今回はコーディング規約について調べてみたのでメモ。

Github をはじめとしたSwift経験者が Swift のコーディング規約をまとめていて、それの日本語版もコミュニティーで翻訳されていたりする。

swift-style-guide/README_JP.md at master · jarinosuke/swift-style-guide · GitHub

raywenderlich/swift-style-guide · GitHub

特に参考になったところをピックアップ。

オプショナル型

これを真っ先に持ってくるあたり、好感が持てる。Swiftを書くと、ほとんどのコンパイルエラーはこのオプショナル型の対応になってくる。慣れないうちはxCodeコンパイルエラーを取るのに一苦労だ。そして手軽に!(暗黙的開示オプショナル型)を使ってアンラップすると実行時エラーになったりする。

極力letを使うようにして、ifによるアンラップをするよう推奨されている。

if let foo = foo {
    // 開示された`foo`の値を使う
} else {
    // 必要な場合はここでオプショナルがnilの場合の処理を行う
}

複数オプショナルの if アンラップの場合

var subview: UIView?
var volume: Double?

// later on...
if let subview = subview, volume = volume {
  // do something with unwrapped subview and volume
}

できる限り struct を使う

Java で書くのに慣れていると、クラス命!って感じなんだけど、Swiftだとクラスはちょいと扱いにくい。 Swift の struct はなぜかクラスでできるようなことをたくさん提供してくれてて、さらにクラスよりシンプルに書けるというのでクラスとの細かい違いはあれど、structを優先的に使うようにしよう。protocolの実装もstructでできちゃうのか!

protocol Vehicle {
    var numberOfWheels: Int { get }
}

func maximumTotalTirePressure(vehicle: Vehicle, pressurePerWheel: Float) -> Float {
    return pressurePerWheel * vehicle.numberOfWheels
}

struct Bicycle: Vehicle {
    let numberOfWheels = 2
}

struct Car: Vehicle {
    let numberOfWheels = 4
}

エクステンションを利用して、プロトコルの実装を元クラスと分ける

テーブルのデリゲートのViewControllerを作成していると、どうもファットなViewControllerになりやすい。エクステンションを利用してそれらを可能な限り分けてみよう。エクステンションでクラスを分けたとしても、同じクラス内の関数を呼び出すことができる。

class MyViewcontroller: UIViewController {
  // class stuff here
}

// MARK: - UITableViewDataSource
extension MyViewcontroller: UITableViewDataSource {
  // table view data source methods
}

// MARK: - UIScrollViewDelegate
extension MyViewcontroller: UIScrollViewDelegate {
  // scroll view delegate methods
}

昔ながらのforは書かない

for _ in 0..<3 {
  println("Hello three times")
}

for (index, person) in enumerate(attendeeList) {
  println("\(person) is at position #\(index)")
}

次回は

matteocrippa/awesome-swift · GitHub をそれぞれ調べてみよう。

iOS8 で画像をアルバム or カメラで取得し、S3 へアップロードする

だいぶお決まりな処理な気がするのでまとめてみる。 iOS8 からは Photos Framework という画像を扱うフレームワークがあり、これを利用していく。 UIImagePickerController を使えば、画像選択まではそれなりにうまくいくのだが、 S3 へアップロードとなるとそのファイルパスが必要になってきて、そのファイルパスをどうやってとってくればいいのかが難しいところ。

画像は特に、自前のサーバへアップロードするんじゃなくて、S3に直接アップロードしたほうが負荷的にも優しいし、マルチパートのPOSTを実装する必要もなくなるので、基本的には採用すべきだと思う。そのURLだけを自前サーバにポストすればいいだけにしよう。

画像をリサイズして色々なサイズの画像をS3に保存したいというようなケースも多くあると思う。その時は自前サーバのAPIで、アプリから渡したS3のURLにある画像を一度サーバに取り込んでからリサイズし、再度S3へアップロードという形の方がいいと思う。とにかくアプリから画像をアップロードする処理の実装はiOSでもAndroidでも大変だからオススメしないw

以下手順をまとめる。

UIImagePickerController を表示

タイプを指定して presentViewController すればいいだけ。とても簡単だ。

        var actionSheet = UIAlertController(title:"Image", message: "Select the image", preferredStyle: UIAlertControllerStyle.ActionSheet)
        var actionCancel = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: {action in
            //nothing
        })
        var actionNormal1 = UIAlertAction(title: "From Album", style: UIAlertActionStyle.Default, handler: {action in
            let imagePickerVc = UIImagePickerController()
            imagePickerVc.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
            imagePickerVc.delegate = self
            self.presentViewController(imagePickerVc, animated: true, completion: nil)
        })
        var actionNormal2 = UIAlertAction(title: "From Camera", style: UIAlertActionStyle.Default, handler: {action in
            let imagePickerVc = UIImagePickerController()
            imagePickerVc.sourceType = UIImagePickerControllerSourceType.Camera
            imagePickerVc.delegate = self
            self.presentViewController(imagePickerVc, animated: true, completion: nil)           
        })
        actionSheet.addAction(actionCancel)
        actionSheet.addAction(actionNormal1)
        actionSheet.addAction(actionNormal2)
        
        self.presentViewController(actionSheet, animated: true, completion: nil)

とりあえずこれだけでアルバムとカメラそれぞれ起動できる。

取得した画像のパスを取得

カメラで撮った場合、画像はまだどこにも保存されない状態で戻ってくるので、いったんどこかに保存して、そのパスを取得する必要がある。アルバムに保存してもいいんだけど、割と撮った写真はそのアプリだけのための一時的な画像であることが多いので、他の適当な場所に保存することにする。

UIImagePickerController が終わったタイミングで呼ばれるメソッドを定義してその中に処理を書く。

    func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) {
            self.dismissViewControllerAnimated(true, completion: nil)
        // from camaera
        if (info.indexForKey(UIImagePickerControllerOriginalImage) != nil) {
            let tookImage: UIImage = info[UIImagePickerControllerOriginalImage] as UIImage
            var imagePath = NSHomeDirectory()
            imagePath = imagePath.stringByAppendingPathComponent("Documents/face.png")
            var imageData: NSData = UIImagePNGRepresentation(tookImage)
            let isSuccess = imageData.writeToFile(imagePath, atomically: true)
            if isSuccess {
                let fileUrl: NSURL = NSURL(fileURLWithPath: imagePath)!
                uploadToS3(fileUrl)
            }
            return
        }
        
        // from album
        var pickedURL:NSURL = info[UIImagePickerControllerReferenceURL] as NSURL
        let fetchResult: PHFetchResult = PHAsset.fetchAssetsWithALAssetURLs([pickedURL], options: nil)
        let asset: PHAsset = fetchResult.firstObject as PHAsset
        
        PHImageManager.defaultManager().requestImageDataForAsset(asset, options: nil, resultHandler: {(imageData: NSData!, dataUTI: String!, orientation: UIImageOrientation, info: [NSObject : AnyObject]!) in
            let fileUrl: NSURL = info["PHImageFileURLKey"] as NSURL
            self.uploadToS3(fileUrl)
        })
    }

このパスを取得する方法だけど、かなり色々試行錯誤した結果の上でのコードなので、他にいいやり方があるのかもしれない。。

これが終わったら、いよいよアップロードだ。

S3 へアップロード

いくつか手順を踏まないといけない。

AWS SDK for iOS のインストール

CocoaPods で簡単に入れられる。

pod 'AWSiOSSDKv2'
pod 'AWSCognitoSync'

からの pod install

Bridging-Header に以下を追記。

#import "AWSCore.h"
#import "S3.h"

これで完了。

Amazon Cognito の登録

ではまずは準備。Amazon Cognito というAWSサービスをまずは登録する必要がある。

これを登録すると、データ同期の仕組みやユーザー認証の仕組みなども割と手軽に実装できるみたいだ。結構データ同期とか自前で実装すると同期に失敗したときとか戻しが効かなくなったりするので、今度機会があったら使ってみよう。

今回はとりあえずこれを登録して、S3を使える状態にする。登録完了後、AppDelegete に以下を追記。

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // Override point for customization after application launch.
        let credentialsProvider = AWSCognitoCredentialsProvider.credentialsWithRegionType(
            AWSRegionType.USEast1,
            accountId: "You AccountId",
            identityPoolId: "your identityPoolId",
            unauthRoleArn: "Your UnauthRoleArn",
            authRoleArn: "Your AuthRoleArn")
        let defaultServiceConfiguration = AWSServiceConfiguration(
            region: AWSRegionType.USEast1,
            credentialsProvider: credentialsProvider)
        AWSServiceManager.defaultServiceManager().setDefaultServiceConfiguration(defaultServiceConfiguration)
        
        credentialsProvider.getIdentityId().continueWithBlock({ (task) -> AnyObject! in
            var myId = credentialsProvider.identityId
            println("myId is \(myId)")
            self.userDefault.setObject(myId, forKey: Conf.KEY_USER_ID)
            self.userDefault.synchronize()
            return nil
        })

        return true
    }

後半の setDefaultServiceConfiguration の呼び出しは、端末固有のIDを生成するのに使っている。これからアップロードする画像の名前を一意にするために、このIDと時刻を使って名前を生成し、S3へアップロードしよう。

S3 の設定

S3アップロード処理を書く前に、S3の設定がいる。そうしないとうまくいっても Permission Denied になってしまう。

まずはBucket を作成しよう。 ちなみにリージョンはus-east-1 がいいみたい? ここら辺は他のネット情報からなのでもしかしたら東京でも大丈夫。

作成したらそのBucketのパーミッションで EveryOne が UploadとRead できるようにしておく。それに追加して、Create Bucket Policy を選択し、以下のJSONを貼る。

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:DeleteObject",
                "s3:GetObject",
                "s3:PutObjectAcl",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
        }
    ]
}

ここのs3:PutObjectAcl がポイント。これをしないと読み込みの時にその画像を閲覧するということができなくなってしまう。

そしたら準備完了。

アップロード処理の記述

そしたらようやくコードが書ける。

    func uploadToS3(fileUrl: NSURL) {
         //make a timestamp variable to use in the key of the video I'm about to upload
        let date:NSDate = NSDate()
        var unixTimeStamp:NSTimeInterval = date.timeIntervalSince1970
        var unixTimeStampString:String = String(format:"%f", unixTimeStamp)
        println("this is my unix timestamp as a string: \(unixTimeStampString)")
        
        // set upload settings
        var myTransferManagerRequest:AWSS3TransferManagerUploadRequest = AWSS3TransferManagerUploadRequest()
        myTransferManagerRequest.bucket = "YOUR_BUCKET_NAME"
        var myId = userDefault.stringForKey(Conf.KEY_USER_ID)
        self.uploadedFileName = "\(myId!)_\(unixTimeStampString).jpg"
        myTransferManagerRequest.key = self.uploadedFileName
        myTransferManagerRequest.body = fileUrl
        myTransferManagerRequest.ACL = AWSS3ObjectCannedACL.PublicRead
        
        var myBFTask:BFTask = BFTask()
        var myMainThreadBFExecutor:BFExecutor = BFExecutor.mainThreadExecutor()
        var myTransferManager:AWSS3TransferManager = AWSS3TransferManager.defaultS3TransferManager()
        myTransferManager.upload(myTransferManagerRequest).continueWithExecutor(myMainThreadBFExecutor, withBlock: { (myBFTask) -> AnyObject! in
            if((myBFTask.result) != nil){
                println("Success!!")
                // send api?
                let s3Path = Conf.AWS_S3_URL + self.uploadedFileName
                println("uploaded s3 path is \(s3Path)")
                
            } else {
                println("upload didn't seem to go through..")
                var myError = myBFTask.error
                println("error: \(myError)")
            }
            return nil
        })       
    }

これで無事、対象のBucketに画像がアップロードされていることだろう。

終わりに

ちゃんとやるなら、たぶんこれにアップロード中は dispatch_asyncでバックグラウンドで処理させることが必要。あとuploading の表示も必要だ。

よくやるはずの処理なのに、まだSwiftでの記事が少なかったり、Photos フレームワークの資料が少なかったりでもっといいやり方があるのかもしれないので、その場合はシェアしてくださると嬉しいです。

UITableView の Accessory 系が全く表示されない

iOS アプリ開発で意外に詰まるところになるような気がしたのでメモ。UITableView を配置し、右側に矢印出したい。こんな感じ。

f:id:cevid_cpp:20141203105210p:plain

以下のようなコードを書いた。

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("identifier", forIndexPath: indexPath) as UITableViewCell
        cell.textLabel.text = self.settings[indexPath.section][indexPath.row]
        
        cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator
        return cell
    }

しかし、右側に矢印っぽいのは表示されない。

原因

UITableView の Widthが画面を超えてしまっていた。本当は表示されているのに、画面外に表示されていた。

ちゃんと UITableView の Width を画面に収まるようにしたら表示されました。。

やられたやられた。これはわっかりにくいw

AccessoryType, AccessoryView

AccessoryTypeはiOS SDK であらかじめ用意されたテンプレート。 AccessoryView は独自のUIView を継承したものならなんでも追加できるみたい。

この AccessoryView を使うことで、テーブルの右側に Switch を置いたり、 TextView を置いたりできる。これはなかなか便利なので覚えておこう。

初めての iPhone アプリリリース

ついに iPhone アプリをリリースした。既に Android アプリで2万ダウンロード突破中のアプリの iPhone版だ。 2つのプラットフォーム対応ということで、特設サイトも作った。このトップ画像が個人的に気に入っている。w 今のところ日本語、英語、フランス語、ドイツ語に対応。国別のiOSのシェアの高い国に絞って国際化した。

Unleashed Revolution

Swift の勉強を本格的に始めたのが11月の上旬で、そこから3週間でリリースまでこぎつけた感じ。Swiftは本当に素晴らしい。Android 出身の自分には以下の本がとても役に立った。

Migrating to Swift from Android

Migrating to Swift from Android

これで既存のObjective-C のコードでも Swift から呼び出せるようになって、外部ライブラリも使えるようになった。あとは細かいSDKの使い方を作っていく過程で学んでいった感じ。ちなみに今回は以下のライブラリを利用した。

platform :ios, "8.0"
pod 'FontAwesomeKit/IonIcons'
pod 'MRProgress'
pod 'MMPickerView', '~> 0.0.1'
pod 'MessageBarManager'

それに加えて、 Alamofire っていうHTTP通信ライブラリ。これはSwiftのライブラリだからCocoaPod には書けなかった。

初めてで一番苦労したのは、リリース周り。 iOS アプリのリリースの準備って本当に複雑で、かなり意味不明だった。 まぁ一回がんばってなんとか動かせればそのあとは同じ作業だからなんとかなるかもしれないが、あれは Apple は改善すべきだと思う。 あと最終ビルドの所の意味不明なエラーで半日は費やしたな。そこらへん Android にはない辛さだった。

まぁともかく、一般的に iPhone アプリの方が多くダウンロードされるって言われているので、今後このアプリがどのように成長していくか、楽しみ!


っと、このアプリは練習用の位置づけ。これからはコネクシィのiPhone アプリを本気で作っていく! ただいま絶賛画面設計中。

Swift で xib でレイアウトされたモーダルを出す

iOSのモーダルは Android でいうActivity#startActivityForResult みたいなやつ。一旦別画面で何かやってもらって、その結果を呼び出し元で取得するようなケースで使える。

ネットで調べてもサンプルがObejctive-C ばかりだったので、Swift での実装方法をまとめてみる。

ViewController の作成

NewFile から Cocoa Touch Class を指定し、親がUIViewController, Also create XIB file にチェックを入れる。

f:id:cevid_cpp:20141115135908p:plain

ModalViewController のセットアップ

作成したModalViewController は以下のような感じになる。

protocol ModalViewControllerDelegate {
    func modalDidFinished(text: String)
}

class ModalViewController: UIViewController {
    
    var delegate: ModalViewControllerDelegate! = nil
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

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

    @IBAction func onCloseed(sender: AnyObject) {
        self.dismissViewControllerAnimated(true, completion: nil)
        self.delegate.modalDidFinished("closed")
    }
}

ここで protocol で ModalViewControllerDelegate を定義しているのは、これを呼び出し元ViewController のコールバックオブジェクトとして登録するためだ。 このコールバックオブジェクトを iOSの世界では delegate と呼ぶらしい。

今回はこのプロトコルを実装した 呼び出し元ViewController を delegate として登録し、終了時に modalDidFinished を呼び出して値を呼び出し元に返している。

呼び出し元 ViewController

モーダル呼び出し元の下準備をする。

class ViewController : UIViewController, ModalViewControllerDelegate {
   
    let modal = ModalViewController(nibName: "ModalViewController", bundle: nil)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.modal.delegate = self
    }

    @IBAction func onModalOpen(sender: AnyObject) {
        self.presentViewController(modal, animated: true, completion: nil)
    }

    func modalDidFinished(text: String) {
        println(text)
    }
}

self(ModalViewControllerDelegateオブジェクト) を modal の delegate に代入する。

ModalViewControllerDelegatemodalDidFinishedを実装する。ここが Modal呼び出し後に呼ばれるコールバックだ。

ModalViewController 初期化時に nibName を指定しないと xib ファイルで作った View が描画されずに真っ黒になるので注意。

終わりに

両者の間で色々と実装しなければいけない所が多々あって最初はわかりにくいかもしれないが、これは Android アプリ開発でも同じようなことをよくやるケースが多いので、マスターしておきたいところ。

UITableViewをカスタマイズして表示する流れ

これは今後何度も使いそうになるのでメモ。

今回やることは、UITableViewの各セルの内容を独自のUIにしつつ、そのデータをネットワーク経由で取得するというもの。 Swift, Xcode 6.1。

UITableView を設置

いつも通りStoryBoardからUITableViewとUITableViewCellをそれぞれ配置。

UITableView の Outlets である dataSourcedelegate のそれぞれの向き先を対象のViewControllerへ。 さらに UITableView の Outlets も ViewController に登録する。

f:id:cevid_cpp:20141114020027p:plain

カスタム UITableViewCell の作成

File -> New で Cocoa Class を指定。 名前を「CustomViewCell」にする。継承元に UITableViewCell を指定。すると、swift ファイルと xib ファイルの2つができる。

xib ファイルには既に UITableViewCellが設置されているのでその中にカスタマイズしたセルの内容を配置していく。配置したそれぞれのUI Objectは、 Outlet で CustomViewCell に登録しておく。

f:id:cevid_cpp:20141114020427p:plain

StoryBoard 内でCustomViewCellを指定

StoryBoard の UITableViewCell の CustomClass に CustomViewCell を指定する。また、identifier を myCell とする。

ViewController でコーディング

いよいよコーディングに入る。以下が基本形だ。

import UIKit

class BoardViewController : UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    @IBOutlet weak var mTableView: UITableView!
    
    var posts: [AnyObject] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // custom view
        var nib  = UINib(nibName: "CustomTableViewCell", bundle:nil)
        mTableView.registerNib(nib, forCellReuseIdentifier:"myCell")
        mTableView.estimatedRowHeight = 100.0
        mTableView.rowHeight = UITableViewAutomaticDimension
    }
    
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return posts.count
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCellWithIdentifier("myCell") as? CustomTableViewCell
        var post = posts[indexPath.row] as Post
        cell?.message.text = post.content

        return cell!
    }
    
    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        tableView.deselectRowAtIndexPath(indexPath,animated: true)
    }
}

CustomViewCell に表示する内容はエンティティとして作成する。上記例だと Post。この配列をViewController内でフィールドとして保持し、その配列と対象メソッドの indexPathを元に表示すべきエンティティを取得し、Cellに反映するといった流れだ。CustomTableViewCell への反映は上記コードの内, cell?.message.text = post.content にあたる。

UITableViewAutomaticDimension を指定すると、高さがコンテンツに応じて動的に変化するようになる。

ネットワーク経由での取得

さてこれで実行しても中身が無いので、空のテーブルが表示されるだけだ。ネットワークを通じてJSONデータを取得しよう。

今回はHTTP通信のライブラリとして Alamofire を利用する。インストール方法等は別途参照

以下のような感じになる。

import Alamofire


class BoardViewController : UIViewController, UITableViewDataSource, UITableViewDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        // ....
        fetchData()
    }

    func fetchData() {
        Alamofire.request(.GET, "http://sample.com/posts.json")
            .responseJSON { (_, _, JSON, _) in
                var posts = JSON?["posts"]
                if let postArray = posts as? Array<Dictionary<String, AnyObject>> {
                    for post in postArray {
                        let p = Post(
                            content: post["content"] as String
                            //...
                        )
                        self.posts.append(p)
                    }
                }
                // update
                self.mTableView.reloadData()
        }
    }
}

取得してフィールドのpostsにデータが入り終わった後にself.mTableView.reloadData()するのがポイント。これで非同期にテーブルが更新できる。

Swift と CococaPods の連携

Swift に移ってもCocoaPods 使うのが一般的なのかな?とりあえず外部ライブラリにはCocoapod を入れたインストール方法が書かれているから、これ導入しないとObj-Cの外部ライブラリは利用できないから入れざるを得ない。

今後、Swift プロジェクト用の外部ライブラリができてきたら、これ使わなくなるのかな。 Alamofire のインストール方法を見てみると、Cocoapodを利用した方法が書かれていなかったからね。

前置きが長くなった。掲題の件、始めます。

実行手順

こちらに全てまとまっていますw

SwiftでCocoaPodsを使う - Qiita

苦労した点

以下のエラーが出て全く進めませんでした。

Cocoapods ld: library not found for -lPods-Projectname

上記リンクをよくよく見てみると、

インストール後はxcworkspaceを開く

とあります。確かにプロジェクト直下を見てみると.xcodeproj.xcworkspaceの2つがありプロジェクトを作った状態だと.xcodeprojを読み込んでいるみたいです。一度Xcodeを終了し、.xcworkspaceをダブルクリックして起動したらうまくいきました。

Cocoapod を使っている人なら当たり前のことみたいっす。。 結構詰まった。

XCode6 で preview をする

初めての iOS ネタ。これからしばらくSwift勉強してく。

さて、以下の本を読んでいて、AutoLayout をプレビューで表示できるということで試してみて詰まった。

Migrating to Swift from Android

解決策

プレビューにするには、Assistant Editor にしなければならないようだ。

f:id:cevid_cpp:20141030190743p:plain

ピンクで囲った部分の内、右上のAssistant Editor で2画面にし、その後その下にあるManual から previewを選択すれば、無事プレビューが表示される。

f:id:cevid_cpp:20141030190920p:plain

さらに画面左下の+ を選択すれば、様々な画面サイズでどのようにAutolayoutが適用されるのかが確認できる。

毎回アプリを起動して修正はとてもめんどいので、こうやってプレビューができると便利。