面白きことは良きことなり

拙く未熟なiOSエンジニアの備忘録と戯言

iOS(Swift)から3ステップでMastodonに投稿を行う

流行りのMastodoniOSアプリがAmaroqというアプリしかないため、
作ってみようかなと思い立った。
Qiitaにも投稿済み

その過程で必要なログインと投稿の処理を記載する。
正確には3ステップ必要。
全部POSTのみでいけるため、下記の処理を使用

今回はmastdn.jpを使用しています。適宜変更してください。
ForceUnwrap等セーフティな書き方をしていないのは行数を減らすため、意図的に書いています。
必要に応じてセーフティな書き方をしてください。

準備

// Mastodonのインスタンスから返ってくるjson格納用
var responseJson = Dictionary<String, AnyObject>()

let session = URLSession.shared

// POST METHOD
func post(url: URL, body: Dictionary<String, String>, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws {
    var request: URLRequest = URLRequest(url: url)

    request.httpMethod = "POST"
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)

    session.dataTask(with: request, completionHandler: completionHandler).resume()
}

Step.1 アプリの認証登録

// 登録URL
let registUrl = URL(string: "https://mastdn.jp/api/v1/apps")!

// client_name: アプリ名とかどこからの投稿かわかるための名前
// redirect_uris: redirectしないのでこういうものとしておまじない
// scopes: 権限。今回に関してはwriteだけで足りるが、必要に応じてスペース区切りで追加 例→ "write read follow"
// website: アプリのURLやWebsite。client_nameにこのリンクがつく(省略可能)
let body: [String: String] = ["client_name": "TestApp", "redirect_uris": "urn:ietf:wg:oauth:2.0:oob", "scopes": "write"]

// 登録POST
do {
    try post(url: registUrl, body: body) { data, response, error in
        do {
            self.responseJson = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! Dictionary<String, AnyObject>
        } catch {
        }
    }
} catch {
}
// registのresponseJsonの中身
{
    "client_id": "abcdef123456789abcdef123456789abcdef123456789abcdef123456789abcd",
    "client_secret": "123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234",
    "id": 9999,
    "redirect_uri": "urn:ietf:wg:oauth:2.0:oob"
}

Step.2 ログイン

// ログインURL
let loginUrl = URL(string: "https://mastdn.jp/oauth/token")!

// scopes: 権限。Step.1で取得しているものだけしか使用できないはず
// client_id: Step.1のresponseで取得したもの
// client_secret:  Step.1のresponseで取得したもの
// grant_type: ログインする方式(で合っているのかな?)
// username: 登録したメールアドレス (アカウント名ではない)
// password: 登録したパスワード
let body: [String: String] = ["scope": "write", "client_id": responseJson["client_id"] as! String, "client_secret": responseJson["client_secret"] as! String, "grant_type": "password", "username": "hogefuga@gmail.com", "password": "hogefuga"]

// ログインPOST
do {
    try post(url: loginUrl, body: body) { data, response, error in
        do {
            self.responseJson = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! Dictionary<String, AnyObject>
        } catch {
        }
    }
} catch {
}
// ログインのresponseJsonの中身
{
    "access_token": "abcdef123456789abcdef123456789abcdef123456789abcdef123456789abcd",
    "created_at": 1234567890,
    "scope": "write",
    "token_type": "bearer"
}

Step.3 Tootの投稿

// TootURL
let tootUrl = URL(string: "https://mastdn.jp/api/v1/statuses")!

// access_token: Step.2で取得しているもの
// status: Tootの内容
// visibility: 公開範囲。public, unlisted, private, directとあるが違いは試していないので不明(省略可能)
let body: [String: String] = ["access_token": responseJson["access_token"] as! String, "status": "開発アプリからのテスト投稿", "visibility": "public"]

// Toot POST
do {
    try post(url: tootUrl, body: body) { data, response, error in
        // dataは返ってくるが、投稿できるまでの処理を書くだけなので省略
    }
} catch {
}

Swiftでsuper.init()前にプロパティを初期化しなければいけない理由

前提

Swiftで独自クラスを作成してinitメソッドを書く時、
この独自クラスが持つプロパティの初期化をsuper.init()の後に行って
エラー( Property 'self.property' not initialized at super.init call )に会う人は少なからずいると思う。
f:id:aryzae:20170306014641p:plain

Objective-Cでは、これで問題なかったので、同じ様に書く人は自分以外にもいそう。
そしてエラー内容の言われるがまま、super.init()の前にプロパティの初期化を書いてエラーを回避するのだけれど、そもそもエラーになる理由をわかっていなかったので調べた。

結論

developer.apple.com
AppleのThe Swift Programming LanguageにあるInitializationで記載されていました。
Two-Phase InitializationのNOTEにこう書かれています。

Swift’s two-phase initialization process is similar to initialization in Objective-C. The main difference is that during phase 1, Objective-C assigns zero or null values (such as 0 or nil) to every property. Swift’s initialization flow is more flexible in that it lets you set custom initial values, and can cope with types for which 0 or nil is not a valid default value.

スタジオガラゴさんから同じ箇所の日本語を引用すると

Swift の 2 段階の初期化処理は、Objective-C での初期化に似ています。主な違いはフェーズ 1 で、Objective-C ではすべてのプロパティにゼロまたは null 値(0 または nil)を代入します。Swift の初期化フローは、初期値を変更できるためより柔軟で、0 や nil が有効なデフォルト値ではない型を扱うことができます。


大雑把に自分の解釈も含めて要約すると、Objective-Cの時は自動でプロパティに0とかnilが入ってて問題なかったけど、
Swiftはnilが入らない型とかあるし自動で値入れられないから、初期値いれといてねって話。


まとめると

  • 初期化処理は2段階で行われる
  • Objective-Cの初期化
    • 1段階目([super init]の以前):すべてのプロパティにデフォルト値として0かnilが入る
    • 2段階目([super init]の後):ユーザーによる初期化
  • Swiftの初期化
    • 1段階目(super.init()の以前):プロパティにデフォルト値を入れる必要がある(non-optional型の時、nilがデフォルト値として入らないことによる設計と思われる)
    • 2段階目(super.init()の後):ユーザーによる初期化

引っ越しました

今まで引っ越したいと思っていてずるずる生活してたけど、
1月末から急に引っ越しに意欲的になり、引っ越し先を決めて引っ越しを完了まで1月程度で行った。

そのために2月中はずっとドタバタしてて記事を書くに書けない日々。
ようやく2月末に引っ越しを終え、3月入った今、部屋の片付けは終わってないものの
生活が落ち着き始めたところ。

まだ申し込んだNUROの工事が終わらず、インターネット回線がつながっていないので、
スマホテザリングやTry WiMaxを駆使してネットしている状態。
Try WiMAXレンタル|UQ WiMAX|超高速モバイルインターネットWiMAX2+

一刻も早くNUROが繋がって欲しいところ。(無事工事が終われば今週末には開通予定)

Amazonで買い物した総額を省みる

とあるサイトでAmazonで使った総額いくらかという話題があったので、
気になり自分も調べてみた。

注文回数(回) 合計金額(円)
2006
1
3,585
2007
1
25,800
2008
3
24,271
2009
6
29,626
2010
8
25,515
2011
11
79,021
2012
13
86,035
2013
21
117,603
2014
22
202,593
2015
28
323,804
2016
63
177,745
トータル
177
1,095,598

Amazonを使い始めてから年々回数と金額が順調に増えて、
お世話になりまくりな傾向。

2014〜2015年の金額が大きいのは物語シリーズのBDやARIAのBD-BOX購入が要因ぽい。

2016年で金額落ちたものの注文回数が増えたのは、
音楽をiTunes購入からCD→ALAC取り込みに移行して、
今まで持っていた曲で手に入りにくいものをAmazonマーケットプレイスで買い漁ったからな気がする。
あと技術書をKindleで手出し始めたこともあるか。
ヨドバシで注文したり購入してるのも金額さがっている要因だろうな〜。
PSVRやPS4 Pro買ってるし。

参考にしたサイト
Amazonでの購入総額を調べる方法(2016年11月更新版) | メモトラ

ViewControllerのpresentedViewControllerを辿る際の落とし穴

iOSのプログラムを書いている際、最前面に表示されているViewControllerを取得したいと思うことがある。
この時、rootViewControllerからpresentedViewControllerがnilになるまで取得して、
最前面のViewControllerを求めることを少なからずやったことだと思う。

ただ、これには落とし穴がある。
UISplitViewControllerとUINavigationControllerを使用しているときは、
これらの前面にModal表示している画面しか取得できず、
UISplitViewControllerのMasterやDetail、UINavigationControllerのchildは取得できない。
これらを取得したいときはClassの型を見て処理を分ける必要がある。


私が遭遇した例では、最前面にUIAlertControllerが表示されているかを確認したいという要件だった。
UIAlertControllerは、UISplitViewControllerやUINavigationControllerが表示されていようが、
それらのMasterやDetail、ChildがUIAlertControllerをpresentしていても、
UISplitViewControllerやUINavigationControllerのpresentedViewControllerとして取得できることを確認した。


最前面取ればいいんだろ〜ってことで迂闊にpresentedViewControllerだけ辿ればいいわけじゃないことをテスト検証して知ったので備忘録として残しておく。