- 投稿日:2020-12-04T23:53:39+09:00
0から始めたiOSアプリ開発で学んだこと
はじめに
Qiitaアドベントカレンダー初参加です。
プログラミング未経験から始めてAppStoreリリースまで持っていった個人開発アプリについて書かせていただきます。
私は非エンジニアで、2020年5月下旬にSwiftで人生初Hello, WorldしてAppStoreにver1.0.0をリリースしたのが6月、そこから少しずつ改良を加えていって11/30にver3.0.0をリリースしました。アプリ制作の理由、きっかけ
吃音サポートアプリとして、ゆっくりもーどを作りました。
吃音症を説明するのは難しいのでWikipediaより引用します。吃音(きつおん、英: stuttering,stammering)とは、言葉が円滑に話せない、スムーズに言葉が出てこないこと。「発語時に言葉が連続して発せられる(連発)」、「瞬間あるいは一時的に無音状態が続く(難発)」「語頭を伸ばして発音してしまう(延発)」などの症状を示す[1]。WHO(世界保健機関)の疾病分類ICD-10では、吃音であり[2]、米国精神医学会の以前のDSM-IVでは吃音、2013年のDSM-5(『精神障害の診断と統計マニュアル』第5版)では 小児期発症流暢症/小児期発症流暢障害(吃音)と、と障害が併記され、英語表記で、Childhood‒Onset Fluency Disorder (Stuttering) の診断名である。非流暢発話状態のひとつ[3][4]。
端的に言うと、「おおお、お...おはようございます..」「.........おはようございます」のように何らかの形で言葉が吃ってしまう症状です。
私自身難発の症状を持っており、かなり人生や実生活上で辛い思いをしてきました。
そのような中、ある時ふと思い立ってAppStoreで吃音関係のアプリを一通りダウンロードしてみたところ、以下の課題を見つけました。
- 有料アプリが多い(無料でもアプリ内課金をしないとまともに使えないアプリも多い)
- 単機能で比較的シンプルなアプリが多い
- 外国製で日本語対応していない(一応していても上手くローカライズできておらず日本語が不自然)
- 単純に数が少ない
自分が吃音関係で「あったらいいなぁ」と望んでいたアプリは、
- 完全無料
- 日本語話者専用
- 吃音の症状の状態管理ができる
- 人によって症状は違う(その時のメンタルによって吃る頻度等が変わる、人によって〜行が言いにくいor〜段が言いにくい)ので、"何となく分かっている"自分特有の症状の特徴を管理したい
のようなアプリだったので、現状iPhoneユーザーである私のニーズを満たすアプリは存在していなかったのです。
そこで、「ないなら自分で作ってみよう」と思い立ち、アプリリリースを目標に0からiOSアプリを独学で勉強し始めました。以上がアプリ制作の理由、きっかけです。アプリ概要&使用技術
アプリ概要
最新リリースのver3.0.0では以下のことができます。
- 一定間隔で音or振動を起こし、そのリズムを使って音読練習ができる機能
- 一定間隔で様々なタイプの振動を起こす機能(こちらは注文時や面接時にリズムを取る用で、画面ロックできるようにしました)
- 吃りやすさとその時の吃音のメンタルの調子を線グラフにし、いわゆる"吃音の波"を管理する機能
- 人によって違う言いにくい言葉をリストに追加していき、ユーザーごとに苦手な五十音の行・段を円グラフで表すことができる機能
使用技術
アーキテクチャはMVC, バージョン管理はGitとGitHubです。
個人開発なのですが、実務を見据えてブランチモデルはGitFlowを参考にしました。具体的にはmaster, developの二本柱を元に、機能追加の場合はdevelopブランチからfeature/~を生やし、タスクが完了したらdevelopに合流させます。機能改善の場合も同様にdevelopブランチからimprovement/~を生やし、目処がついた時点で同じようにdevelopブランチに合流させます。基本masterブランチには触らず、Appleに審査を申請する最終段階でdevelopブランチをmasterブランチに合流させ、masterブランチのものを審査に出す という形です。
また、機能追加や機能改善などのタスクは全てGitHubのプロジェクトに(自分で)issueを作り、そのissueに則る形で開発を進めていきました。
あとver3.0.0リリース前には頑張ってベータ版配布にも手を出してみました。
機能 ライブラリ アプリ内広告 AdMob グラフ描写 Charts アニメーション Lottie-iOS キーボード調整 IQKeyboardManager お問い合わせ MessageUI
機能 mBaas ユーザー管理, ログイン機能 Firebase Auth データ保存 Firebase Firestore アナリティクス Firebase Analytics クラッシュ情報解析 Firebase Crashlytics ベータ版配布 Firebase AppDistribution 頑張ったこと
割と色々あるのですが、ここは的を絞って2点に集約させます。
- 小さく産んで大きく育てる
- 生のユーザーの声を聞きにいく
小さく産んで大きく育てる
(なんかこんなことどこかの大臣が言ってたな...)
私は非エンジニアで全くのプログラミング未経験から始めたので、まず自分に期待することをやめました。
初めてAppStoreにver1.0.0をリリースした時点ではGit管理もしておりませんでしたし、アプリにログイン機能なども実装しておりませんでした。
まずは最低限超えるべきハードルを自分の中で設定し、プラスアルファで実現したいことはその最低限のハードルを超えた後に取り組むLv.2タスクとして設定するよう心がけました。
こうすることでモチベーション維持につながりますし、段階的にレベルアップしていけるので挫折にもなりにくいです。生のユーザーの声を聞きにいく
吃音者が定期的に集まる自助団体(NPO法人)が各都道府県にあります。
私もたまに参加しているのですが、開発者としてフィードバックを得るために、リリース後の段階でアプリ紹介をしにお邪魔させてもらうことがありました。基本的にアプリ開発者は、アナリティクスの動向やストア評価を通してユーザーと間接的に接点を持つと思います。
私の場合は、潜在的にユーザーになり得る特定集団が身近に存在していたこともあり、「これ以上的確なフィードバックを得る手段はおそらくないであろう」という思いから、ボロクソ言われるのを承知の上、決死覚悟で飛び込みました。
まぁ案の定ボロクソ言われることもありましたが、反面ポジティブな反響をいただくことも多々あり、今ではやってよかったとつくづく感じています。学んだこと
これも色々ありますが、特に印象に残ったこととしてウォークスルー画面の重要性について書きます。
初めて吃音の自助団体でアプリ紹介を行なった時、アプリはver1.0.0リリース直後でした。
その頃は機能も限定的だったため、ウォークスルー画面の必要性を感じず、全く実装していない状態でした。しかしいざダウンロードして使ってもらう段階になると、特にスマホの操作に慣れていない比較的ご年配の方々を中心に
「どう操作すればいいのか分からない」「ここを押すと何ができるのか分からない」
といったご指摘が挙がりました。これは当然で、個人開発者は自作アプリの機能について100を知っているが、ユーザーは0しか知らないという情報の非対称性が存在することに私が気付いていなかったことが原因です。スマホネイティブではないご年配の方々なら尚更ですね。
「このアプリは何のためのアプリなのか」「このアイコンは何を表すのか」といった、橋渡し的な役割を果たすのがウォークスルー画面です。
「ウォークスルー?別にあってもなくてもどっちでもいいでしょ」という認識だったのですが、この経験を元に、ウォークスルーのようなものは絶対作るべきだと考えが180度変わりました。今後の課題
RxSwiftを使ってMVVMに書き換えてみる
今RxSwiftを現在進行形で勉強中なのでもう少し時間はかかりそうなのですが、直近の目標として定めています。
ユーザー数を増やす
こればっかりは努力で何とかできるものではないので何とも言えませんが、機能追加や改善・SNSでアピールするなどして頑張っていこうと思っています。
引用
https://ja.wikipedia.org/wiki/吃音症
その他
実は今日誕生日です?
- 投稿日:2020-12-04T22:53:46+09:00
【Swift】Enter押したときとキーボード領域以外をタッチしたときにキーボードを閉じる
どういうことか
たとえばiPhoneでユーザー名入力フォームとかをタッチすると『入力モード』になってキーボードが出てくる。で、タイトルにある行動をしたらキーボードを閉じたい。
はじめに
ViewControllerクラスでUITextFieldに対して『デレゲート』の呪文を使い、我らがアップルのこしらえてくれたメソッドを使えるようにしておく必要がある。
class ViewController: UIViewController,UITextFieldDelegate { // 中略 }閉じる
定型文として使っていいと思う。
// タッチでキーボードを閉じる override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { view.endEditing(true) } // リターンキーを押したときにキーボードを閉じる func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true }
- 投稿日:2020-12-04T21:29:07+09:00
【備忘録】Swift 実践入門について 第6章
Swift 実践入門のまとめ。
分からない部分の抜粋も記載し、解決できたら随時更新していきます。
なお、ここに記載している以外でも「わけわからん…」となっている部分も多々ありますが、
今は必要ない、と言い聞かせて飛ばしています。第6章
関数とクロージャ 複数の工程や処理などを1つにまとめたもの。切り出すことで再利用可能
理解度:70%くらいか6-2 関数
func double(_ x : Int) -> Int { //_は呼出時にdouble(x : 2)ではなくdouble(2)って書ける
return x * 2
}
double(2) //4引数…関数への入力。「引数名:型」を指定する。型の後に= “~”など入れればデフォルト引数になる。
呼出時に具体的な値を入れる。6-3 クロージャ…関数と同じく再利用可能な1つのまとまり。関数はクロージャの一種。
クロージャはその処理を変数、定数、関数などに渡すことができる。
・定義方法
var closure: (String) -> Int
closure = { (string: String) -> Int in
return string.count * 2
}
closure(“abc”) //6引数…複数の引数を取り、引数名を$を使って簡略化できる。$0で最初の引数、$1で2番目の引数を表す。
キャプチャ…クロージャ自身が定義されたスコープ内の遠陬や定数をスコープ外でも使える機能。今はまだどのように使っていくかイメージが沸かない…
引数としてのクロージャ…引数としてクロージャを利用できるが、スコープ外で利用する場合「@escaping」の記述が必要。このようにクロージャを関数に渡すと属性が必要になるが、ここはまだ理解できていない。
- 投稿日:2020-12-04T19:41:27+09:00
SwiftでCodableの実装を爆速でする方法
はじめに
- サーバとの通信処理ではResponseの値をmappingやparseして、Swiftで扱いやすいように変換する処理をよく書きます。JSONからSwiftの値に変換するときにはCodableを使うと楽に実装できます。そんなCodableの実装もAPIが増えるごとにたくさん実装、メンテする必要がでてきます。そんな実装を爆速でするための方法について、まとめました。
JSONの型とSwiftで使う型の変換
{ "name": "Tanaka", "age": "26", "gender": "0" }
- このようなJSONあるときに実装で扱いやすい型に変換するときに苦労した経験があります。(String <-> Intの変換など) Codableで逐次変換処理を書くと可読性が落ちて、メンテし辛いコードが出来上がります。それを克服するためにDTOを使って、可読性の高いコードにしてみます。
DTOを使ったCodableの処理
DTOとは
Data Transfer Object
を省略したものです。Layer間の値の受け渡しに使います。
このJSONをSWiftの値に変換する処理を書いてみます。
{ "name": "Tanaka", "age": "26", "gender": "0" }
- before
- 普通に書くと
try
の処理と型変換の処理がたくさん書くことになりますimport UIKit let json = """ { "name": "Tanaka", "age": "26", "gender": "0" } """.data(using: .utf8)! enum Gender: Int { case male = 0 case female case unknown } struct Person { let name: String let age: Int let gender: Gender enum CodingKeys: String, CodingKey { case name case age case gender } } enum DecodableError: Error { case ageError case genderError } extension Person: Decodable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) let ageValue = try container.decode(String.self, forKey: .age) guard let age = Int(ageValue) else { throw DecodableError.ageError } self.age = age let genderString = try container.decode(String.self, forKey: .gender) guard let genderValue = Int(genderString), let gender = Gender(rawValue: genderValue) else { throw DecodableError.genderError } self.gender = gender } } let decoder = JSONDecoder() let person = try decoder.decode(Person.self, from: json)
- after
DTODecodable
を使って、tryの処理を書かなくていいようにします。DTOではJSONで宣言されている型の通りに変換することで値のparseのみ行います。Entity側ではDTOの値を元にEntityで必要な型に変換します。Entity側ではtryの処理を意識しないのでコードの可読性が上がりました。import UIKit protocol DTODecodable: Decodable { associatedtype DTO: Decodable init(dto: DTO) throws } extension DTODecodable { init(from decoder: Decoder) throws { let dto = try DTO(from: decoder) self = try Self.init(dto: dto) } } let json = """ { "name": "Tanaka", "age": "26", "gender": "0" } """.data(using: .utf8)! enum Gender: Int { case male = 0 case female case unknown } struct Person { let name: String let age: Int let gender: Gender enum CodingKeys: String, CodingKey { case name case age case gender } } extension Person: DTODecodable { struct DTO: Decodable { let name: String let age: String let gender: String } init(dto: DTO) throws { self.name = dto.name guard let age = Int(dto.age) else { throw DTODecodableError.ageError } guard let genderValue = Int(dto.gender), let gender = Gender(rawValue: genderValue) else { throw DTODecodableError.genderError } self.age = age self.gender = gender } enum DTODecodableError: Error { case ageError case genderError } } let decoder = JSONDecoder() let person = try decoder.decode(Person.self, from: json)
- 実装イメージ
Codableのコードを生成する
ここでは2つのツールについて紹介します。2つともJSONを元にCodableの実装を生成するツールです。
- WebでJSONからCodableの実装が生成されます
- JSONtoCodableを使うとCodableの実装が生成されます
まとめ
- DTOを使うことでCodableの可読性があがります。
- 生成ツールを使うとさらに爆速で開発できます
参考リンク
- 投稿日:2020-12-04T19:14:01+09:00
HTTP通信とは
巷に良記事がいっぱいありますが、自分自身の備忘録目的でまとめます。
まずはじめに通信プロトコルについて
日本人がアメリカ人に日本語で話しかけても話は成立しませんよね。
そのため共通の言語で話す必要があります。
ネットワークやコンピューター間でもそれは同様で
コミュニケーションするのに同じ言語でやりとりしましょうよ、という約束事を通信プロトコルといいます。TCP/IP
インターネット普及に伴い、現在事実上の標準(デファクトスタンダード)となっているのは
TCP/IPプロトコル(Transmission Control Protocol/ Internet Protocol)です。
TCP/IPは以下の4層で構成され、トランスポート層(TCP, UDP)とインターネット層(IP)が重要な役割を担ってることから、TCP/IPプロトコルというそうです。アプリケーション層:HTTP, FTP, TELNET, SMTP, POPなど
トランスポート層:TCP, UDP
インターネット層:IP
ネットワークインタフェース層:PPP,イーサネットなどTCP/IPの代表的なプロトコル
HTTP(Hyper Text Transfer Protocol):サーバとクライアント間で、データを送受信する際の決まりごと
FTP(File Transfer Protocol):サーバーとクライアント間で、ファイルを送受信する通信の決まりごと
Telnet:ネットワークに接続された機器を遠隔操作するために使用する決まりごと
SMTP(Simple Mail Transfer Protocol):メール送信・配信に使用されるきまりごと
POP(Post Office Protoco):メール受信に使用される決まりごとHTTP(S)通信について
私がWebAPIを使用するにあたって特に勉強しなくてはいけなさそうなのはHTTP(S)通信ぽそうなので
HTTP(S)通信について触れていこうと思います。HTTPとHTTPSの違い
HTTPよりHTTPS通信のほうが安全みたいです。
HTTPSはログインや支払いなどで使用されているようですが
最近は常時使用しましょうみたいな流れがあるらしい??詳しくは以下のサイトをご覧ください。
HTTPとHTTPSの主な違いは通信内容が暗号化されていないか、されているかの違いです。HTTPSに対応しているサーバでは、ホームページのデータをやり取りする際に暗号化されて通信するようになります。通信内容を暗号化することで、安全にWebの情報をやり取りすることができます。
引用元:httpとhttpsの違いとは?知らないと恥ずかしいWEBセキュリティの基本HTTP(S)とアプリの関係
WebAPIを使用する際には
アプリ<「欲しい情報がある!」
↓HTTPリクエストを出します
サーバ<「えええ。しょうがない。あげるよ〜」
↓HTTPレスポンスが返ってくる
アプリ<「ありがとー
この情報使うね!」
基本的にはこの関係らしい。
リクエスト
→
![]()
HTTPリクエストには種類(HTTPメソッド)がある。
「GET」「POST」「PUT」「DELETE」以外にもHTTPメソッドはいくつかありますが、今回この4つを取り上げて說明する理由は、Web開発をする上で比較的多く使われるメソッドであるためです。
特にAPIを使ったWebサービス・アプリ開発を行うときによく使います。
引用元:Web開発でよく使う4つのHTTPメソッド【REST API】なるへそなるへそ
(もう死語だったりするのだろう。)
GET:それ見せてよ!ちょうだいよ!
POST:これ登録よろしく!
PUT:これ更新よろしく!
DELETE:これ消しといて!
あと個人的によく聞くけど知らなかったのは
PATCH:これ更新よろしく!
PUTとPATCH、「これ更新よろしく!」なんてシンプルに書いたけど、きっとなにか違うんだろうな。
そのうち調べよう。とにかくいろんなメソッドがあるみたいです。
レスポンス
→
![]()
レスポンスのデータ形式には
HTML
JSON
IMG
XMLなどがあるらしい。
一昔前はXMLが多かったようですが、現在はJSONが主流のようですJSONとは?
JSONとは?データフォーマット(データ形式)について学ぼう!同期・非同期通信
よく目にした言葉だけど、まぁ今んとこ関係ないか、、、と放置していたこのワードたち。
同期通信はデータ通信のリクエストを出してからレスポンスが来るまでほかの処理を行わずにレスポンスを待ち続けるが、非同期通信ではレスポンスを待っている間にほかの処理を行える。ほかの処理を行っている際に、レスポンスを受信すると受信処理を実行する
引用元:@ITこのサイトも図がついていて分かりやすかった。
あなたはHTTP通信の説明が出来ますか?同期・非同期の仕組みを解説!ふむふむ
一見良さげな非同期通信はデータ量が増えてしまうんですね。
それ以外にもメリット、デメリットあるみたいですが
これについては実際に使用してみて徐々に理解していきたいと思います。SwiftでHTTP通信をする方法
URLSession
Alamofire
APIkitなどなど
いろいろな方法がありそうです。
これらの違いはいまのところよくわかっていません。あとは手を動かしながら学んでいこうと思います
後半にいくにつれまとめが雑になっているきがしないでもない。笑
- 投稿日:2020-12-04T16:07:41+09:00
APIとは
APIとは??
![]()
巷でよく聞くAPI。
触れてみたい技術に一つでしたが、まずはじめにAPIとは??状態なのでそこから学んでいきます。
間違っていたらコメントいただけますと幸いです。Application Programming Interfaceの略で
ソフトウェアの一部を外部に公開することで、第三者がそのソフトウェアの機能を共有して使用できる、というものみたいです。
身近にある例としては、とあるサービスを使用する時にGoogleやFacebookでログインすることができる、というのものがありますよね。これもAPIのおかげのようです。APIはWeb上に公開されているので、通信を利用するのが一般的なようで、WebAPIともいうらしいです。
はて、、、通信ですと??
![]()
アプリケーションの区別の仕方はいろいろあるようですが、二つに大別することができます。
・スタンドアローン
・クライアントサーバスタンドアローンは、通信なしでアプリケーション単体で動くもの。
クライアントサーバは通信が必要なアプリケーションのこと。
昨今では当たり前のようにサーバとの連携を行うアプリが主流のようです。サーバとの連携というのは
Webサイト閲覧を例にすると
「なんやこの記事、知っとるわい次の違うサイトみてやるわい!」
↓
PC・スマホのブラウザ(クライアント) 「りょ!かえたるわい。次のURLおくるわ。このページの情報くれや!」
↓リクエスト
サーバ 「ほいよ、このサイトの情報あげるわ」
↓レスポンス
PC・スマホのブラウザ(クライアント) 「ほいよ」
↓
「これよこれー」
という一連の流れのように
クライアントがリクエストをサーバになげて、サーバからレスポンスをうけとり表示するということをやっています。APIではブラウザの代わりにプログラムが「この情報が欲しいよ!」とインターネット経由で機能を公開しているサーバに対してリクエストを投げて、プログラムが欲しがっている情報を返してあげる、ということをやっているようです。
帰ってくるデータの形式は今はJSONが多いようです。なんとなくAPIがなんなのか分かったところで
Swiftでの通信をしたことがないので、これについても勉強しなくてはいけなさそうですね。
こちらについても備忘録がてら別記事にまとめたいと思います
- 投稿日:2020-12-04T15:39:58+09:00
Elixir から Swift 5.3のコードを呼び出す方法(Autotoolsを使って / Apple Silicon M1チップにも対応)
この記事はElixir Advent Calendar 2020の10日目です。
昨日は @kobae964 さんの「Rustler で行儀の良い NIF を書く」でした。
さて,Apple Silicon M1チップの性能が明らかになるにつれて,今後,ElixirエコシステムからMacの潜在能力をフル活用することが求められてくるように思います(というか,是非活用したい)。そこで,この記事はElixirからSwift 5.3のコードを呼び出す方法について紹介しています。
Autotoolsを使ってObjective-CやSwiftのヘッダファイルや関数の存在判定などもできるようにする方法も示していますので,Elixir開発者だけでなく,iOSネイティブアプリやMacアプリの開発者にも部分的には有用かと思います。しかも,Apple Silicon M1チップ搭載のMacでも動作検証をしています!
この記事のGitHubレポジトリは https://github.com/zacky1972/swift_elixir_test です。下記に同様のもののつくりかたを詳説しています。
注意点: この記事はApple Silicon M1チップ搭載のMacに概ね対応していますが,(1)ErlangをARMネイティブでビルドして(2)かつRosetta 2モードでターミナルを起動した場合には,NIFのロード時にアーキテクチャの不一致によるエラーが発生し,NIFを実行できないという問題があります。この問題は,従来のNIFプログラム全般で発生する可能性のある問題だと認識しています。現在,さらに調査を進めて問題の解決に当たっているところです。
まずは
mix new
まずは
mix new
でプロジェクトを作ります。プロジェクト名はswift_elixir_test
としました。% mix new swift_elixir_test * creating README.md * creating .formatter.exs * creating .gitignore * creating mix.exs * creating lib * creating lib/swift_elixir_test.ex * creating test * creating test/test_helper.exs * creating test/swift_elixir_test_test.exs Your Mix project was created successfully. You can use "mix" to compile it, test it, and more: cd swift_elixir_test mix test Run "mix help" for more commands. %書かれている指示に従って,進めます。
% cd swift_elixir_test swift_elixir_test % mix test Compiling 1 file (.ex) Generated swift_elixir_test app .. Finished in 0.03 seconds 1 doctest, 1 test, 0 failures Randomized with seed 231147 swift_elixir_test %ここで自動生成されたコードを修正しておきます。
lib/swift_elixir_test.exdefmodule SwiftElixirTest do @moduledoc """ Documentation for `SwiftElixirTest`. """ @doc """ Hello world. ## Examples iex> SwiftElixirTest.hello() :world """ def hello do :world end endこれを次のようにします。(
hello
関数を削除)lib/swift_elixir_test.exdefmodule SwiftElixirTest do @moduledoc """ Documentation for `SwiftElixirTest`. """ endそして自動生成されたテストコードも修正します。
test/swift_elixir_test_test.exsdefmodule SwiftElixirTestTest do use ExUnit.Case doctest SwiftElixirTest test "greets the world" do assert SwiftElixirTest.hello() == :world end endこれを次のようにします。
test/swift_elixir_test_test.exsdefmodule SwiftElixirTestTest do use ExUnit.Case doctest SwiftElixirTest endここで,
mix test
を実行して,テストが0個になることを確認します。swift_elixir_test % mix test Compiling 1 file (.ex) Finished in 0.02 seconds 0 failures Randomized with seed 71756 swift_elixir_test %ここまでできたら,
git
に登録しましょう。swift_elixir_test % git init swift_elixir_test % git add -A swift_elixir_test % git commit -m "initial commit" swift_elixir_test % git branch -M mainAutoconf の初期設定
ここではAutoconfを使ってビルド時の環境を認識するようにします。ただしAutoconfで生成した環境認識スクリプト
configure
は並列ビルドできないという欠点があるため遅いという難点があります。せっかく並列実行に強いElixirなので,将来はElixirで並列実行できるようにしたいですが,将来課題とします。まず空の
configure.ac
を作成します。configure.acdnl Process this file with autoconf to produce a configure script AC_INIT()
dnl
で始まる行はコメント行です。AC_INIT()
はautoconf
に初期化を指示します。パラメータを与えるのが普通なのですが,いったん無しで実行します。この状態で
autoconf
を実行します。もしHomebrewを使っているならあらかじめ次のコマンドを実行しておきます。swift_elixir_test % brew install autoconf
では
autoconf
を実行しましょう。swift_elixir_test % autoconfそうすると次のファイルが生成されます。
autom4te.cache configure
.gitignore
に下記を追記してgit
が追加ファイルを無視するようにしましょう。.gitignore# For Autoconf /autom4te.cache/ # For configure /configure
configure
を実行してみます。swift_elixir_test % ./configureすると
config.log
が生成されるので,これも.gitignore
に下記を追記して無視するように設定します。.gitignore# For configure /config.log /configure
elixir_make
でconfigure
を呼ぶ
elixir_make
を使うとmix compile
をしたときにmake
を用いたビルドをしてくれます。Elixirの作者のJosé Valim(ジョゼ・ヴァリム)にelixir_make
を使ってconfigure
を呼び出す方法を教えてもらいました( https://github.com/elixir-lang/elixir_make/issues/42 )ので,紹介したいと思います。まず,
mix.exs
を書き換えてelixir_make
をインストールします。mix.exs
の下記の部分がインストールするライブラリを指定する部分です。mix.exsdefp deps do [ # {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} ] endこれを次のように書き換えます。
mix.exsdefp deps do [ {:elixir_make, "~> 0.6.2", runtime: false} ] endそれから次のコマンドを実行します。
swift_elixir_test % mix deps.getこれで
elixir_make
がインストールされました。次に
mix.exs
に次のような関数を追加します。System.cmd("#{File.cwd!()}/configure", []) で
./configure` を実行することになります。mix.exsdefp configure(_args) do System.cmd("#{File.cwd!()}/configure", []) endそして
mix.exs
の下記の部分がプロジェクト情報なのですが,これを書き換えます。mix.exsdef project do [ app: :swift_elixir_test, version: "0.1.0", elixir: "~> 1.11", start_permanent: Mix.env() == :prod, deps: deps() ] end次のようにします。
mix.exsdef project do [ app: :swift_elixir_test, version: "0.1.0", elixir: "~> 1.11", start_permanent: Mix.env() == :prod, deps: deps(), compilers: [:elixir_make] ++ Mix.compilers, aliases: [compile: [&configure/1]] ] endこのようにすると,
make
を呼び出す代わりに./configure
を呼び出します。mix compile
を実行してエラーがないことを確認しましょう。(なお,この時点ではmake
を呼んでいません)Automakeでライブラリを生成
次にAutomakeの設定をします。
ElixirからSwiftを呼ぶために,ElixirからCで生成したネイティブコードをリンクして呼出すNIFを利用します。NIFで呼出すためには動的ライブラリとして生成しますので,Automakeで動的ライブラリを生成するように設定する必要があります。
Cのソースコードを
native/libnif.c
に配置しましょう。次のコマンドを実行します。swift_elixir_test % mkdir -p native
native/libnif.c
を作成します。native/libnif.c#include <erl_nif.h>
erl_nif.h
というのはNIF APIのヘッダファイルです。
Makefile.am
を次のように作成します。Makefile.amAUTOMAKE_OPTIONS = subdir-objects ACLOCAL_AMFLAGS = -I m4 lib_LTLIBRARIES = priv/libnif.la priv_libnif_la_SOURCES = native/libnif.c priv_libnif_la_CFLAGS = $(CFLAGS) $(ERL_CFLAGS) priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic説明は次のとおりです。
AUTOMAKE_OPTIONS = subdir-objects
でサブディレクトリにソースコード等を配置することを指定します。ACLOCAL_AMFLAGS = -I m4
はaclocal
で設定した値を読み込みます。lib_LTLIBRARIES = priv/libnif.la
はビルドしたいライブラリを指定します。拡張子が.la
ですが,Automakeでは一律にこのように指定するので,心配しないでください。priv_libnif_la_
というのはpriv/libnif.la
に対応するオプションであることを示す接頭辞です。
priv_libnif_la_SOURCES
でソースコードを指定します。ここではnative/libnif.c
をコンパイルします。priv_libnif_la_CFLAGS
でコンパイルする時のCFLAGS
の値を決めます。ここでは,CFLAGS
とERL_CFLAGS
の値を設定します。ERL_CFLAGS
は後でconfigure.ac
の中で設定しますが,Erlang が提供するヘッダファイルの情報などを定義します。priv_libnif_la_LDFLAGS
で同様にリンクする時のLDFLAGS
の値を決めます。ここでは,LDFLAGS
とERL_LDFLAGS
の値を設定します。ERL_LDFLAGS
は,ERL_CFLAGS
と同様です。動的な共有ライブラリを生成するために,-shared
-module
-export-dynamic
を指定します。.so
というようにバージョン番号を記載しないようにするために-avoid-version
を指定します。そして
configure.ac
を次のように変更します。configure.acdnl Process this file with autoconf to produce a configure script AC_INIT([priv/.libs/libnif.so], [1.0]) AC_CONFIG_MACRO_DIRS([m4]) AM_INIT_AUTOMAKE([-Wall -Werror foreign]) AC_ARG_VAR([ELIXIR], [Elixir]) AC_ARG_VAR([ERL_EI_INCLUDE_DIR], [ERL_EI_INCLUDE_DIR]) AC_ARG_VAR([ERL_EI_LIBDIR], [ERL_EI_LIBDIR]) AC_ARG_VAR([CROSSCOMPILE], [CROSSCOMPILE]) AC_ARG_VAR([ERL_CFLAGS], [ERL_CFLAGS]) AC_ARG_VAR([ERL_LDFLAGS], [ERL_LDFLAGS]) AC_PROG_CC AM_PROG_AR AC_PATH_PROG(ELIXIR, $ELIXIR, elixir) AC_MSG_CHECKING([setting ERL_EI_INCLUDE_DIR]) if test "x$ERL_EI_INCLUDE_DIR" = "x"; then AC_SUBST([ERL_EI_INCLUDE_DIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/include") |> IO.puts')]) fi AC_MSG_RESULT([$ERL_EI_INCLUDE_DIR]) AC_MSG_CHECKING([setting ERL_EI_LIBDIR]) if test "x$ERL_EI_LIBDIR" = "x"; then AC_SUBST([ERL_EI_LIBDIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/lib") |> IO.puts')]) fi AC_MSG_RESULT([$ERL_EI_LIBDIR]) AC_MSG_CHECKING([setting ERL_CFLAGS]) if test "x$ERL_CFLAGS" = "x"; then AC_SUBST([ERL_CFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-I#{System.get_env("ERL_EI_INCLUDE_DIR", "#{to_string(:code.root_dir)}/usr/include")}" |> IO.puts')]) fi AC_MSG_RESULT([$ERL_CFLAGS]) AC_MSG_CHECKING([setting ERL_LDFLAGS]) if test "x$ERL_LDFLAGS" = "x"; then AC_SUBST([ERL_LDFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-L#{System.get_env("ERL_EI_LIBDIR", "#{to_string(:code.root_dir)}/usr/lib")}" |> IO.puts')]) fi AC_MSG_RESULT([$ERL_LDFLAGS]) LT_INIT() AC_CONFIG_FILES([Makefile]) AC_OUTPUT説明は次のとおりです。
AC_INIT
に生成するライブラリの情報を与えます。AC_CONFIG_MACRO_DIRS([m4])
でaclocal
で得られた設定を読むようにします。AC_INIT_AUTOMAKE
で Automake の使用を宣言します。オプションでエラーや警告を表示するようにしています。AC_ARG_VAR
で,configure
に与える環境変数を定義します。第1引数に変数名,第2引数にconfigure --help
の時に表示する説明を記載します。本当は第2引数をていねいにドキュメンテーションすべきところですが,手を抜いています。AC_PROG_CC
とAC_PROG_AR
はそれぞれ,CC
とAR
で指定されたコンパイラとリンカが存在することを確認します。AC_PATH_PROG(ELIXIR, $ELIXIR, elixir)
で環境変数ELIXIR
が設定されている場合にはそのパス上のプログラムが,設定されていない時にはelixir
が,PATH
上に存在するかを確認してその結果を表示します。- その後の
AC_MSG_CHECKING
からAC_MSG_RESULT
の一塊は,それぞれErlangに関連する環境変数が設定されているかを確認します。
AC_MSG_CHECKING([setting ERL...])
で確認中のメッセージを表示します。if test "x$ERL..." = "x"; then ... fi
で環境変数ERL...
が設定されているかを確認します。このような書き方は,シェルで移植性の高い記述をするためのAutoconfでは定番の書き方です。AC_SUBST
は第1引数の環境変数に第2引数の値を代入します。- ここでは
elixir --eval ワンライナープログラム
とすることで,それぞれ少しずつ異なるElixirのワンライナーのプログラムを実行して設定に必要なパスを取得しています。LC_ALL=en_US.UTF-8
を設定しているのはLinux環境でロケールに関する警告を抑制するためです。AC_MSG_RESULT
で設定された結果を表示します。- Elixirのワンライナーのプログラムは次のようになっています。
:code.root_dir |> to_string()
とすることで実行する Erlang の処理系の存在するパスを表示します。この値を仮に$1
としましょう。ERL_EI_INCLUDE_DIR
:$1/usr/include
を設定します。ERL_EI_LIBDIR
:$1/usr/lib
を設定します。ERL_CFLAGS
:ERL_EI_INCLUDE_DIR
が設定されているならば-I$ERL_EI_INCLUDE_DIR
を,そうでなければ-I$1/usr/include
を設定します。ERL_LDFLAGS
:ERL_EI_LIBDIR
が設定されているならば-L$ERL_EI_LIBDIR
を,そうでなければ-L$1/usr/lib
を設定します。LT_INIT
でLibtoolの初期化をします。AC_CONFIG_FILES([Makefile])
でMakefile
を出力するように設定します。AC_OUTPUT
で,以上の結果を出力します。これらのファイルを記述した後,もしHomebrewを使っているならあらかじめ次のコマンドを実行しておきます。
swift_elixir_test % brew install automake libtool
そして次のコマンドを実行します。
swift_elixir_test % autoreconf -i
.gitignore
に次を追記しましょう。.gitignore# For Autoconf /autom4te.cache/ /Makefile.in /aclocal.m4 /libtool /ar-lib /compile /install-sh /ltmain.sh /m4/ /missing /depcomp # For configure /config.log /config.status /config.guess /config.sub /configure # For build files /native/.deps /native/.dirstamp /native/.libs /native/*.o /native/*.lo /priv Makefile
mix.exs
のproject
情報を次のように変えます。mix.exsdef project do [ app: :swift_elixir_test, version: "0.1.0", elixir: "~> 1.11", start_permanent: Mix.env() == :prod, deps: deps(), compilers: [:elixir_make] ++ Mix.compilers(), aliases: [ compile: [&autoreconf/1, &configure/1, "compile"], clean: [&autoreconf/1, &configure/1, "clean"] ], make_clean: ["clean"] ] endまた,
autoreconf
を呼び出すようにmix.exs
に次の関数を足します。mix.exsdefp autoreconf(_args) do System.cmd("autoreconf", ["-i"]) endこれで
mix compile
を実行します。エラーなくビルドが終わりましたか? 出来たら次のようにして動的ライブラリが出来上がっていることを確認します。swift_elixir_test % file priv/.libs/libnif.so priv/.libs/libnif.so: Mach-O 64-bit bundle x86_64やった!
elixir_make
でNIFのビルドうまくいったので,
native/libnif.c
を仮実装します。native/libnif.c#include <stdlib.h> #include <erl_nif.h> static ERL_NIF_TERM test(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { ERL_NIF_TERM atom_error = enif_make_atom(env, "error"); return enif_make_tuple(env, 2, atom_error, enif_make_atom(env, "not_implemented")); } static ErlNifFunc nif_funcs[] = { {"test", 0, test} }; ERL_NIF_INIT(Elixir.SwiftElixirTest, nif_funcs, NULL, NULL, NULL, NULL)この段階で
mix compile
してエラーがなくビルドできることを確認します。説明は次のとおりです。
NULL
を使うために,#include <stdlib.h>
としました。#include <erl_nif.h>
はErlang の NIF API を定義しているヘッダファイルをインクルードします。もしここでエラーになるようならば,Automakeの設定のところが間違えていますので,見直してください。test
関数の定義はNIF APIに沿っています。第1引数が実行時環境,第2引数と第3引数で可変長の引数を形成しています。ERL_NIF_TERM
型はElixir/Erlangの変数のインタフェースです。enif_make_atom
でアトムを生成します。第1引数が実行時環境,第2引数がアトムの名前です。enif_make_tuple
でタプルを生成します。第1引数が実行時環境,第2引数から可変長の引数を形成していて,第2引数が要素数,第3引数以下が各要素です。- 仮に
{:error, :not_implemented"}
を返しています。nif_funcs
で関数を登録します。この場合の意味としてはElixirのtest関数の引数の数(アリティ)が0であるようなtest
という名称の関数を定義しています。ERL_NIF_INIT
でモジュールを登録します。第1引数がモジュール名,第2引数がnif_funcs
,第3〜6引数は初期化やリロード時の設定をする関数を登録します。ここでは仮に第3〜6引数にはNULL
を登録します。次に
lib/swift_elixir_test.ex
を変更します。lib/swift_elixir_test.exdefmodule SwiftElixirTest do require Logger @moduledoc """ Documentation for `SwiftElixirTest`. """ @on_load :load_nif def load_nif do nif_file = '#{:code.priv_dir(:swift_elixir_test)}/.libs/libnif' case :erlang.load_nif(nif_file, 0) do :ok -> :ok {:error, {:reload, _}} -> :ok {:error, reason} -> Logger.warn("Failed to load NIF: #{inspect(reason)}") end end def test(), do: raise("NIF test/0 not implemented") end説明は次のとおりです。
require Logger
とすることで,デバッグ等のログ出力を行うモジュールを呼び出せるようにします。@on_load :load_nif
とすることで,このモジュールを読み込む時,load_nif
関数を呼び出します。nif_file
に読み込むNIFライブラリの情報を与えます。
nif_file='...'
のようにシングルクォーテーションであるのに注意してください。Erlangに直接渡す文字列なので,char listにしてあります。:code_priv_dir
は,第1引数で指定したモジュールのpriv
ディレクトリを参照する関数です。SwiftElixirTest
モジュールのpriv/.libs/libnif.so
を読み込むので,.so
を取って:code_priv_dir(:swift_elixir_test)/.libs/libnif
とします。:erlang.load_nif
はNIFをロードする関数です。:ok
もしくは{:error, {:reload, ...}}
が返ってきた時には正常終了します。- それ以外の
{:error, ...}
が返ってきた時には,...
をreason
に代入して,Logger
を使って警告表示をします。test
関数の定義がNIF関数へのスタブです。NIFを定義する場合の定番で,呼び出した時に例外を発生するようにしています。NIFライブラリが正常に読み込めると上書きされて,NIFを呼び出すようになります。ここまで出来たら
iex -S mix
を実行してみましょう。少し待った後に,次のように正常に起動しましたか?swift_elixir_test % iex -S mix Erlang/OTP 23 [erts-11.1.2] [source] [64-bit] [smp:6:6] [ds:6:6:10] [async-threads:1] [hipe] make: Nothing to be done for `all'. Interactive Elixir (1.11.2) - press Ctrl+C to exit (type h() ENTER for help) iex(1)>もし実際にビルドしている時には次のように表示されます。
swift_elixir_test % iex -S mix Erlang/OTP 23 [erts-11.1.2] [source] [64-bit] [smp:6:6] [ds:6:6:10] [async-threads:1] [hipe] /bin/sh ./libtool --tag=CC --mode=compile gcc -DPACKAGE_NAME=\"priv/.libs/libnif.so\" -DPACKAGE_TARNAME=\"priv--libs-libnif-so\" -DPACKAGE_VERSION=\"1.0\" -DPACKAGE_STRING=\"priv/.libs/libnif.so\ 1.0\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"priv--libs-libnif-so\" -DVERSION=\"1.0\" -DSTDC_HEADERS=1 -DHAVE_SYS_TYPES_H=1 -DHAVE_SYS_STAT_H=1 -DHAVE_STDLIB_H=1 -DHAVE_STRING_H=1 -DHAVE_MEMORY_H=1 -DHAVE_STRINGS_H=1 -DHAVE_INTTYPES_H=1 -DHAVE_STDINT_H=1 -DHAVE_UNISTD_H=1 -DHAVE_DLFCN_H=1 -DLT_OBJDIR=\".libs/\" -I. -g -O2 -I/Users/zacky/.asdf/installs/erlang/23.1.2/usr/include -g -O2 -MT native/priv_libnif_la-libnif.lo -MD -MP -MF native/.deps/priv_libnif_la-libnif.Tpo -c -o native/priv_libnif_la-libnif.lo `test -f 'native/libnif.c' || echo './'`native/libnif.c libtool: compile: gcc -DPACKAGE_NAME=\"priv/.libs/libnif.so\" -DPACKAGE_TARNAME=\"priv--libs-libnif-so\" -DPACKAGE_VERSION=\"1.0\" "-DPACKAGE_STRING=\"priv/.libs/libnif.so 1.0\"" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"priv--libs-libnif-so\" -DVERSION=\"1.0\" -DSTDC_HEADERS=1 -DHAVE_SYS_TYPES_H=1 -DHAVE_SYS_STAT_H=1 -DHAVE_STDLIB_H=1 -DHAVE_STRING_H=1 -DHAVE_MEMORY_H=1 -DHAVE_STRINGS_H=1 -DHAVE_INTTYPES_H=1 -DHAVE_STDINT_H=1 -DHAVE_UNISTD_H=1 -DHAVE_DLFCN_H=1 -DLT_OBJDIR=\".libs/\" -I. -g -O2 -I/Users/zacky/.asdf/installs/erlang/23.1.2/usr/include -g -O2 -MT native/priv_libnif_la-libnif.lo -MD -MP -MF native/.deps/priv_libnif_la-libnif.Tpo -c native/libnif.c -fno-common -DPIC -o native/.libs/priv_libnif_la-libnif.o libtool: compile: gcc -DPACKAGE_NAME=\"priv/.libs/libnif.so\" -DPACKAGE_TARNAME=\"priv--libs-libnif-so\" -DPACKAGE_VERSION=\"1.0\" "-DPACKAGE_STRING=\"priv/.libs/libnif.so 1.0\"" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"priv--libs-libnif-so\" -DVERSION=\"1.0\" -DSTDC_HEADERS=1 -DHAVE_SYS_TYPES_H=1 -DHAVE_SYS_STAT_H=1 -DHAVE_STDLIB_H=1 -DHAVE_STRING_H=1 -DHAVE_MEMORY_H=1 -DHAVE_STRINGS_H=1 -DHAVE_INTTYPES_H=1 -DHAVE_STDINT_H=1 -DHAVE_UNISTD_H=1 -DHAVE_DLFCN_H=1 -DLT_OBJDIR=\".libs/\" -I. -g -O2 -I/Users/zacky/.asdf/installs/erlang/23.1.2/usr/include -g -O2 -MT native/priv_libnif_la-libnif.lo -MD -MP -MF native/.deps/priv_libnif_la-libnif.Tpo -c native/libnif.c -o native/priv_libnif_la-libnif.o >/dev/null 2>&1 mv -f native/.deps/priv_libnif_la-libnif.Tpo native/.deps/priv_libnif_la-libnif.Plo /bin/sh ./libtool --tag=CC --mode=link gcc -g -O2 -I/Users/zacky/.asdf/installs/erlang/23.1.2/usr/include -g -O2 -L/Users/zacky/.asdf/installs/erlang/23.1.2/usr/lib -shared -module -avoid-version -export-dynamic -o priv/libnif.la -rpath /usr/local/lib native/priv_libnif_la-libnif.lo libtool: link: gcc -Wl,-undefined -Wl,dynamic_lookup -o priv/.libs/libnif.so -bundle native/.libs/priv_libnif_la-libnif.o -L/Users/zacky/.asdf/installs/erlang/23.1.2/usr/lib -g -O2 -g -O2 libtool: link: ( cd "priv/.libs" && rm -f "libnif.la" && ln -s "../libnif.la" "libnif.la" ) Compiling 1 file (.ex) Generated swift_elixir_test app Interactive Elixir (1.11.2) - press Ctrl+C to exit (type h() ENTER for help) iex(1)>では次のように
SwiftElixirTest.test
関数を呼び出して,仮の実装である{:error, :not_implemented}
が返ってくることを確かめましょう。iex(1)> SwiftElixirTest.test {:error, :not_implemented} iex(2)>Autoconf/AutomakeでMacかどうかを判別するには
まず
configure.ac
を次のように書き換えて,OSを認識するようにしましょう。さしあたり,macOSとLinuxで動くようにします。configure.acdnl Process this file with autoconf to produce a configure script AC_INIT([priv/.libs/libnif.so], [1.0]) AC_CANONICAL_BUILD AC_CANONICAL_HOST AC_CANONICAL_TARGET AC_CONFIG_MACRO_DIRS([m4]) AM_INIT_AUTOMAKE([-Wall -Werror foreign]) AC_ARG_VAR([ELIXIR], [Elixir]) AC_ARG_VAR([ERL_EI_INCLUDE_DIR], [ERL_EI_INCLUDE_DIR]) AC_ARG_VAR([ERL_EI_LIBDIR], [ERL_EI_LIBDIR]) AC_ARG_VAR([CROSSCOMPILE], [CROSSCOMPILE]) AC_ARG_VAR([ERL_CFLAGS], [ERL_CFLAGS]) AC_ARG_VAR([ERL_LDFLAGS], [ERL_LDFLAGS]) AC_ARG_VAR([OBJC_FLAGS], [OBJC_FLAGS]) AC_PROG_CC build_linux=no build_mac=no all_mac=no case "${host_os}" in linux*) build_linux=yes ;; cygwin*|mingw*) AC_MSG_ERROR([OS $host_os on Windows is not supported]) ;; darwin*) case "${build_os}" in darwin*) case "${target_os}" in darwin*) all_mac=yes AC_PATH_PROG(XCRUN, xcrun) ;; *) ;; esac ;; *) ;; esac build_mac=yes ;; *) AC_MSG_ERROR([OS $host_os is not suppurted]) ;; esac AM_CONDITIONAL([LINUX], [test "x$build_linux" = "xyes"]) AM_CONDITIONAL([OSX], [test "x$build_mac" = "xyes"]) AM_CONDITIONAL([ALLOSX], [test "x$all_mac" = "xyes"]) AM_PROG_AR AC_PATH_PROG(ELIXIR, $ELIXIR, elixir) AC_MSG_CHECKING([setting ERL_EI_INCLUDE_DIR]) if test "x$ERL_EI_INCLUDE_DIR" = "x"; then AC_SUBST([ERL_EI_INCLUDE_DIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/include") |> IO.puts')]) fi AC_MSG_RESULT([$ERL_EI_INCLUDE_DIR]) AC_MSG_CHECKING([setting ERL_EI_LIBDIR]) if test "x$ERL_EI_LIBDIR" = "x"; then AC_SUBST([ERL_EI_LIBDIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/lib") |> IO.puts')]) fi AC_MSG_RESULT([$ERL_EI_LIBDIR]) AC_MSG_CHECKING([setting ERL_CFLAGS]) if test "x$ERL_CFLAGS" = "x"; then AC_SUBST([ERL_CFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-I#{System.get_env("ERL_EI_INCLUDE_DIR", "#{to_string(:code.root_dir)}/usr/include")}" |> IO.puts')]) fi AC_MSG_RESULT([$ERL_CFLAGS]) AC_MSG_CHECKING([setting ERL_LDFLAGS]) if test "x$ERL_LDFLAGS" = "x"; then AC_SUBST([ERL_LDFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-L#{System.get_env("ERL_EI_LIBDIR", "#{to_string(:code.root_dir)}/usr/lib")}" |> IO.puts')]) fi AC_MSG_RESULT([$ERL_LDFLAGS]) LT_INIT() AC_CONFIG_FILES([Makefile]) AC_OUTPUT追加分を説明します。
AC_CANONICAL_BUILD
,AC_CANONICAL_HOST
,AC_CANONICAL_TARGET
を指定することで,それぞれ,ビルド時,ホスト,ターゲットのCPU,ベンダー,OSの情報を得ることが出来るようになります。これらは,AC_INIT
の直後に置くのが賢明です。OBJC_FLAGS
という変数を足しました。build_linux=no
からAM_CONDITIONAL([ALLOSX], [test "x$all_mac" = "xyes"])
までがOSの種類の判別です。さしあたり,私が準備できる検証環境であるLinuxの場合とmacOSの場合にのみビルドができるようにしています。macOSの場合は,さらにビルド時,ホスト,ターゲットがいずれもmacOSの場合でのみ,ALLOSX
という条件を成立させるようにしています。- また,この条件が成立した時にのみ,Xcodeのコマンドラインツールである
xcrun
がパス上に存在するかを確認しています。次に
Makefile.am
を次のように変更します。Makefile.amAUTOMAKE_OPTIONS = subdir-objects ACLOCAL_AMFLAGS = -I m4 lib_LTLIBRARIES = priv/libnif.la priv_libnif_la_SOURCES = native/libnif.c if ALLOSX priv_libnif_la_CFLAGS = -DALLOSX $(CFLAGS) $(ERL_CFLAGS) else priv_libnif_la_CFLAGS = $(CFLAGS) $(ERL_CFLAGS) endif priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic
ALLOSX
が成立している場合,すなわちビルド時,ホスト,ターゲットがいずれもmacOSの場合に,マクロALLOSX
を定義してnative/libnif.c
をコンパイルするようにしています。これで,
native/libnif.c
中で#ifdef ALLOSX
とすれば,ビルド時,ホスト,ターゲットがいずれもmacOSの場合か,それ以外かを判別してプログラムコードを書き分けることが可能になります。NIFからObjective-Cのコードを呼び出す
では,ビルド時,ホスト,ターゲットがいずれもmacOSの場合に,次のようなObjective-Cのコードを呼び出してみましょう。
caller.m#import <Foundation/Foundation.h> #import "caller.h" void caller() { NSLog(@"Hello world from Objective-C."); }caller.h#ifndef CALLER_H #define CALLER_H void caller(); #endif // CALLER_HObjective-Cのコードと言いつつ,ほぼCのコードですが,このcaller関数を起点に任意のObjective-Cのコードを呼び出せると思ってください。さしあたり,
Foundation
に定義されているNSLog
を用いて,Hello, worldしたいと思います。ビルド・リンクするには,
Makefile.am
を次のようにします。Makefile.amAUTOMAKE_OPTIONS = subdir-objects ACLOCAL_AMFLAGS = -I m4 lib_LTLIBRARIES = priv/libnif.la priv_libnif_la_SOURCES = native/libnif.c if ALLOSX priv_libnif_la_LIBADD = $(LIBOBJS) native/caller.lo native/caller.lo: native/caller.m native/caller.h $(LIBTOOL) --mode=compile xcrun clang -c $(OBJC_FLAGS) $(CFLAGS) -o $@ $< endif if ALLOSX priv_libnif_la_CFLAGS = -DALLOSX $(CFLAGS) $(ERL_CFLAGS) else priv_libnif_la_CFLAGS = $(CFLAGS) $(ERL_CFLAGS) endif priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic追加分の説明は次のとおりです。
priv_libnif_la_LIBADD
として,priv/.libs/libnif.so
にnative/caller.lo
を追加するようにしています。$(LIBOBJS)
は,それまでのオブジェクトファイル群を登録している変数です。native/caller.lo: native/caller.m native/caller.h
として依存関係を定義しています。$(LIBTOOL) --mode=compile xcrun clang -c $(OBJC_FLAGS) $(CFLAGS) -o $@ $<
とすることで,XcodeのClangを明示的に呼び出してコンパイルし,Libtoolを使って.lo
形式に変換しています。XcodeのClangを明示的に呼び出すことで,Framework
をリンクしてくれますし,Swiftコードをリンクした時にもバージョンの不一致を避けられます。Foundation と
NSLog
の動作確認(Objective-C)せっかくAutotoolsを使っているので,試しにFoundationと
NSLog
が動作するかをチェックするスクリプトを導入してみましょう。configure.ac
を次のようにします。configure.acdnl Process this file with autoconf to produce a configure script AC_INIT([priv/.libs/libnif.so], [1.0]) AC_CANONICAL_BUILD AC_CANONICAL_HOST AC_CANONICAL_TARGET AC_CONFIG_MACRO_DIRS([m4]) AM_INIT_AUTOMAKE([-Wall -Werror foreign]) AC_ARG_VAR([ELIXIR], [Elixir]) AC_ARG_VAR([ERL_EI_INCLUDE_DIR], [ERL_EI_INCLUDE_DIR]) AC_ARG_VAR([ERL_EI_LIBDIR], [ERL_EI_LIBDIR]) AC_ARG_VAR([CROSSCOMPILE], [CROSSCOMPILE]) AC_ARG_VAR([ERL_CFLAGS], [ERL_CFLAGS]) AC_ARG_VAR([ERL_LDFLAGS], [ERL_LDFLAGS]) AC_ARG_VAR([OBJC_FLAGS], [OBJC_FLAGS]) AC_PROG_CC build_linux=no build_mac=no all_mac=no case "${host_os}" in linux*) build_linux=yes ;; cygwin*|mingw*) AC_MSG_ERROR([OS $host_os on Windows is not supported]) ;; darwin*) case "${build_os}" in darwin*) case "${target_os}" in darwin*) all_mac=yes AC_PATH_PROG(XCRUN, xcrun) ;; *) ;; esac ;; *) ;; esac build_mac=yes ;; *) AC_MSG_ERROR([OS $host_os is not suppurted]) ;; esac AM_CONDITIONAL([LINUX], [test "x$build_linux" = "xyes"]) AM_CONDITIONAL([OSX], [test "x$build_mac" = "xyes"]) AM_CONDITIONAL([ALLOSX], [test "x$all_mac" = "xyes"]) AM_PROG_AR AC_PATH_PROG(ELIXIR, $ELIXIR, elixir) AC_MSG_CHECKING([setting ERL_EI_INCLUDE_DIR]) if test "x$ERL_EI_INCLUDE_DIR" = "x"; then AC_SUBST([ERL_EI_INCLUDE_DIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/include") |> IO.puts')]) fi AC_MSG_RESULT([$ERL_EI_INCLUDE_DIR]) AC_MSG_CHECKING([setting ERL_EI_LIBDIR]) if test "x$ERL_EI_LIBDIR" = "x"; then AC_SUBST([ERL_EI_LIBDIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/lib") |> IO.puts')]) fi AC_MSG_RESULT([$ERL_EI_LIBDIR]) AC_MSG_CHECKING([setting ERL_CFLAGS]) if test "x$ERL_CFLAGS" = "x"; then AC_SUBST([ERL_CFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-I#{System.get_env("ERL_EI_INCLUDE_DIR", "#{to_string(:code.root_dir)}/usr/include")}" |> IO.puts')]) fi AC_MSG_RESULT([$ERL_CFLAGS]) AC_MSG_CHECKING([setting ERL_LDFLAGS]) if test "x$ERL_LDFLAGS" = "x"; then AC_SUBST([ERL_LDFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-L#{System.get_env("ERL_EI_LIBDIR", "#{to_string(:code.root_dir)}/usr/lib")}" |> IO.puts')]) fi AC_MSG_RESULT([$ERL_LDFLAGS]) working_foundation=no working_nslog=no if test "x$all_mac" = "xyes"; then AC_MSG_CHECKING([whether Foundation Framework exists]) cat>_framework.m<<EOF #import <Foundation/Foundation.h> int main() { return 0; } EOF if xcrun clang _framework.m -o _framework > /dev/null 2>&1 && ./_framework > /dev/null 2>&1 ; then working_foundation=yes fi rm -f _framework.m _framework.o _framework AC_MSG_RESULT([$working_foundation]) AC_MSG_CHECKING([whether NSLog works]) cat>_nslog.m<<EOF #import <Foundation/Foundation.h> int main() { NSLog(@"hello world"); return 0; } EOF if xcrun clang _nslog.m -o _nslog -framework Foundation > /dev/null 2>&1 && ./_nslog > /dev/null 2>&1 ; then working_nslog=yes fi rm -f _nslog.m _nslog.o _nslog AC_MSG_RESULT([$working_nslog]) fi AM_CONDITIONAL([EXIST_FOUNDATION], [test "x$working_foundation" = "xyes"]) AM_CONDITIONAL([WORK_NSLOG], [test "x$working_nslog" = "xyes"]) LT_INIT() AC_CONFIG_FILES([Makefile]) AC_OUTPUT追加分を説明します。
working_foundation=no
とworking_nslog=no
でそれぞれのフラグを初期化します。if test "x$all_mac" = "xyes"; then
で,ビルド時,ホスト,ターゲットが全てmacOSのときのみ動作するようにします。AC_MSG_CHECKING([whether Foundation Framework exists])
でFoundationが存在するかチェックしますと表示します。- 次の
cat
から2つ目のEOF
までがテストするプログラムコードです。- 次の
if
でこのプログラムコードをコンパイルして実行し,正常終了することを確認します。標準出力とエラー出力をまとめてリダイレクトする点に注意してください。rm
で生成したファイルを削除します。AC_MSG_RESULT([$working_foundation])
で検証結果を表示します。- 同様に
NSLog
についても動作確認します。AM_CONDITIONAL
で判定結果をMakefile.am
で利用できるようにします。それぞれ,EXIST_FOUNDATION
とWORK_NSLOG
で真偽値を取り出せるようにしています。Foundationと
NSLog
は標準の機能なので,存在をチェックしてコードに反映するのはナンセンスだと思いますが,例としてやってみましょう。Makefile.amAUTOMAKE_OPTIONS = subdir-objects ACLOCAL_AMFLAGS = -I m4 if EXIST_FOUNDATION OBJC_FLAGS += -DEXIST_FOUNDATION endif if WORK_NSLOG OBJC_FLAGS += -DWORK_NSLOG endif lib_LTLIBRARIES = priv/libnif.la priv_libnif_la_SOURCES = native/libnif.c if ALLOSX priv_libnif_la_LIBADD = $(LIBOBJS) native/caller.lo native/caller.lo: native/caller.m native/caller.h $(LIBTOOL) --mode=compile xcrun clang -c $(OBJC_FLAGS) $(CFLAGS) -o $@ $< endif if ALLOSX priv_libnif_la_CFLAGS = -DALLOSX $(CFLAGS) $(ERL_CFLAGS) else priv_libnif_la_CFLAGS = $(CFLAGS) $(ERL_CFLAGS) endif priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic追加分の説明です。
if EXIST_FOUNDATION
からendif
まではEXIST_FOUNDATION
が真の時にOBJC_FLAGS
に-DEXIST_FOUNDATION
を追加することで,マクロEXIST_FOUNDATION
を定義しています。OBJC_FLAGS
の行をインデントしない点に注意してください。インデントすると変数の更新が読まれなくなってしまいます。- 同様に
if WORK_NSLOG
からendif
まではWORK_FOUNDAION
が真の時にOBJC_FLAGS
に-DWORK_NSLOG
を追加しています。caller.m#ifdef EXIST_FOUNDATION #import <Foundation/Foundation.h> #endif #import "caller.h" void caller() { #ifdef WORK_NSLOG NSLog(@"Hello world from Objective-C."); #endif }マクロ
EXIST_FOUNDATION
とWORK_NSLOG
が定義されているかをみて,それぞれ#import <Foundation/Foundation.h>
とNSLog(...)
をスイッチしています。Objective-CからSwiftのコードを呼び出す
Swift 5.3のコードをObjective-Cから呼び出す方法で既に紹介しましたが,Autoconfに対応させましょう。
次のようなSwiftのコードを呼び出します。このコードの出典はhttps://docs.swift.org/swift-book/LanguageGuide/Methods.html です。
native/ExampleClass.swiftimport Foundation @objc class ExampleClass: NSObject { var count = 0 @objc func increment() { count += 1 NSLog("Hello world from Swift.") } @objc func increment(by amount: Int) { count += amount } @objc func reset() { count = 0 } }Objective-Cから呼び出せるようにするためには,次の2つのことを行います。
import Foundation
として,NSObject
から派生するようにクラスを定義する@objc
をクラスと,Objective-Cから呼び出したいメソッドに付記するこのようなクラスを足がかりとして,任意のSwiftコードを呼び出せば良いというわけです。
Objective-Cの次のように変更します。
native/caller.m#import <Foundation/Foundation.h> #import "ExampleClass-Swift.h" #import "caller.h" void caller() { ExampleClass *obj = [[ExampleClass alloc] init]; [obj increment]; NSLog(@"Hello world from Objective-C."); }ポイントは次のとおりです。
- クラス名が
ExampleClass
である場合には,import "ExampleClass-Swift.h"
とする(クラス名に-Swift.h
をつけたヘッダファイルをインポートする)- あとはSwiftのコードをObjective-Cに読み替えて呼び出す。
configure.ac
を次のようにします。configure.acdnl Process this file with autoconf to produce a configure script AC_INIT([priv/.libs/libnif.so], [1.0]) AC_CANONICAL_BUILD AC_CANONICAL_HOST AC_CANONICAL_TARGET AC_CONFIG_MACRO_DIRS([m4]) AM_INIT_AUTOMAKE([-Wall -Werror foreign]) AC_ARG_VAR([ELIXIR], [Elixir]) AC_ARG_VAR([ERL_EI_INCLUDE_DIR], [ERL_EI_INCLUDE_DIR]) AC_ARG_VAR([ERL_EI_LIBDIR], [ERL_EI_LIBDIR]) AC_ARG_VAR([CROSSCOMPILE], [CROSSCOMPILE]) AC_ARG_VAR([ERL_CFLAGS], [ERL_CFLAGS]) AC_ARG_VAR([ERL_LDFLAGS], [ERL_LDFLAGS]) AC_ARG_VAR([OBJC_FLAGS], [OBJC_FLAGS]) AC_ARG_VAR([SWIFT_FLAGS], [SWIFT_FLAGS]) AC_PROG_CC build_linux=no build_mac=no all_mac=no case "${host_os}" in linux*) build_linux=yes ;; cygwin*|mingw*) AC_MSG_ERROR([OS $host_os on Windows is not supported]) ;; darwin*) case "${build_os}" in darwin*) case "${target_os}" in darwin*) all_mac=yes AC_PATH_PROG(XCRUN, xcrun) ;; *) ;; esac ;; *) ;; esac build_mac=yes ;; *) AC_MSG_ERROR([OS $host_os is not suppurted]) ;; esac AM_CONDITIONAL([LINUX], [test "x$build_linux" = "xyes"]) AM_CONDITIONAL([OSX], [test "x$build_mac" = "xyes"]) AM_CONDITIONAL([ALLOSX], [test "x$all_mac" = "xyes"]) AM_PROG_AR AC_PATH_PROG(ELIXIR, $ELIXIR, elixir) AC_MSG_CHECKING([setting ERL_EI_INCLUDE_DIR]) if test "x$ERL_EI_INCLUDE_DIR" = "x"; then AC_SUBST([ERL_EI_INCLUDE_DIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/include") |> IO.puts')]) fi AC_MSG_RESULT([$ERL_EI_INCLUDE_DIR]) AC_MSG_CHECKING([setting ERL_EI_LIBDIR]) if test "x$ERL_EI_LIBDIR" = "x"; then AC_SUBST([ERL_EI_LIBDIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/lib") |> IO.puts')]) fi AC_MSG_RESULT([$ERL_EI_LIBDIR]) AC_MSG_CHECKING([setting ERL_CFLAGS]) if test "x$ERL_CFLAGS" = "x"; then AC_SUBST([ERL_CFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-I#{System.get_env("ERL_EI_INCLUDE_DIR", "#{to_string(:code.root_dir)}/usr/include")}" |> IO.puts')]) fi AC_MSG_RESULT([$ERL_CFLAGS]) AC_MSG_CHECKING([setting ERL_LDFLAGS]) if test "x$ERL_LDFLAGS" = "x"; then AC_SUBST([ERL_LDFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-L#{System.get_env("ERL_EI_LIBDIR", "#{to_string(:code.root_dir)}/usr/lib")}" |> IO.puts')]) fi AC_MSG_RESULT([$ERL_LDFLAGS]) working_foundation=no working_nslog=no if test "x$all_mac" = "xyes"; then AC_MSG_CHECKING([whether Foundation Framework exists]) cat>_framework.m<<EOF #import <Foundation/Foundation.h> int main() { return 0; } EOF if xcrun clang _framework.m -o _framework > /dev/null 2>&1 && ./_framework > /dev/null 2>&1 ; then working_foundation=yes fi rm -f _framework.m _framework.o _framework AC_MSG_RESULT([$working_foundation]) AC_MSG_CHECKING([whether NSLog works]) cat>_nslog.m<<EOF #import <Foundation/Foundation.h> int main() { NSLog(@"hello world"); return 0; } EOF if xcrun clang _nslog.m -o _nslog -framework Foundation > /dev/null 2>&1 && ./_nslog > /dev/null 2>&1 ; then working_nslog=yes fi rm -f _nslog.m _nslog.o _nslog AC_MSG_RESULT([$working_nslog]) fi AM_CONDITIONAL([EXIST_FOUNDATION], [test "x$working_foundation" = "xyes"]) AM_CONDITIONAL([WORK_NSLOG], [test "x$working_nslog" = "xyes"]) LT_INIT() AC_CONFIG_FILES([Makefile]) AC_OUTPUT変更点は次のとおりです。
AC_ARG_VAR([SWIFT_FLAGS], [SWIFT_FLAGS])
を追加する本題の
Makefile.am
を次のようにします。Makefile.amAUTOMAKE_OPTIONS = subdir-objects ACLOCAL_AMFLAGS = -I m4 lib_LTLIBRARIES = priv/libnif.la priv_libnif_la_SOURCES = native/libnif.c if ALLOSX priv_libnif_la_LIBADD = $(LIBOBJS) native/caller.lo native/ExampleClass.lo native/caller.lo: native/caller.m native/caller.h native/ExampleClass-Swift.h $(LIBTOOL) --mode=compile xcrun clang -c $(OBJC_FLAGS) $(CFLAGS) -o $@ $< native/ExampleClass.lo: native/ExampleClass.swift $(LIBTOOL) --mode=compile ./swiftc_wrapper $(SWIFT_FLAGS) -emit-object -parse-as-library $< -o $@ native/ExampleClass-Swift.h: native/ExampleClass.swift xcrun swiftc $(SWIFT_FLAGS) $< -emit-objc-header -emit-objc-header-path $@ endif if ALLOSX priv_libnif_la_CFLAGS = -DALLOSX $(CFLAGS) $(ERL_CFLAGS) else priv_libnif_la_CFLAGS = $(CFLAGS) $(ERL_CFLAGS) endif if ALLOSX priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic -L`xcrun --show-sdk-path`/usr/lib/swift -undefined dynamic_lookup else priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic endif変更点は次のとおりです。
priv_libnif_la_LIBADD
にnative/ExampleClass.lo
を加えるnative/caller.lo
を生成するルールの依存関係にnative/ExampleClass-Swift.h
を加えるnative/ExampleClass.lo
を生成するルールを加える
- 依存ファイルは
native/ExampleClass.swift
- Libtoolで
swiftc
を呼び出すのですが,そのまま呼び出すとオプションのエラーになるので,ラッパーswiftc_wrapper
を介して呼び出す(後述)-emit-object
をつけることでオブジェクトファイルを生成する-parse-as-library
をつけることでライブラリとしてリンクできるようにするnative/ExampleClass-Swift.h
を生成するルールを加える
xcrun swiftc
でSwiftのコンパイラを呼び出す-emit-objc-header
でObjective-Cのヘッダファイルを生成させる-emit-objc-header-path
で生成先を指定する- (残課題)
ExampleClass{,.swift{doc,module,sourceinfo}}
を生成してしまうのだけど,生成を抑制する方法がわからないALLOSX
のとき,priv_libnif_la_LDFLAGS
に以下を加えることで,Swiftのライブラリをリンクする-L`xcrun --show-sdk-path`/usr/lib/swift -undefined dynamic_lookup
swiftc_wrapper
は次のようなシェルスクリプトです。#!/bin/sh echo "xcrun swiftc $@" | sed -e 's/-fno-common//g' | sed -e 's/-DPIC//g' > _swiftc_wrapper chmod +x _swiftc_wrapper ./_swiftc_wrapper rm _swiftc_wrapperやっていることは,Libtoolがコンパイルモードでコンパイラを起動する時につけるオプションである
-fno-common
と-DPIC
はswiftc
では認識されないので,外す,ということです。もうちょっとスマートに書けそうに思います。良い方法があったら教えてください。
以上で
mix clean
とiex -S mix
を実行してコンパイルエラーがないことを確認してください。さらに次のように実行すると,SwiftとObjective-CからそれぞれNSLog
でメッセージを表示してくれるはずです。iex(1)> SwiftElixirTest.test 2020-12-01 06:45:06.883 beam.smp[97578:5144991] Hello world from Swift. 2020-12-01 06:45:06.883 beam.smp[97578:5144991] Hello world from Objective-C. :ok iex(2)>Linuxで実行すると次のようになります。
iex(1)> SwiftElixirTest.test {:error, :not_implemented} iex(2)>Foundation と
NSLog
の動作確認(Swift)Foundationと
NSLog
の動作確認をSwiftでも行いましょう。configure.acdnl Process this file with autoconf to produce a configure script AC_INIT([priv/.libs/libnif.so], [1.0]) AC_CANONICAL_BUILD AC_CANONICAL_HOST AC_CANONICAL_TARGET AC_CONFIG_MACRO_DIRS([m4]) AM_INIT_AUTOMAKE([-Wall -Werror foreign]) AC_ARG_VAR([ELIXIR], [Elixir]) AC_ARG_VAR([ERL_EI_INCLUDE_DIR], [ERL_EI_INCLUDE_DIR]) AC_ARG_VAR([ERL_EI_LIBDIR], [ERL_EI_LIBDIR]) AC_ARG_VAR([CROSSCOMPILE], [CROSSCOMPILE]) AC_ARG_VAR([ERL_CFLAGS], [ERL_CFLAGS]) AC_ARG_VAR([ERL_LDFLAGS], [ERL_LDFLAGS]) AC_ARG_VAR([OBJC_FLAGS], [OBJC_FLAGS]) AC_ARG_VAR([SWIFT_FLAGS], [SWIFT_FLAGS]) AC_PROG_CC build_linux=no build_mac=no all_mac=no case "${host_os}" in linux*) build_linux=yes ;; cygwin*|mingw*) AC_MSG_ERROR([OS $host_os on Windows is not supported]) ;; darwin*) case "${build_os}" in darwin*) case "${target_os}" in darwin*) all_mac=yes AC_PATH_PROG(XCRUN, xcrun) ;; *) ;; esac ;; *) ;; esac build_mac=yes ;; *) AC_MSG_ERROR([OS $host_os is not suppurted]) ;; esac AM_CONDITIONAL([LINUX], [test "x$build_linux" = "xyes"]) AM_CONDITIONAL([OSX], [test "x$build_mac" = "xyes"]) AM_CONDITIONAL([ALLOSX], [test "x$all_mac" = "xyes"]) AM_PROG_AR AC_PATH_PROG(ELIXIR, $ELIXIR, elixir) AC_MSG_CHECKING([setting ERL_EI_INCLUDE_DIR]) if test "x$ERL_EI_INCLUDE_DIR" = "x"; then AC_SUBST([ERL_EI_INCLUDE_DIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/include") |> IO.puts')]) fi AC_MSG_RESULT([$ERL_EI_INCLUDE_DIR]) AC_MSG_CHECKING([setting ERL_EI_LIBDIR]) if test "x$ERL_EI_LIBDIR" = "x"; then AC_SUBST([ERL_EI_LIBDIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/lib") |> IO.puts')]) fi AC_MSG_RESULT([$ERL_EI_LIBDIR]) AC_MSG_CHECKING([setting ERL_CFLAGS]) if test "x$ERL_CFLAGS" = "x"; then AC_SUBST([ERL_CFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-I#{System.get_env("ERL_EI_INCLUDE_DIR", "#{to_string(:code.root_dir)}/usr/include")}" |> IO.puts')]) fi AC_MSG_RESULT([$ERL_CFLAGS]) AC_MSG_CHECKING([setting ERL_LDFLAGS]) if test "x$ERL_LDFLAGS" = "x"; then AC_SUBST([ERL_LDFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-L#{System.get_env("ERL_EI_LIBDIR", "#{to_string(:code.root_dir)}/usr/lib")}" |> IO.puts')]) fi AC_MSG_RESULT([$ERL_LDFLAGS]) working_foundation=no working_nslog=no if test "x$all_mac" = "xyes"; then AC_MSG_CHECKING([whether Foundation Framework exists in Objective C]) cat>_framework.m<<EOF #import <Foundation/Foundation.h> int main() { return 0; } EOF if xcrun clang _framework.m -o _framework > /dev/null 2>&1 && ./_framework > /dev/null 2>&1 ; then AC_MSG_CHECKING([and Swift]) cat>_framework.swift<<EOF import Foundation import Darwin exit(0) EOF if xcrun swiftc _framework.swift -o _framework > /dev/null 2>&1 && ./_framework > /dev/null 2>&1 ; then working_foundation=yes fi fi rm -f _framework.swift _framework.m _framework.o _framework AC_MSG_RESULT([$working_foundation]) AC_MSG_CHECKING([whether NSLog works in Objective-C]) cat>_nslog.m<<EOF #import <Foundation/Foundation.h> int main() { NSLog(@"hello world"); return 0; } EOF if xcrun clang _nslog.m -o _nslog -framework Foundation > /dev/null 2>&1 && ./_nslog > /dev/null 2>&1 ; then AC_MSG_CHECKING([and Swift]) cat>_nslog.swift<<EOF import Foundation import Darwin NSLog("hello world") exit(0) EOF if xcrun swiftc _nslog.swift -o _nslog > /dev/null 2>&1 && ./_nslog > /dev/null 2>&1 ; then working_nslog=yes fi fi rm -f _nslog.swift _nslog.m _nslog.o _nslog AC_MSG_RESULT([$working_nslog]) fi AM_CONDITIONAL([EXIST_FOUNDATION], [test "x$working_foundation" = "xyes"]) AM_CONDITIONAL([WORK_NSLOG], [test "x$working_nslog" = "xyes"]) LT_INIT() AC_CONFIG_FILES([Makefile]) AC_OUTPUTMakefile.amAUTOMAKE_OPTIONS = subdir-objects ACLOCAL_AMFLAGS = -I m4 if EXIST_FOUNDATION OBJC_FLAGS += -DEXIST_FOUNDATION SWIFT_FLAGS += -DEXIST_FOUNDATION endif if WORK_NSLOG OBJC_FLAGS += -DWORK_NSLOG SWIFT_FLAGS += -DWORK_NSLOG endif lib_LTLIBRARIES = priv/libnif.la priv_libnif_la_SOURCES = native/libnif.c if ALLOSX priv_libnif_la_LIBADD = $(LIBOBJS) native/caller.lo native/ExampleClass.lo native/caller.lo: native/caller.m native/caller.h native/ExampleClass-Swift.h $(LIBTOOL) --mode=compile xcrun clang -c $(OBJC_FLAGS) $(CFLAGS) -o $@ $< native/ExampleClass.lo: native/ExampleClass.swift $(LIBTOOL) --mode=compile ./swiftc_wrapper $(SWIFT_FLAGS) -emit-object -parse-as-library $< -o $@ native/ExampleClass-Swift.h: native/ExampleClass.swift xcrun swiftc $(SWIFT_FLAGS) $< -emit-objc-header -emit-objc-header-path $@ endif if ALLOSX priv_libnif_la_CFLAGS = -DALLOSX $(CFLAGS) $(ERL_CFLAGS) else priv_libnif_la_CFLAGS = $(CFLAGS) $(ERL_CFLAGS) endif if ALLOSX priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic -L`xcrun --show-sdk-path`/usr/lib/swift -undefined dynamic_lookup else priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic endifnative/ExampleClass.swift#if EXIST_FOUNDATION import Foundation #endif @objc class ExampleClass: NSObject { var count = 0 @objc func increment() { count += 1 #if WORK_NSLOG NSLog("Hello world from Swift.") #endif } @objc func increment(by amount: Int) { count += amount } @objc func reset() { count = 0 } }だいたいObjective-Cの場合と同様ですが,いくつか違いがあります。
swiftc
では-framework Foundation
は不要です。- Cでの
#ifdef
はSwiftでは#if
です。おわりに
この記事ではElixirからObjective-CやSwift 5.3のコードを呼び出す方法について詳説しました。またAutotoolsを使ってObjective-CやSwiftのヘッダファイルや関数が存在するかどうかを確認する方法も示しています。
将来課題としては,Autotoolsを使うとビルドに時間がかかるようになってくるので,
make -j
による並列コンパイルをしたり,必要な時だけautoreconf
や./configure
をするように改めたいのと,もっと長期的にはAutotoolsの代わりをするElixirベースのビルドツールを構築したいなと思っています。あと,M1 Macだったときにはユニバーサルバイナリを生成するようにしてみたいですね。
- ユニバーサルバイナリの生成方法はBuilding a Universal macOS Binaryに書かれています。
- また,
system_profiler SPSoftwareDataType
とするとSystem Version
の項目にmacOSのバージョンが出ます。以上を利用して,Big Surは macOS 11.0なので,それ以降だったらx86_64とarm64のユニバーサルバイナリを生成するというロジックでも良いかと思います。この方法はまた後日試してみたいと思います。
明日のElixir Advent Calendar 2020 11日目の記事は @pojiro さんの「Elixirで並行コマンド実行サーバーを作ったら感動した話」です。よろしくお願いします。
本研究成果は、科学技術振興機構研究成果展開事業研究成果最適展開支援プログラム A-STEP トライアウト JPMJTM20H1 の支援を受けた。
- 投稿日:2020-12-04T15:16:24+09:00
CloudFirestoreのServerTimestampBehaviorの扱い方
これはSOUSEI Technology アドベントカレンダー4日目の記事です!
普段はiOS開発をメインに活動しています。今回もiOSのミニTIPSを投稿していきます?CloudFirestoreのServerTimestampとは
CloudFirestoreに各端末個別の時刻を保存してしまうのは良くないです。
各端末個別の時刻は正確とは言えないからです。(自分で変更することもできてしまいます)
Firebaseのサーバー時刻を使用して保存すればデータ上では一貫して保存できます。
その時に使用するのがServerTimestampです。NG
let data: [String: Any] = [ "createdAt": Date() ] // 保存処理 Firestore.firestore().collection(collectionId).addDocument(data: data)OK
let data: [String: Any] = [ "createdAt": FiledValue.serverTimestamp() ] // 保存処理 Firestore.firestore().collection(collectionId).addDocument(data: data)保存する値に
FiledValue.serverTimestamp()
を指定することで、Firebaseのサーバー時刻を元にした値を保存してくれます。ServerTimestamp取得の注意点
しかしこの便利なFirestoreのServerTimestampには少し罠があります。
それはサーバーで時刻の値が決まるまで、値が空で返ってきてしまうということです。アプリの仕様によっては空で返ってきたら困るということもあると思います。
下記のように書くとnilで返ってくる可能性があります。struct MessageEntity { let createdAt: Date? init(document: QueryDocumentSnapshot) { let data = document.data() let timestamp = data["createdAt"] as? Timestamp self.createdAt = timestamp?.dateValue() } }ServerTimestampBehaviorが出来た
しかしこの問題を解消できるものがあります。
それがServerTimestampBehaviorです。
公式ドキュメントのリンクを貼ったので詳しくはそちらを見るのが良いと思いますが、英語なのでこちらでも少し説明します。
ServerTimestampBehaivorはenumとして定義されていて、Xcodeで定義を見にいくとこのようになっています。typedef NS_ENUM(NSInteger, FIRServerTimestampBehavior) { /** * Return `NSNull` for `FieldValue.serverTimestamp()` fields that have not yet * been set to their final value. */ FIRServerTimestampBehaviorNone, /** * Return a local estimates for `FieldValue.serverTimestamp()` * fields that have not yet been set to their final value. This estimate will * likely differ from the final value and may cause these pending values to * change once the server result becomes available. */ FIRServerTimestampBehaviorEstimate, /** * Return the previous value for `FieldValue.serverTimestamp()` fields that * have not yet been set to their final value. */ FIRServerTimestampBehaviorPrevious } NS_SWIFT_NAME(ServerTimestampBehavior);NoneとEstimateとPreviousの3種類が存在することが分かります。
各項目の意味としては、このようになっています。(そのまま訳しただけです)
ServerTimestampBehaviorの種別 意味 None まだ最終的な値が設定されていないFieldValue.serverTimestamp()フィールドに対してはNSNullを返します Estimate まだ最終値に設定されていないFieldValue.serverTimestamp()フィールドのローカル推定値を返します。この推定値は最終値とは異なる可能性が高く、サーバの結果が利用可能になると保留されていた値が変更される可能性があります Previous まだ最終値に設定されていないFieldValue.serverTimestamp()フィールドの前の値を返します ありがたいことにライブラリの定義のコメントがとても分かりやすく説明しているのでイメージがつくのではないかと思います。
None
に関してはこれまでのServerTimestampと同じく、値が決まるまでは空の値を返すので指定してもあまり意味がないのではないでしょうか。(明示的にnilを許容するという可読性を考慮すればわざわざ指定しても良さそうです)
Estimate
は最終的なサーバー時刻が設定されるまでローカル推定値を返すので空の値で返ってくることがありません。
Previous
は最終値が決まっていない場合は、前の値を返します。ただし、前の値が空であれば、やはり空で返ってきてしまいます。Firebaseライブラリのリポジトリの該当箇所の実装を見ると、
estimate以外はNSNullが返却される可能性のあることが分かります。- (id)convertedServerTimestamp:(const FieldValue &)value options:(const FieldValueOptions &)options { const auto &sts = value.server_timestamp_value(); switch (options.server_timestamp_behavior()) { case ServerTimestampBehavior::kNone: return [NSNull null]; case ServerTimestampBehavior::kEstimate: { FieldValue local_write_time = FieldValue::FromTimestamp(sts.local_write_time()); return [self convertedTimestamp:local_write_time]; } case ServerTimestampBehavior::kPrevious: return sts.previous_value() ? [self convertedValue:*sts.previous_value() options:options] : [NSNull null]; } UNREACHABLE(); }取得の仕方
SwiftでServerTimestampBehaviorを使用して値を取得する方法について一応載せておきます。
if let createdAt = document.get("createdAt", serverTimestampBehavior: .estimate) as? Timestamp { print(createdAt.dateValue()) }ご覧のようにTimestampにキャストしてからdateValue()を呼ぶとDate型で取得できます。
そこは通常のServerTimestampを使用するのとほとんど変わらないですね。ServerTimestampがどのように返却されるか調べてみた
ServerTimestampBehaivorについて大体わかったところで実際にどのような結果が返ってくるのかを出力してみました。
// 最終値が決定する前 None ==> nil Estimate ==> 2020-12-03 10:48:47 +0000 Previous ==> nil // 最終値が決定した後 None ==> 2020-12-03 10:48:47 +0000 Estimate ==> 2020-12-03 10:48:47 +0000 Previous ==> 2020-12-03 10:48:47 +0000ご覧のようになっています。
次に端末時刻をわざと3時間後にずらしてみました。// 最終値が決定する前 None ==> nil Estimate ==> 2020-12-03 13:53:36 +0000 Previous ==> nil // 最終値が決定した後 None ==> 2020-12-03 10:54:45 +0000 Estimate ==> 2020-12-03 10:54:45 +0000 Previous ==> 2020-12-03 10:54:45 +0000そうすると値が決まる前は端末時刻が設定されていることが分かります。
しかし値が決まった後はわざとずらした3時間は無効にされてFirebaseのサーバー時刻で値が設定されています。値が決まる前の時刻を使用するかどうかについてはアプリの要件と相談して決めれば良いのではないでしょうか。
- 投稿日:2020-12-04T14:56:53+09:00
[事例報告] Instance method ‘didTapMessage(in:)’ nearly matches defaulted requirement ‘didTapMessage(in:)’ of protocol ‘MessageCellDelegate’
本事例はMessageKitで起きた問題ですが、一般に起こりうるのでメモしておきます。
下記のようなプロトコルがあり
public protocol MessageCellDelegate: MessageLabelDelegate { func didTapMessage(in cell: MessageCollectionViewCell) } public extension MessageCellDelegate { func didTapBackground(in cell: MessageCollectionViewCell) {} }これを実装しようとすると
Instance method ‘didTapMessage(in:)’ nearly matches defaulted requirement ‘didTapMessage(in:)’ of protocol ‘MessageCellDelegate’と怒られます。
原因がはMessageKitで用意されているMessageCollectionViewCellと同じ名前のMessageCollectionViewCellというクラスを
別にアプリ内で定義していたからでした。気づきにくいバグでした。ご注意を‥。
- 投稿日:2020-12-04T11:16:27+09:00
複数のライブラリを入れた場合にResultが干渉してエラーが出てしまうケース
複数のライブラリを使用していると
別々のライブラリで同じ命名が使われている場合にエラーが出ます。今回はもともとNukeを使っていて、そこにURLEmbeddedViewを
追加導入したところ、下記コードにてNuke.loadImage( with: imageURL, options: ImageLoadingOptions(), into: cell.fullImageView, progress: nil) { (result: Result<ImageResponse, ImagePipeline.Error>) inこのようなエラーが発生しました。
Generic type 'Result' specialized with too many type parameters (got 2, but expected 1)Xcodeでエラーを詳しく見てみましょう。
左パネルの①のあたりをクリックすると原因箇所に飛ぶことが出来ます。②のあたりを見てみるとURLEmbeddedViewは独自にResultというenumを定義していますね。
ところでSwiftは5系になってからResultという機能を実装しました。NukeはSwiftのResultを使っています。
SwiftのResultとURLEmbeddedViewのResultが干渉してしまっているのですね。Xcodeから見るとResultと書かれても
Swift.ResultかURLEmbeddedView.Resultかわかんないわけです。なので普段は省略しているSwift.Resultという風にどの名前空間なのかを
明示してあげることでXcodeが迷わなくなるって感じです。というわけで、こんな風にすると修正できました。
Nuke.loadImage( with: imageURL, options: ImageLoadingOptions(), into: cell.fullImageView, progress: nil) { (result: Swift.Result<ImageResponse, ImagePipeline.Error>) in参考資料:
- Result | Apple Developer Documentation
- 【Swift】Result型を使ってみた
- 投稿日:2020-12-04T10:35:16+09:00
コレクションについて〜範囲〜
範囲型 とは
配列型とは、主にRange<Bound>型のことを表しますが、
その他にも様々な範囲型がswiftには用意されています。それらの型には、末尾を含まない
..<演算子
と末尾を含む...演算子
が存在します。
演算子の他にも、両端の境界の有無や、カウント可能かどうかによって分類されます。
囲型 境界 カウント可能 ..<演算子(終了の値を含まない) Range<Bound> 両端 不可能 CountableRange<Bound> 両端 可能 PartialRangeUpTo<Bound> 末尾のみ 不可能 ...演算子(終了の値含む) ClosedRange<Bound> 両端 不可能 CountableClosedRange<Bound> 両端 可能 PartialRangeThrough<Bound> 末尾のみ 不可能 PartialRangeFrom<Bound> 先頭のみ 不可能 CountablePartialRangeFrom<Bound> 先頭のみ 可能 カウント可能な範囲とは、
Int型のように数えられる範囲のことをカウント可能な範囲といいます。範囲型のBoundは、プレースホルダ型となっており、
使用する際は、Range<Float>型やCountableClosedRange<Int>型のように、
Bound型に具体的な型を指定して使用します。範囲演算子
先ほど記載しましたが、範囲演算子は2種類存在します。
末尾を含まない..<演算子
と、末尾を含む...演算子
の2種類です。
どちらの演算子を使用した場合も先頭の値は範囲に含まれます。..<演算子
末尾の値を含まない範囲を表す型には、
Range<Bound>型
、CountableRange<Bound>型
、PartialRangeUpTo<Bound>型
の
3つの型が存在します。Range<Bound>型とCountableRange<Bound>型を生成するには、
左辺に先頭の値、右辺に末尾の値を指定する必要があります。
また、右辺と左辺がInt型の場合はカウント可能なのでCountableRange<Bound>型になります。let range1 = 1 ..< 4 // CountableRange<Int>型 let range2 = 1.0 ..< 4.0 // Range<Double>型カウント可能なCountableRange<Bound>型は、
要素を列挙するためのプロトコルであるSequenceプロトコルに準拠しています。Sequenceプロトコルに準拠している型は、
for-in文を使いその要素に対して順次アクセスできます。let range = 1 ..< 4 // CountableRange(1..<4) for value in range { print(value) } //実行結果 1 2 3for-in文に関しては別の記事で詳しく書いていきますが、
rangeの範囲だけ{ }内の処理を繰り返します。この時にカウント可能でないと範囲を正確に判断できないので、
Sequenceプロトコルに準拠しているCountableRange<Bound>型のみがfor-in文を使用できます。また、..<演算子の左辺の値を指定しない場合はPartialRangeUpTp<Bound>型になります。
この型は、右辺の値未満という範囲を表します。今回の場合ですと、4未満ということになります。
let range = ..<4 // PartialRangeUpTo<Int>...演算子
末尾を含む範囲を表すには、
ClosedRange<Bound>型
、CountableClosedRange<Bound>型
PartialRangeThrough<Bound>型
、PartialRangeFrom<Bound>型
CountablePartialRangeFrom<Bound>型
の5つが存在します。ClosedRange型とCountableClosedRange型を生成するには、
左辺に先頭の値、右辺に末尾の値を記述する必要があります。...演算子の両辺がInt型の場合はカウント可能なので、
CountableClosedRange<Bound>型になります。また、..<演算子と同じく、カウント可能なCountableClosedRange<Bound>型は、
Sequenceプロトコルに準拠しているためfor-in文で要素に順次アクセスできます。let range1 = 1.0 ... 4.0 // ClosedRange<Double>型 let range2 = 1 ... 4 // CountableClosedRange<Int>型 for value in range2 { print(value) } 実行結果 1 2 3 4...演算子の左辺の値を指定しない場合は、
PartialRangeThrough<Bound>型
を生成することができます。
PartialRangeThrough<Bound>型は、右辺の値以下という意味を持ちます。逆に右辺の値を指定しない場合は、
PartialRangeFrom<Bound>型
を生成することができます。
PartialRangeFrom<Bound>型は、左辺の値以上という意味を持ちます。また、他の範囲型と同様に、カウント可能なInt型で生成された場合は、
CountablePartialRangeFrom<Bound>型
が生成されます。型推論
..<演算子や...演算子によって範囲型を生成する場合、
そのプレースホルダ型Boundは、両辺の値から型を推論します。let intRange = 1 ..< 3 // CountableRange<Int>型 let doubleRange = 1.0 ..< 4.0 // Range<Double>型もちろん型を明示的に宣言することもできます。
let floatRange: Range<Float> = 1 ..< 3 // Range<Float>型境界に使用可能な型
範囲の先頭と末尾の型を表すプレースホルダ型のBound型は、
大小関係を比較するためのプロトコルである
Comparableプロトコルに準拠している必要があります。Comparableプロトコルに準拠している型といえば、
Int型やDouble型、String型などがあります。両辺に値を指定する場合は、値の型がBound型という一つのプレースホルダ型で表されるため、
2つの値は同じ型である必要があります。先頭の型がInt型であれば、末尾の型もInt型でないとコンパイルエラーが発生します。
let range1 = 1 ..< 2 // CountableRange<Int>型 let range2 = 1 ..< "a" // コンパイルエラー範囲型の操作
範囲型の境界値には、
lowerBound
とupperBound
を使用してアクセスします。lowerBoundプロパティは範囲の先頭の値を、
upperBoundプロパティは範囲の末尾の値をそれぞれのBound型の値として返します。let range 0 ... 10 // CountableRange(0...10) range.lowerBound // 0 range.upperBOund // 10片側範囲の場合は下記のようになります。
let range = ...5 // PartialRangeThorough(...5) range.lowerBound // 存在しないためコンパイルエラー range.upperBound // 5値が範囲に含まれているかどうかの判定方法も存在します。
範囲方の値が表現する範囲内に、
特定の値が含まれているかどうか判断するcontain(_:)
メソッドを使用します。contain(_:)メソッドは、指定された範囲内に値が含まれているかどうかをBool型の値で返します。
let range = 1 ... 4 // CountableClosedRange(1...4) range.contain(2) // true range.contain(10) //falseこれらが、範囲型の操作になります。
範囲型は、for-in文などでよく使うと思いますので、
覚えておいた方がいいと思います!いろいろな型が存在してややこしいですが頑張りましょう・・・!
以上、最後までご覧いただきありがとうございました。
- 投稿日:2020-12-04T03:21:51+09:00
"private"って一体何者?
ある程度学習が進んでくると『private』という記述に遭遇しませんか。
今回でその正体を明かしていきます。[今回のテーマ]
・いったいどういう役割をするのか
・どんなときに使えば良いのかそもそもSwiftではアクセス修飾子というものを利用できます。これにより、書いたコードの役割を適応させたい範囲を明確に定義することができます。
ちなみに、、
Swiftには3つの修飾子があります。・private
・public
・internal[本題]
❶privateはどんな役割をするのか?=ファイルの外のコードから、クラスやそのメンバー、エクステンション、トップレベルの関数などに対してアクセスできなくする。
❷どんなときに使えばいいの?
=クラスからはアクセスできるけど、外からはアクセスできないようにしたいとき。
have a great day...
- 投稿日:2020-12-04T01:43:44+09:00
【超ミニマム】AWS AppSync + AmplifyでiOSチャットアプリを作る
はじめに
この記事はand factory Advent Calendar 2020 の4日目の記事です。
昨日は@ykkdさんのSwiftlint autocorrectでコードを自動修正するでした!?AWS AppSync / Amplify
チャットアプリをはじめとしたモバイルのバックエンドとしてFirebaseが利用されることが多いと思いますが、
AWSのマネージドサービスには、他のAWSサービスとの連携が容易という大きなメリットがあります。
私自身iOSエンジニアではあるもののAWSの提供しているサービスや構成には興味があったので、
今回はAppSyncとAmplifyを使ってみることにしました。AppSyncとはAWSが提供しているGraphQLでのサービス開発をサポートするマネージドサービスで、
GraphQLで用意されているサブスクリプション機能を使うことでチャット機能を比較的用意に作ることができます。
また、AmplifyはクライアントがAppSyncにアクセスするために提供されているツールで、
iOSではAmplify SDKを利用することで、AppSyncとのデータの受け渡しをSDK側が請け負ってくれます。今回はAWS AppSync + Amplifyで爆速で(?)超ミニマムなチャットアプリを作ってみます。
Amplifyのセットアップ
前提条件
- AWSアカウントを持っている
- Amplifyのセットアップが完了している
- Amplifyを実行するコマンドラインツールのセットアップ、Podのインストールを行います。
- 詳細はこちらを参照ください。
Amplifyのコマンドラインツールで以下を実行します。
Amplifyの初期化
amplify initその後プロジェクト名などの初設定を尋ねられるので回答します。今回は以下のように答えました。
? Enter a name for the project -> SampleChatApp ? Enter a name for the environment -> dev ? Choose your default editor: -> Visual Studio Code ? Choose the type of app that you're building -> ios ? Do you want to use an AWS profile? -> Yes ? Please choose the profile you want to use -> default✅ Amplify setup completed successfully.
と出たら完了です。APIのセットアップ
amplifyのセットアップが終わったら、コマンドラインツールを使ってAPIをセットアップしていきます。
プロジェクトルートで下記のコマンドを実行し、確認内容に沿って回答していきます。
amplify add api再び質問されます。次はAPIについての初設定です。今回のケースの回答は以下です。
? Please select from one of the below mentioned services: -> GraphQL ? Provide API name: -> samplechatapp ? Choose the default authorization type for the API -> API key ? Enter a description for the API key: -> SampleChatApp's API key. ? After how many days from now the API key should expire (1-365): -> 7 ? Do you want to configure advanced settings for the GraphQL API -> No, I am done. ? Do you have an annotated GraphQL schema? -> No ? Choose a schema template: -> Single object with fields (e.g., “Todo” with ID, name, description) GraphQL schema compiled successfully. ? Do you want to edit the schema now? -> Yes // Yesとするとエディタが開き、スキーマを編集できるモデル定義
APIの作成の最後の質問にYesで答えるとエディタが開きスキーマを編集できるようになります。
GraphQLではgraphqlファイルで定義されたスキーマをもとにAPIを作成します。
ここでは簡単にメッセージのモデルを以下のように定義しました。
テキスト、作成日(エポックマイクロ秒を想定)、UserIDの最小限のプロパティです。schema.graphqltype Message @model { id: ID! text: String! createdAt: String! user: String! }スキーマを定義したら、
これまでに作成したスキーマやらAPIの定義ファイルやらのローカルのリソースをリモートにpushします。amplify pushpushに成功すると、例によって質問がなされます。
この回答に基づいて作成したAPIにアクセスするためのラップ処理の種類や命名などが決まります。? Do you want to generate code for your newly created GraphQL API -> Yes ? Enter the file name pattern of graphql queries, mutations and subscriptions -> graphql/**/*.graphql ? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscripti ons -> Yes ? Enter maximum statement depth [increase from default if your schema is deeply nested] -> 2 ? Enter the file name for the generated code -> API.swiftここまで行うとAmplifyのコマンドラインツールにてAPIが作成され、
AWSのコンソール->サービス→AppSyncから確認することができます。クライアント実装
セットアップ
amplify init
の際に作成された、
amplifyconfiguration.json
とawsconfiguration.json
のふたつのjsonファイルを
Xcodeのプロジェクト内に移し替えます。また、アプリ起動時のAmplifyのセットアップ処理としてAppDelegateで以下を実行します。
AppDelegate.swiftimport UIKit import Amplify import AmplifyPlugins @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Setup Amplify do { try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels())) try Amplify.configure() } catch { print("An error occurred setting up Amplify: \(error)") } // 略 return true } }チャット機能実装
チャット機能の実装です。
viewDidLoadでデータソースに保存されたメッセージを取得する
REST APIでのGetはGraphQLではqueryが請け負います。
Amplify SDKではAmplify.API.query(request:)
を実行すして、Messageの配列を取得します。
また、件数を指定するlimitや次の値の参照を持つnextTokenを組み合わせることでページネーションを行うこともできます。ChatViewController.swift@IBOutlet private weak var tableView: UITableView! var messages: [Message] = [] override func viewDidLoad() { super.viewDidLoad() self.fetchMessage() } func fetchMessage() { // Amplify SDK経由でqueryオペレーションを実行しMessageの配列を取得 Amplify.API.query(request: .list(Message.self, where: nil)) { event in switch event { case .success(let result): // GraphQLの場合、Query失敗時のerrorもレスポンスに含まれる switch result { case .success(let messages): self.messages = messages DispatchQueue.main.async { // tableViewを更新 self.tableView.reloadData() } case .failure(let error): // サーバーから返されるエラーはこっち } case .failure(let error): // 通信エラー等の場合はこっち } } }送信ボタンでデータを投稿する
GraphQLにおいてデータの作成、変更などの書き込み操作はmutateが行います。
ChatViewController.swift@IBAction func tappedSendButton() { // キーボード閉じる self.textField.resignFirstResponder() // メッセージ内容 guard let text = self.textField.text, !text.isEmpty else { return } // 送信時間を取得 let createdAt = String(Date().timeIntervalSince1970) // 別管理しているUserID let user = UserIdRepositoryProvider.provide().getUserId() let message = Message(text: text, ts: ts, user: user!) // mutateで新規メッセージを作成 Amplify.API.mutate(request: .create(message)) { event in switch event { case .success(let result): switch result { case .success(let message): print("Successfully created the message: \(message)") case .failure(let graphQLError): // サーバーからのエラーの場合はこっち print("Failed to create graphql \(graphQLError)") } case .failure(let apiError): // 通信まわりなどのErrorになった場合はこっち print("Failed to create a message", apiError) } } // 初期化しておく self.textField.text = "" }データソースの購読
最後にリアルタイムな結果の反映について実装します。
GraphQLではサブスクリプション機能を使うことによって、双方向のソケット通信を実現します。
Amplifyでは、Amplify.API.subscribe()
を実行することで、データソースの変更をレスポンシブに反映できるようになります。ChatViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() self.fetchMessage() self.subscribeMessage() } func subscribeMessage() { // 新たなメッセージの作成を購読する subscription = Amplify.API.subscribe(request: .subscription(of: Message.self, type: .onCreate), valueListener: { (subscriptionEvent) in // 購読したイベント内容をチェック switch subscriptionEvent { // サブスクリプションの接続状態の変更を検知 case .connection(let subscriptionConnectionState): print("Subscription connect state is \(subscriptionConnectionState)") // データの更新を検知 case .data(let result): switch result { case .success(let createdMessage): self.messages.append(createdMessage) DispatchQueue.main.async { // テーブル更新 self.tableView.reloadData() // 最新のメッセージまでスクロール let indexPath = IndexPath(row: self.messages.count - 1, section: 0) self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true) } case .failure(let error): print("Got failed result with \(error.errorDescription)") } } }) { result in switch result { case .success: print("Subscription has been closed successfully") case .failure(let apiError): print("Subscription has terminated with \(apiError)") } } }ひとまず完成
できてるっぽい〜〜〜
やりきれなかったこと
できてるっぽい雰囲気が若干しますが、実は今回ソートができませんでした。
(サブスクリプションで購読したものは時系列順で取得できるのでごまかせるのですが、queryで取得したものはKeyのMessageのIDでソートされてしまい順序性が狂ってしまいます)
時系列順 ID順(再度queryで取得した場合) チャットであれば当然時系列順に並んでほしいところですが、超ミニマムということでお許しください。
(schema.graphqlをいじってソートキーにcreatedAtを指定することはできるのですが、
Amplify SDKで経由でどのように呼び出せばいいのかわからず。。知見がある人がいたら教えていただきたいです。)おわりに
本記事では全く触れられませんでしたが、チャット機能に関しては
- 認証情報との紐付け
- 送信開始時点で送信中というステータスがユーザーに伝わるようにする
- 送信に失敗したときにユーザーに通知して再送信を促す
- 送信中にアプリを落としても送信が行われるようにする
- 画像や動画などコンテンツの拡充
- データの永続化をしてユーザービリティを高める
など考え出すとどんどん検討項目が出てくるので、
SDKにどこまでおまかせするべきなのか難しいところだなと思いました。
とはいえAmplify iOS SDKに関してはまだまだ調査段階なので、
また色々触って理解度深めていきたいところです。最後まで見ていただきありがとうございました?♂️
- 投稿日:2020-12-04T01:02:04+09:00
ios13から導入されたScreenDelegateの概要を3分でさっとおさらい
ScreenDelegateとは?
Xcode11から、新しくプロジェクトを作ると、ScreenDelegate.swiftという新しい設定ファイルが作成されるようになった。
iOS13未満では、AppDelegateがアプリのエントリーポイントだった。
ライフサイクル+セットアップ+画面の表示の全てのコード存在し、AppDelegateは肥大化しやすかった。
しかし、iOS13からは、これが解決する。
AppDelegateに、ライフサイクル+セットアップ。
ScreenDelegateに、画面のライフサイクル。
と、今までのAppledegateの処理を2ファイルに分割し、コードのスパゲッティ化を防ぐ詳細な説明
AppDelegate
アプリのエントリーポイント。
アプリレベルのライフサイクルが呼ばれる。
デフォルトでは3種のメソッドが存在
1. func application(:didFinishLaunchingWithOptions:) -> Bool
2. func application(:configurationForConnecting:options:) -> UISceneConfiguration
3. func application(_:didDiscardSceneSessions:)ScreenDelegate
UIWinndow+UIScreenの処理を担当
画面のライフサイクルを担当(画面の生成、消滅、再生成など)
デフォルトでは以下6つのメソッドが存在
1. scene(:willConnectTo:options:)
2. sceneDidDisconnect(:)
3. sceneDidBecomeActive(:)
4. sceneWillResignActive(:)
5. sceneWillEnterForeground(:)
6. sceneDidEnterBackground(:)大体名前からどのライフサイクルのどの時点で発火するか分かるぐらい良い命名だ。
参考
この記事は、以下の英記事を荒く要約した物です。
https://medium.com/@kalyan.parise/understanding-scene-delegate-app-delegate-7503d48c5445
- 投稿日:2020-12-04T01:02:04+09:00
ios13から導入されたSceneDelegateの概要を3分でさっとおさらい
ScreenDelegateとは?
Xcode11から、新しくプロジェクトを作ると、SceneDelegate.swiftという新しい設定ファイルが作成されるようになった。
iOS13未満では、AppDelegateがアプリのエントリーポイントだった。
ライフサイクル+セットアップ+画面の表示の全てのコード存在し、AppDelegateは肥大化しやすかった。
しかし、iOS13からは、これが解決する。
AppDelegateに、ライフサイクル+セットアップ。SceneDelegateに、画面のライフサイクル。
と、今までのAppledegateの処理を2ファイルに分割し、コードのスパゲッティ化を防ぐ詳細な説明
AppDelegate
アプリのエントリーポイント。
アプリレベルのライフサイクルが呼ばれる。
デフォルトでは3種のメソッドが存在
1. func application(:didFinishLaunchingWithOptions:) -> Bool
2. func application(:configurationForConnecting:options:) -> UISceneConfiguration
3. func application(_:didDiscardSceneSessions:)SceneDelegate
UIWinndow+UIScreenの処理を担当
画面のライフサイクルを担当(画面の生成、消滅、再生成など)
デフォルトでは以下6つのメソッドが存在
1. scene(:willConnectTo:options:)
2. sceneDidDisconnect(:)
3. sceneDidBecomeActive(:)
4. sceneWillResignActive(:)
5. sceneWillEnterForeground(:)
6. sceneDidEnterBackground(:)大体名前からどのライフサイクルのどの時点で発火するか分かるぐらい良い命名だ。
参考
この記事は、以下の英記事を荒く要約した物です。
https://medium.com/@kalyan.parise/understanding-scene-delegate-app-delegate-7503d48c5445