20201204のSwiftに関する記事は15件です。

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点に集約させます。

  1. 小さく産んで大きく育てる
  2. 生のユーザーの声を聞きにいく

小さく産んで大きく育てる

(なんかこんなことどこかの大臣が言ってたな...)

私は非エンジニアで全くのプログラミング未経験から始めたので、まず自分に期待することをやめました。
初めてAppStoreにver1.0.0をリリースした時点ではGit管理もしておりませんでしたし、アプリにログイン機能なども実装しておりませんでした。
まずは最低限超えるべきハードルを自分の中で設定し、プラスアルファで実現したいことはその最低限のハードルを超えた後に取り組むLv.2タスクとして設定するよう心がけました。
こうすることでモチベーション維持につながりますし、段階的にレベルアップしていけるので挫折にもなりにくいです。

生のユーザーの声を聞きにいく

吃音者が定期的に集まる自助団体(NPO法人)が各都道府県にあります。
私もたまに参加しているのですが、開発者としてフィードバックを得るために、リリース後の段階でアプリ紹介をしにお邪魔させてもらうことがありました。

基本的にアプリ開発者は、アナリティクスの動向やストア評価を通してユーザーと間接的に接点を持つと思います。
私の場合は、潜在的にユーザーになり得る特定集団が身近に存在していたこともあり、「これ以上的確なフィードバックを得る手段はおそらくないであろう」という思いから、ボロクソ言われるのを承知の上、決死覚悟で飛び込みました。
まぁ案の定ボロクソ言われることもありましたが、反面ポジティブな反響をいただくことも多々あり、今ではやってよかったとつくづく感じています。

学んだこと

これも色々ありますが、特に印象に残ったこととしてウォークスルー画面の重要性について書きます。

初めて吃音の自助団体でアプリ紹介を行なった時、アプリはver1.0.0リリース直後でした。
その頃は機能も限定的だったため、ウォークスルー画面の必要性を感じず、全く実装していない状態でした。

しかしいざダウンロードして使ってもらう段階になると、特にスマホの操作に慣れていない比較的ご年配の方々を中心に
「どう操作すればいいのか分からない」「ここを押すと何ができるのか分からない」
といったご指摘が挙がりました。

これは当然で、個人開発者は自作アプリの機能について100を知っているが、ユーザーは0しか知らないという情報の非対称性が存在することに私が気付いていなかったことが原因です。スマホネイティブではないご年配の方々なら尚更ですね。
「このアプリは何のためのアプリなのか」「このアイコンは何を表すのか」といった、橋渡し的な役割を果たすのがウォークスルー画面です。
「ウォークスルー?別にあってもなくてもどっちでもいいでしょ」という認識だったのですが、この経験を元に、ウォークスルーのようなものは絶対作るべきだと考えが180度変わりました。

今後の課題

RxSwiftを使ってMVVMに書き換えてみる

今RxSwiftを現在進行形で勉強中なのでもう少し時間はかかりそうなのですが、直近の目標として定めています。

ユーザー数を増やす

こればっかりは努力で何とかできるものではないので何とも言えませんが、機能追加や改善・SNSでアピールするなどして頑張っていこうと思っています。

引用

https://ja.wikipedia.org/wiki/吃音症

その他

実は今日誕生日です?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】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」の記述が必要。このようにクロージャを関数に渡すと属性が必要になるが、ここはまだ理解できていない。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)
  • 実装イメージ

Untitled Diagram.png

Codableのコードを生成する

  • ここでは2つのツールについて紹介します。2つともJSONを元にCodableの実装を生成するツールです。

  • quicktype

    • WebでJSONからCodableの実装が生成されます
  • YutoMizutani/JSONtoCodable

    • JSONtoCodableを使うとCodableの実装が生成されます

Kapture 2020-12-04 at 18.21.51.gif

まとめ

  • DTOを使うことでCodableの可読性があがります。
  • 生成ツールを使うとさらに爆速で開発できます

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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を使用する際には
アプリ:iphone: <「欲しい情報がある!」
↓HTTPリクエストを出します
サーバ:robot: <「えええ。しょうがない。あげるよ〜」
↓HTTPレスポンスが返ってくる
アプリ:iphone: <「ありがとー:relaxed:この情報使うね!」

基本的にはこの関係らしい。

リクエスト:iphone::robot:

HTTPリクエストには種類(HTTPメソッド)がある。

「GET」「POST」「PUT」「DELETE」以外にもHTTPメソッドはいくつかありますが、今回この4つを取り上げて說明する理由は、Web開発をする上で比較的多く使われるメソッドであるためです。
特にAPIを使ったWebサービス・アプリ開発を行うときによく使います。
引用元:Web開発でよく使う4つのHTTPメソッド【REST API】

なるへそなるへそ:grinning:(もう死語だったりするのだろう。)

GET:それ見せてよ!ちょうだいよ!
POST:これ登録よろしく!
PUT:これ更新よろしく!
DELETE:これ消しといて!
あと個人的によく聞くけど知らなかったのは
PATCH:これ更新よろしく!
PUTとPATCH、「これ更新よろしく!」なんてシンプルに書いたけど、きっとなにか違うんだろうな。
そのうち調べよう。

とにかくいろんなメソッドがあるみたいです。

レスポンス:robot::iphone:

レスポンスのデータ形式には
HTML
JSON
IMG
XMLなどがあるらしい。
一昔前はXMLが多かったようですが、現在はJSONが主流のようです

JSONとは?
JSONとは?データフォーマット(データ形式)について学ぼう!

同期・非同期通信

よく目にした言葉だけど、まぁ今んとこ関係ないか、、、と放置していたこのワードたち。

同期通信はデータ通信のリクエストを出してからレスポンスが来るまでほかの処理を行わずにレスポンスを待ち続けるが、非同期通信ではレスポンスを待っている間にほかの処理を行える。ほかの処理を行っている際に、レスポンスを受信すると受信処理を実行する
引用元:@IT

このサイトも図がついていて分かりやすかった。
あなたはHTTP通信の説明が出来ますか?同期・非同期の仕組みを解説!

ふむふむ:slight_smile:
一見良さげな非同期通信はデータ量が増えてしまうんですね。
それ以外にもメリット、デメリットあるみたいですが
これについては実際に使用してみて徐々に理解していきたいと思います。

SwiftでHTTP通信をする方法

URLSession
Alamofire
APIkitなどなど
いろいろな方法がありそうです。
これらの違いはいまのところよくわかっていません。

あとは手を動かしながら学んでいこうと思います:relaxed:
後半にいくにつれまとめが雑になっているきがしないでもない。笑

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

APIとは

APIとは??:astonished:

巷でよく聞くAPI。
触れてみたい技術に一つでしたが、まずはじめにAPIとは??状態なのでそこから学んでいきます。
間違っていたらコメントいただけますと幸いです。

Application Programming Interfaceの略で
ソフトウェアの一部を外部に公開することで、第三者がそのソフトウェアの機能を共有して使用できる、というものみたいです。
身近にある例としては、とあるサービスを使用する時にGoogleやFacebookでログインすることができる、というのものがありますよね。これもAPIのおかげのようです。

APIはWeb上に公開されているので、通信を利用するのが一般的なようで、WebAPIともいうらしいです。

はて、、、通信ですと??:astonished:

アプリケーションの区別の仕方はいろいろあるようですが、二つに大別することができます。
・スタンドアローン
・クライアントサーバ

スタンドアローンは、通信なしでアプリケーション単体で動くもの。
クライアントサーバは通信が必要なアプリケーションのこと。
昨今では当たり前のようにサーバとの連携を行うアプリが主流のようです。

サーバとの連携というのは
Webサイト閲覧を例にすると

:confused: 「なんやこの記事、知っとるわい次の違うサイトみてやるわい!」

PC・スマホのブラウザ(クライアント) 「りょ!かえたるわい。次のURLおくるわ。このページの情報くれや!」
↓リクエスト
サーバ 「ほいよ、このサイトの情報あげるわ」
↓レスポンス
PC・スマホのブラウザ(クライアント) 「ほいよ」

:smile: 「これよこれー」

という一連の流れのように
クライアントがリクエストをサーバになげて、サーバからレスポンスをうけとり表示するということをやっています。

APIではブラウザの代わりにプログラムが「この情報が欲しいよ!」とインターネット経由で機能を公開しているサーバに対してリクエストを投げて、プログラムが欲しがっている情報を返してあげる、ということをやっているようです。
帰ってくるデータの形式は今はJSONが多いようです。

なんとなくAPIがなんなのか分かったところで

Swiftでの通信をしたことがないので、これについても勉強しなくてはいけなさそうですね。
こちらについても備忘録がてら別記事にまとめたいと思います:hugging:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.ex
defmodule 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.ex
defmodule SwiftElixirTest do
  @moduledoc """
  Documentation for `SwiftElixirTest`.
  """
end

そして自動生成されたテストコードも修正します。

test/swift_elixir_test_test.exs
defmodule SwiftElixirTestTest do
  use ExUnit.Case
  doctest SwiftElixirTest

  test "greets the world" do
    assert SwiftElixirTest.hello() == :world
  end
end

これを次のようにします。

test/swift_elixir_test_test.exs
defmodule 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 main

Autoconf の初期設定

ここではAutoconfを使ってビルド時の環境を認識するようにします。ただしAutoconfで生成した環境認識スクリプトconfigureは並列ビルドできないという欠点があるため遅いという難点があります。せっかく並列実行に強いElixirなので,将来はElixirで並列実行できるようにしたいですが,将来課題とします。

まず空のconfigure.acを作成します。

configure.ac
dnl 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_makeconfigureを呼ぶ

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.exs
  defp 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.exs
  defp 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.exs
  defp configure(_args) do
    System.cmd("#{File.cwd!()}/configure", [])
  end

そしてmix.exsの下記の部分がプロジェクト情報なのですが,これを書き換えます。

mix.exs
  def project do
    [
      app: :swift_elixir_test,
      version: "0.1.0",
      elixir: "~> 1.11",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

次のようにします。

mix.exs
  def 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.am
AUTOMAKE_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 m4aclocal で設定した値を読み込みます。
  • 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 の値を決めます。ここでは,CFLAGSERL_CFLAGS の値を設定します。ERL_CFLAGS は後で configure.acの中で設定しますが,Erlang が提供するヘッダファイルの情報などを定義します。
    • priv_libnif_la_LDFLAGS で同様にリンクする時の LDFLAGS の値を決めます。ここでは,LDFLAGSERL_LDFLAGS の値を設定します。ERL_LDFLAGSは,ERL_CFLAGSと同様です。動的な共有ライブラリを生成するために,-shared -module -export-dynamic を指定します。.so というようにバージョン番号を記載しないようにするために -avoid-version を指定します。

そしてconfigure.acを次のように変更します。

configure.ac
dnl 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_CCAC_PROG_ARはそれぞれ,CCARで指定されたコンパイラとリンカが存在することを確認します。
  • 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.exsproject情報を次のように変えます。

mix.exs
  def 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.exs
  defp 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.ex
defmodule 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.ac
dnl 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.am
AUTOMAKE_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_H

Objective-Cのコードと言いつつ,ほぼCのコードですが,このcaller関数を起点に任意のObjective-Cのコードを呼び出せると思ってください。さしあたり,Foundationに定義されているNSLogを用いて,Hello, worldしたいと思います。

ビルド・リンクするには,Makefile.amを次のようにします。

Makefile.am
AUTOMAKE_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.sonative/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.ac
dnl 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=noworking_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_FOUNDATIONWORK_NSLOGで真偽値を取り出せるようにしています。

FoundationとNSLogは標準の機能なので,存在をチェックしてコードに反映するのはナンセンスだと思いますが,例としてやってみましょう。

Makefile.am
AUTOMAKE_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_FOUNDATIONWORK_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.swift
import 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.ac
dnl 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.am
AUTOMAKE_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_LIBADDnative/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-DPICswiftc では認識されないので,外す,ということです。

もうちょっとスマートに書けそうに思います。良い方法があったら教えてください。

以上でmix cleaniex -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.ac
dnl 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_OUTPUT
Makefile.am
AUTOMAKE_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
endif
native/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 の支援を受けた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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のサーバー時刻で値が設定されています。

値が決まる前の時刻を使用するかどうかについてはアプリの要件と相談して決めれば良いのではないでしょうか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[事例報告] 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というクラスを
別にアプリ内で定義していたからでした。

気づきにくいバグでした。ご注意を‥。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

複数のライブラリを入れた場合に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でエラーを詳しく見てみましょう。
左パネルの①のあたりをクリックすると原因箇所に飛ぶことが出来ます。

CleanShot 2020-12-04 at 11.png

②のあたりを見てみると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型を使ってみた

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コレクションについて〜範囲〜

範囲型 とは

配列型とは、主に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
3

for-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"   // コンパイルエラー

範囲型の操作

範囲型の境界値には、lowerBoundupperBoundを使用してアクセスします。

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文などでよく使うと思いますので、
覚えておいた方がいいと思います!

いろいろな型が存在してややこしいですが頑張りましょう・・・!

以上、最後までご覧いただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

"private"って一体何者?

ある程度学習が進んでくると『private』という記述に遭遇しませんか。
今回でその正体を明かしていきます。

[今回のテーマ]
・いったいどういう役割をするのか
・どんなときに使えば良いのか

そもそもSwiftではアクセス修飾子というものを利用できます。これにより、書いたコードの役割を適応させたい範囲を明確に定義することができます。

ちなみに、、
Swiftには3つの修飾子があります。

・private
・public
・internal

[本題]
❶privateはどんな役割をするのか?

=ファイルの外のコードから、クラスやそのメンバー、エクステンション、トップレベルの関数などに対してアクセスできなくする。

❷どんなときに使えばいいの?

=クラスからはアクセスできるけど、外からはアクセスできないようにしたいとき。

have a great day...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【超ミニマム】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側が請け負ってくれます。

こんなイメージだと思っています↓
スクリーンショット 2020-12-04 1.26.02.png

今回は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.graphql
type Message @model {
  id: ID!
  text: String!
  createdAt: String!
  user: String!
}

スキーマを定義したら、
これまでに作成したスキーマやらAPIの定義ファイルやらのローカルのリソースをリモートにpushします。

amplify push

pushに成功すると、例によって質問がなされます。
この回答に基づいて作成した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から確認することができます。

スクリーンショット 2020-12-03 22.09.29.png

クライアント実装

セットアップ

amplify initの際に作成された、
amplifyconfiguration.jsonawsconfiguration.jsonのふたつのjsonファイルを
Xcodeのプロジェクト内に移し替えます。

また、アプリ起動時のAmplifyのセットアップ処理としてAppDelegateで以下を実行します。

AppDelegate.swift
import 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.swift
    override 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で取得した場合)
Simulator Screen Shot - iPhone 11 - 2020-12-03 at 23.53.48.png Simulator Screen Shot - iPhone 11 - 2020-12-03 at 23.54.10.png

チャットであれば当然時系列順に並んでほしいところですが、超ミニマムということでお許しください。
(schema.graphqlをいじってソートキーにcreatedAtを指定することはできるのですが、
Amplify SDKで経由でどのように呼び出せばいいのかわからず。。知見がある人がいたら教えていただきたいです。)

おわりに

本記事では全く触れられませんでしたが、チャット機能に関しては

  • 認証情報との紐付け
  • 送信開始時点で送信中というステータスがユーザーに伝わるようにする
  • 送信に失敗したときにユーザーに通知して再送信を促す
  • 送信中にアプリを落としても送信が行われるようにする
  • 画像や動画などコンテンツの拡充
  • データの永続化をしてユーザービリティを高める

など考え出すとどんどん検討項目が出てくるので、
SDKにどこまでおまかせするべきなのか難しいところだなと思いました。
とはいえAmplify iOS SDKに関してはまだまだ調査段階なので、
また色々触って理解度深めていきたいところです。

最後まで見ていただきありがとうございました?‍♂️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む