2023年2月1日水曜日

[Xcode Organizer] ipaファイルをexportできない

【問題】

Xcode OrganizerでArchivesを選び「Distribute App」するとエラーが出る

次出るはずの画面はApp Thining(下図)なのだが、エラーになるということは、おそらくプルダウンメニューを作るために走らせているrubyのスクリプトが、途中でコケている模様。

なお残念ながら「Show Logs」を押しても、足がかりとなる情報は何も得られない。


【手がかり】

「/usr/bin/gem list」のコマンドが、正しく動作しないことに気がついた。


【背景】

cocoapodsを入れるために、Rossetaモードで試みたり、brewで導入したりと、環境を悪くしたことに原因があると思われる。


【対処】

「sudo /usr/bin/gem update --system」で、gemの更新を実行(3.0.3.1→3.4.2)。
エラーとなったので、Rossetaモードで実行し直した。
「/usr/bin/gem list」は動くようになり、「Distribute App」は成功するようになった。

【解決までの過程】

Xcodeは、プリインストールされたrubyを使っていると思われる。

brewによる導入のためgemが2つ存在(where gemで確認できる)し、brew側が優先されている場合に、優先されていないプリインストールされた「/usr/bin/gem」側で実行しようとして、失敗しているようだった。

上手くいっている別環境と比べると、「/usr/bin/gem env」で見えるパスに「/usr/bin/〜」が混ざっている点に気がついた。

ここを直せば治りそうなものだが、設定を直す方法がさっぱりわからず、試行錯誤していたところ「ver.3.0.3.xのgemはupdateすべき」というメッセージが出てきたので、試しに3.4.xに更新したら解決した。

改めてenvしてみると「/usr/bin/〜」が

「/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/〜」

に置き換わっていた。

2023年1月22日日曜日

[Xcode] 古いXcodeを使う方法

libstdc++を要求するモジュールをビルドしたいのだが、


の方法でlibstdc++を導入しても、ビルドできるのは、Xcode 13.2.1が最後。


でXcode自体は入手できるが、


というマークとなり、起動すると「最新版を取得せよ」と意地悪される。
この問題を解決したい。


【命題】


古いXcodeでビルドしたい。


【解決策】


ターミナルで、

#!/bin/zsh

/Applications/Xcode_13.2.1.app/Contents/MacOS/Xcode



というスクリプトを書き、例えば「Xcode13.command」という名で保存する。


【使い方】


このコマンドをダブルクリックすると、Xcode13.2.1が立ち上がる。
ただしターミナルは開きっぱなしになる。

Xcode終了時に自動的に閉じるようにするには、以下のようにする。



【残念ながら】


ここまで頑張っても、App Storeへの提出要件が、Xcode14になるとリリースできなくなる。

Xcode13.2.1でしか生成できないものはライブラリとしてFIXしてしまい、Xcode14で改めて外部リソースとして引き込むように、プロジェクトを見直せばいいのかな?

2022年11月21日月曜日

[Xcode, CocoaPods] VenturaでCocoaPodが動作しなくなった


1. 問題点


macOS 13 Venturaにアップデートして、pod --versionとしただけで、

Ignoring ffi-1.15.5 because its extensions are not built. Try: gem pristine ffi --version 1.15.5

Ignoring ffi-1.15.4 because its extensions are not built. Try: gem pristine ffi --version 1.15.4


こんなのがたくさん出るようになった。


2. 処方箋


1. Command Line Tools for Xcode X.Xを忘れずに。

2. Rossetaチェックを入れてターミナルを起動。uname -mでx86_64であることを確認しておくこと。

3. gem listでcocoapodsがあることを確認。入れてない人はsudo gem install cocoapods
 →gemがない人は論外。which gemでプリインストール/usr/bin/gemを確認できるはず。

4. 以下を実行
sudo gem install ffi --version 1.15.4
sudo gem install ffi --version 1.15.5

3. 結果


pod --versionとしても、エラーなくバージョンが表示される。
pod installやpod updateも、正しく動作する。

4. うまくいかない場合


ruby -v →2.6.10、gem -v →3.0.3.1の場合はうまくいったが、別に所有するgem -v →3.3.11の環境では、にっちもさっちも行かなかった。

dolphinicityさんのやり方に従ったところ、成功しました。
→ありがとうございます。


5. 追記


だんだん混乱してきたので、頭の中を整理するために記録しておく。


・そもそもの問題点

Venturaに含まれているruby/gemでは、cocoapodsはインストールできない。

→解決法

brewでrubyをインストールする。


・既に/usr/localにbrew導入済だと以下のエラーが出る

Error: Cannot install in Homebrew on ARM processor in Intel default prefix (/usr/local)!

Please create a new installation in /opt/homebrew using one of the

"Alternative Installs" from:

  https://docs.brew.sh/Installation


→解決法は以下のどちらか

・brewを/opt/homebrewに入れ直す。
・brewを使う時は、我慢していちいちRossetaでターミナルを起動する。

2022年7月18日月曜日

[Xcode13.3] 非消耗型アプリ内課金コンテンツのホスティングサービスの終了


序文


Appleから2月頃以下のようなメールがきた。普段はすぐ捨てるが、なんか引っかかるところがあったので、捨てないで置いておいた。

Title: Upcoming changes to hosting non-consumable in-app purchases.

We’re reaching out to let you know about upcoming changes to in-app purchase content hosting. In Xcode 13.3, we’ve removed the option to upload non-consumable in-app purchase assets for Apple to host. In addition, support for managing these assets in App Store Connect will be removed starting in April 2022.

Existing assets hosted by Apple won’t be affected and you’ll still be able to manage their associated in-app purchase details, such as pricing and availability, in App Store Connect.

Moving forward, we recommend taking advantage of on-demand resources to host in-app purchase assets on the App Store, separately from the app bundle. You can enable smaller app bundles, faster downloads, and richer content.

最近久々に、アプリの更新をしてみたところ、このお知らせの意味する重大な問題にやっと気づいたので、これを解説したい。

何ができなくなるのか


私がリリースしている「学区ビューア」というアプリでは、GIS情報がデカすぎる等の理由から、アプリに内包(バンドル)せず、後からダウンロードできるようにしている。
今まではApple Store Connectを利用して、Appleが用意したサーバーにアップロードできていたわけだが、どうもこのサービスをやめたいらしい。
正確には、新規登録は受け付けない、既存の更新は許すが、アップロードしたいなら13.2以前のXcodeを使うか、別な手段(コマンドライン)でやりなさいということらしい。

私の例でいうとXcode13.2では、


のような画面を拝めたが、13.3以降では機能がなくなったので、プロジェクトをロードした時点でエラーとなる。

代替手段はあるのか


Apple Developer Forums
で議論されている。

Appleの中の人は、ODR(オンデマンドリソース)とやらを利用せよというが、


ODRは概念であって、非消耗型アプリ内課金コンテンツに対して、新しい置き場所が用意されたわけではないようだ。
実際、ODRのドキュメントには「Cloud or Apple Store」と記載されており、今後Apple Storeを利用できないとなると、Cloudしか残らない。
そしてCloudとは何かというと、要は自分でサーバーを用意せいっということらしい。
少なくとも私はそう読み取った。

外部サーバーへの接続、外部コンテンツの取得は、初期の頃はガイドライン的にNGだったはずだが、大手デベロッパーから押されてAppleも徐々に方針転換。
大手は大歓迎だろうが、個人が証明書類が揃ったサーバーを用意してまで、アプリのコンテンツを配信するのかと問われると、それは厳しいとしか言いようがない。

2020年9月30日水曜日

[Visual Studio] Xamarinセットアップ

1.Android,Windows,iOSのクロスコンパイルができてしまう⁉


Visual Studio 2017/2019には、Microsoftが買収したXamarinが入っており、Android、Windows、iOSのクロスコンパイルができてしまうという、夢のような仕組みがあるらしい。
このうちiOSについては、そんなにうまい話ではないことが、露見した。
どうも、Visual Studio for Macを入れたMacが必要で、WindowsのVisual Studioと連携するらしい。
実行ファイルの生成、実行ファイルの実行はMac側で行われ、Windowsは単に窓口ということらしい。
なんかかえって面倒なことに巻き込まれそうなので、iOSはあきらめた。

今回の目的は、既にiOSで動いているアプリを、Androidに移植することであり、どうせならWindows版も同時に作れたらいいなぁぐらいの感じだったので、Android Studioではなく、このXamarinで開発することに決めた。


2.UWPというプロレス団体


さすがにMFCはもう使っていないが、いつもWindowsアプリは、.Net Framework+C#で作成し、Windows Formsアプリとしてリリースしている。
WPFとかStoreアプリという選択肢は見たことがあるが使ったことはなく、UWPという新しいプロレス団体のようなものについては、知る由もない。
とにかくXamarinを使うなら、この団体に加入しないといけないらしい。UIはXAMLで記述しないといけないらしい。


3.「Android SDKとツール」が真っ白


とりあえずMicrosoftのいう通りやってみた。

ビルドは通り、Androidのエミュレータも起動するが、サンプルが実行されない。
なんかよくわからないけど、バージョンが古いとか、Hyper-V使った方がいいとか言われ、「Android SDKとツール」ダイアログに誘導される。
が真っ白で何もでてこなくて困惑。

【処方箋】
歯車マークから、リポジトリをMicrosoft(推奨)→Google(サポート対象外)に変更すると治った。


4.Hyper-Vの有効化


Microsoft様の説明

結論からいうと、Windowsの機能で、Hyper-VをONにすればいい。
しかし、BIOSでVirtualization Technology (VT-X)が無効になっていると、Hyper-Vプラットフォームのチェックボックスがグレーアウトで触れない。


その場合は、BIOSでの設定変更が必要(いまどきの64bit Windows PCのBIOSなら選択肢があるはず)。


5.管理者権限でVisual Studioをつかう


「選択したエミュレーターを起動できませんでした。これはプロセスに十分なアクセス許可がないことが考えられます。VS を管理者として起動すると回避できる可能性があります。」
といわれる。常に管理者権限でないとダメなの?


6.UWPでビルドできない⁉


ソリューションエクスプローラにUWPが出てくるが、移行が必要とか言われる。
そもそも、新規プロジェクト>Cross-Platformで、UWPのチェックボックスが触れない。


Visual Studio InstallerでUWPを全部入れたら治った。


7.XAMLデザイナーって使えないんすか(゚Д゚)ハァ?


どうやって画面作ったらいいんでしょう? しばし茫然。
プレビューはあるらしいのだが、メニューに見当たらない。どこにあるのやら。

よ~く見ると右下に左右分割という小さなボタンがあり、これを押すとXAMLのプレビュー画面がでる。





2020年7月18日土曜日

[App Store Connect] App Storeに掲載する動画(Appプレビュー)をつくる

1. 実機がある場合


iOS12からいいものが実装された。録画ボタンを押すだけ!


あらかじめ15〜30秒以内に編集しておくと楽。

しかし最近の機種は、FPSが30以上になってしまう。
「設定>カメラ>ビデオ撮影」では、スクリーンキャプチャのFPSは制御できない模様。

FPSは30以下にしないとApp Storeにアップロードできないようなので、例えばffmpegで変更する。

コマンド例)ffmpeg -i in.mp4 -r 30 out.mp4


CUIが嫌な人は、下記のiMoveを使ってもいい。


2. 実機がないので、Simulatorを録画する場合


1. QuickTime Playerの「新規画面収録」を使う。


なんか知らないが、キャプチャーサイズは1/2スケールでいいようだ。
iPad用の1200×1600なら、600×800。
【ハマりPoint】収録終わりのボタンは、なぜかメニューバーにある。


2. QuickTime Playerが吐き出したmp4ファイルを、iMovieで編集。



「新規アプリケーションプレビュー」で編集する。

とにかく最終的に共有メニューから「アプリケーションプレビュー」(赤丸)を選びたいので、そうなるように頑張る。
なお、下段の編集タイムライン上をハイライトさせないと、赤丸は出現しない。


【ハマりPoint】このままだと無音なので、音を追加。

無音だとApp Storeにアップロードできない。
瞬間でもいいので、音声(紫丸)を追加すること。


3. iMovieの代わりにKeynote


以前Keynoteでもやったことがあるので、備忘録として記載する。

1. ドキュメントサイズを所望のサイズに設定
2. その上にmp4ファイルを載せる
3. ムービーで書き出す。元ファイルの前2秒、後5秒に静止画が追加される模様。



2020年6月6日土曜日

[iOS13, Xcode11, ShareExtension, 共有] 「〜で開く」ではなく「〜にコピー」にしたい。

1. 共有には2種類ある


自分で実装するまであまり考えたことがなかったのだが、共有には「Clipbox+」と「"Clipbox+"で開く」のように、「で開く」が付かない物と付く物の2種類がある。
そして動きが違う。
赤丸は編集モードのようなものが開くが、青丸はアプリ(ここではClipbox+)に遷移する。


2. Share Extensionを使った方法


赤丸はShare Extensionを実装すると出てくるようになる。
こちらのページが詳しい。

何をしないといけないかというと、Share Extension機能をもった別のBundle ID(例えばClipbox.shareとか)を持つ、新しいアプリを作らないといけない。

この新しく作ったアプリのinfo.plist内NSExtensionActivationRuleで、サポートしている機能を記述することにより、iOSに自分が持つ機能を宣言することになる。
結果、共有ボタンを押した時にiOSが、候補(Suggestions)に、新しく作ったアプリのCFBundleDisplayNameを表示できるようになる。

候補から新しく作ったアプリを押下すると、新しく作ったアプリがキックされ、標準で用意されているPostを押下すると、didSelectPost()でURL名、ファイル名、文字列等が獲得できる。

獲得したものを元のアプリに渡せば一件落着のはずなのだが、実は、新しく作ったアプリと、元のアプリは別のものなので、直接データのやりとりはできない。

この障壁を回避するために、App Groupsと言われる共有空間が用意されている。
新しく作ったアプリと、元のアプリで同じApp Groupsを定義して使う。
Xcode上で実施すればうまくいくはずなのだが、うまく自動生成できない場合は、Apple Developper Program「Certificates, Identifiers & Profiles」のページで手動登録するしかない。

作成した共有空間にデータを書き出す方法としては、ググると、plistでの方法が、

let userDefaults = UserDefaults(suiteName: "group.XXX.XXX")
userDefaults.set(item, forKey: "text")
userDefaults.synchronize()


紹介されているが、後々のことを考えると、ファイルやりとりの方が便利と思う。

let dst = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.XXX.XXX")?.appendingPathComponent("test.txt")
item.write(to: dst, atomically: true, encoding: .utf8)

3. Share Extension :アプリをキックできない


ここまで頑張って実装しても、コード上から元アプリを呼び出すことができないので、元アプリをユーザーが立ち上げなくてはならず格好が悪い。
iOSの仕様変更で今はできない。昔はできていただけに情けない(こちらに詳しい)。

でもAdobeはできてるんだよね。何か方法はあるのだろう。


4. Document Content Type UTIを使った方法


表題の「〜で開く」「〜にコピー」の話だが、Windowsで言うところの拡張子の関連付けみたいなものだろうか、UTIで設定すればいいだけ。
Share Extensionに比べると相当楽。

ただ以下の点でつまづいた。
・public.textを指定しても、標準アプリの「メモ」からは呼び出せない。
・「〜にコピー」にしたいのに、「〜で開く」になってしまう。

それぞれの処方箋は以下のとおり。
【処方箋】標準アプリの「メモ」はそもそもUTIが謎。あきらめてShare Extensionの方法で対応した。

【処方箋】「〜で開く」は、LSSupportsOpeningDocumentsInPlaceを削除すると「〜にコピー」にできる。(*1
iOSは、LSSupportsOpeningDocumentsInPlace=NOが必要。(*2
MacOSは、LSSupportsOpeningDocumentsInPlaceは削除。(*3

*1 以前は、ApplicationDelegateで受け取るために、LSSupportsOpeningDocumentsInPlaceが必要だったが、今は常にSceneDelegateが呼ばれる仕様に変わったようなので、削除することができる。

*2 iOSアプリをAppStoreにあげる時、LSSupportsOpeningDocumentsInPlaceがないと、Document Typeを指定してるくせにと怒られる。

*3 一方でMacOSアプリでは、LSSupportsOpeningDocumentsInPlace=NOは許されないので、削除するしかない。


5. ShareExtention:public.textだけどpublic-file-urlもサポートすべき例


public.text系のアプリから、青丸の「〜でコピー」を選択すればうまくいくのだが、試しに赤丸のShare Extensionも試してみると、UTTypeText(public-text)が空っぽだった。

しかし、kUTTypeFileURL(public-file-url)には、ファイルパスが入っていたので、どうやらこれを使えということらしい。
標準の「メッセージ」や「メール」は、当然のことながら、うまく実装できていた(下図)。



2020年6月2日火曜日

[Swift5, Xcode11] Mac Catalyst で悪戦苦闘

1. archiveできない!?


古いXcodeを使っていた時のくせで、archiveの時は[Generic iOS Device]を選ぶものと思い込んでいた。
でもそれだと、ipaファイルしか生成されない。

【処方箋】正解は、[My Mac]を選んでarchive


2. スワイプできなくねぇ?


SceneKit使っていて、2本指によるスワイプ(位置移動)、ピンチイン・アウト(拡大縮小)ができなかった。ドラッグ(回転)は1本指なのでできる。
ググって見ると、ベータ版の頃から話題になっているよう。重大な不具合に思えるが、現状、直されていないし、制限事項にも上がっていない⁉️

option押しながらのマウス操作で、スワイプ ・ドラッグができるのかと思いきや、なんと用意されていない。
Mac miniとかMac Proでは、入力デバイスとしてタブレットが必要になってしまう。

SceneKit、ARKitを使ったiOSアプリでは、Mac Catalyst対応しました!って能天気にリリースできないという厳しい現実。

【処方箋】別途UIを実装する逃げしかない模様。


3. TableViewも変じゃねぇ?


複数選択(multiple selection)が働かない。

【処方箋】willSelectRowAtとwillDeselectRowAtで、むりやり実装するしか無さそう。

mizuBuro_cureさんの記事が参考になった。

このほかハイライト色(GrayとBlueの差別化)にも問題がありそうなので、複数選択をむりやり実装すると、なんか変な感じになる。
がんばったとしても、開発者のバグをカバーする努力に対して、App Store「アプリ審査」が冷や水を浴びせる可能性が高い。


4. SplitViewの幅おかしくねぇ?


起動時、primary側の幅がせまい。
minimum値を設定しても、それを下回る幅で起動する。

【処方箋】maxとminに同じ値を設定して逃げた。
splitViewController.preferredDisplayMode = .allVisible
splitViewController.minimumPrimaryColumnWidth = 300
splitViewController.maximumPrimaryColumnWidth = 300


5. アプリをクリーンインストールできなくねぇ?


~/Library/Containers/(bundle ID)/Data/Document
~/Library/Containers/(bundle ID)/Data/Library/Application Support/(app)
を参照してるので、アプリケーションフォルダから「通常通り」に削除しても、設定(plistやCoreDataなどなど)は残存してしまう。

【処方箋】Terminalで、defaults delete (Bunndle ID)を実行。


※App StoreでReady for Saleになってから確認したが、正式リリース版では「通常通り」の削除で残存物はなく、クリーンインストールは実現できた。


6. アプリ内課金ができない?


request = SKProductsRequest(productIdentifiers: Set([”XXX.XXX.XXX”]))
request.delegate = self
request.start()

のrequestの永続性が、iOSとは違うみたい。

【処方箋】requestは、SKProductsRequestDelegateを継承しているControllerで保持する。



7. ShareExtensionはむずかしい


私のアプリの作りが悪いのか、NSExtensionPrincipalClassを指定しないと、App Storeに上がらなかった。結果、NSExtensionMainStoryboardは排他の関係となるので、削除した。

MainStoryboardがなくなると、@Objcでクラス名を宣言しないと、OSは呼んでくれなくなる。
例:
@objc(ShareViewController)
class ShareViewController: SLComposeServiceViewController {
...
}


8. 勝手にメニューが作成されてしまう


勝手に作ってくれるのはいいのだけれど、問題だらけ。

1. Bundle display nameが使われない。XXX.appのXXXの部分が使われてしまう。


【処方箋】PRODUCT_NAMEを変えるしかなさそう。


2. File > Preferenceで、Version、Build、Acknowledgementが出てくるが空っぽ。


RunScriptで以下のようなスクリプトを走らせてるんだけど、これじゃぜんぜんダメ。

version=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$PROJECT_DIR/$INFOPLIST_FILE")

build=$CURRENT_PROJECT_VERSION

root=${CODESIGNING_FOLDER_PATH}/Settings.bundle/Root.plist

if test -f $root; then

/usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:1:DefaultValue $version" "$root"

/usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:2:DefaultValue $build" "$root"

fi



【処方箋】以下のようにしてほぼ削除。

extension AppDelegate {

    override func buildMenu(with builder: UIMenuBuilder) {

        super.buildMenu(with: builder)

        guard builder.system == .main else { return }

        builder.remove(menu: .preferences)

        builder.remove(menu: .services)

        builder.remove(menu: .file)

        builder.remove(menu: .edit)

        builder.remove(menu: .format)

        builder.remove(menu: .view)

        builder.remove(menu: .window)

        builder.remove(menu: .help)

    }

}


「Helpがないと審査に通らない」という書き込みも見たような気がするが、とりあえずこれで挑戦してみる。

追記:
Aboutのコピーライトだけは、NSHumanReadableCopyrightで簡単にできた。

2020年5月23日土曜日

[Swift 5, Xcode11, CoreData] UITableViewでSectionタイトルを出す

1. モデルエディタでCoreDataを編集


Sectionタイトルに対応する項目を追加する。
ここでは、sectionName:Stringとし、Default値を「Data」とした。
Transientは任意だが、ONとした。
TransientをONにすると、データベース(sqlite)にカラムは生成されない。


2. コーディング


NSFetchedResultsController作成時に、引数sectionNameKeyPath:で、作成したCoreDataの項目名"sectionName"を渡す。


Sectionタイトル用のtableViewのデリゲートメソッドを使い、.nameを返す。


3. 動作例


Sectionタイトルが現れ、Default値「Data」で分類できた。



2020年3月24日火曜日

[iOS App] フランス語 中国語 ローカライズの効果

1. フランス語のローカライズ


続報です。
一過性ではなく、シェアはそのまま維持されているようです。
さらなる伸びはないですが...


2. 中国語のローカライズ


gengoに700円くらい払って、繁体と簡体のローカライズしてみました。
フランス語よりは効果あるみたいです。
世界シェア的にはもっとあってもいいような気もしますが、中国本土でAppleのアカウント持ってる人(海外に出ることができる限られた富裕層)ってこのぐらいしかいないんですかね。


2020年2月22日土曜日

[iOS App] フランス語 ローカライズの効果

私が配信しているアプリで、唯一のヒット作が Midnight Video Camera


ですが、これを使って多言語化がどれだけ効果があるのか検証してみました。

1. まずは実績


英語をメイン言語にし、日本語を加えて、2年くらい配信しているのですが、アメリカ3:日本7が実体です。イギリス、オーストラリアもあるけど、ほぼアメリカと考えていいようです。


当方としては初めから海外リリースを念頭に取り組んできたので、シェア的には5:5になるべきものが3:7であるという結果は予想外で、原因があれば取り除きたいと考えています。
が、思い当たる節はないんですよね。そもそもアプリ名は英語名しか用意していないし。
App Storeの仕様としか思えないんですがね。

2. フランス語化


買ってくれそうでパイがでかい国となると、ヨーロッパ言語かと思い、手始めにフランス語化してみました。


配信開始月(1月)に明らかに伸びはありましたが、全体(アメリカ+日本)からみると1/50くらいですか。劇的な効果はでませんね。
あとヨーロッパならやってドイツ語か(ブラジルが買ってくれるなら)ポルトガル語もありかと思いますが、やはり中国語ですかね。
App Storeのシェアは、アメリカ:日本:中国=1:1:1らしいんで。

2020年2月11日火曜日

[Xcode11] libstdc++をまだ使いたい

Podで引き込んだライブラリが要求するんで、libstdc++を今でも使う場面があるかと思います。
最終リリースはXcode9なので、そこから抽出する必要があります。

ビルドするための処方箋


1. ヘッダー


/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include
のc++フォルダをまるごと、Xcode11の同ディレクトリにコピー。

2. ライブラリ


/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib
の3つ
libstdc++.6.0.9.tbd (実体)
libstdc++.6.tbd (シンボリックリンク)
libstdc++.tbd (シンボリックリンク)
をXcode11の同ディレクトリにコピー。


iOS Simulatorでデバッグするための処方箋


1. ヘッダー


/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include
のc++フォルダをまるごと、Xcode11の同ディレクトリにコピー。

2. ライブラリ


/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib
の3つ
libstdc++.6.0.9.tbd (実体)
libstdc++.6.tbd (シンボリックリンク)
libstdc++.tbd (シンボリックリンク)
をXcode11の同ディレクトリにコピー。

3. ライブラリ


/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib
の3つ
libstdc++.6.0.9.dylib (実体)
libstdc++.6.dylib (シンボリックリンク)
libstdc++.dylib (シンボリックリンク)
をXcode11の
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib
にコピー。

2019年12月29日日曜日

ipaファイル内のembedded.mobileprovisionから有効期限(ExpirationDate)を読み込む

AdHocやEnterpriseでのリリースでは有効期限があるので、それを取り出し、ユーザーに見せるようにしていた。

embedded.mobileprovisionをNSDataで読み込み、NSStringに全変換してから、NSStringのメソッドで処理していたのだが、暗号化された文字コード以外の値が入っているため、SDK13ではクラッシュを招いてしまった。

反省して、NSPropertyListSerializationで処理する正しい方法に改めた。



-(NSString *)readExpiredDay
{
    NSString *path = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
    if (!path) return @"NG";
    NSData *data = [NSData dataWithContentsOfFile:path];
    
    // plistを抽出
    NSRange start = [data rangeOfData:[@"<?xml" dataUsingEncoding:NSUTF8StringEncoding] options:NSDataSearchBackwards range:NSMakeRange(0, [data length])];
    NSRange end = [data rangeOfData:[@"</plist>" dataUsingEncoding:NSUTF8StringEncoding] options:NSDataSearchBackwards range:NSMakeRange(start.location, [data length] - start.location)];
    NSData *body = [data subdataWithRange:NSMakeRange(start.location, end.location + end.length - start.location)];
    
    // NSDictionaryに変換
    NSPropertyListFormat format;
    NSDictionary *dic = (NSDictionary *)[NSPropertyListSerialization propertyListWithData:body options:NSPropertyListMutableContainersAndLeaves format:&format error:nil];
    if (!dic) return @"NG";
    
    // ExpirationDateを検出
    NSDate *date = dic[@"ExpirationDate"];
    if (!date) return @"NG";

    // UTC->JST変換し文字列とする
    NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
    [fmt setDateFormat:@"yyyy/MM/dd"];
    [fmt setTimeZone:[NSTimeZone defaultTimeZone]];
    NSString *str = [fmt stringFromDate:date];
    
    if (!str) return @"NG";
    return str;
}

swift版のM.Ike様のコードを参考にしました。


[Xcode11 SDK13] UISplitViewControllerでメニューが引っ込まない

Xcode6(SDK8)で非推奨になったのに、放っておいたら、「戻る」ボタンは表示されないし、メニューとして使っているプライマリビューは隠れないし、たいへんなことになってしまった。

willHideViewController:withBarButtonItem:forPopoverController:
のforPopoverControllerでメニューのオブジェクトを取得し、メニューを隠すのに使っていたのだが、SDK13からforPopoverControllerは何も返さなくなってしまった。

修正点は以下の2点。

1. メニューを隠す


縦画面の時だけ隠したいので、端末の向きを、

// NotificationCenter登録


[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changedOrientation:) name:UIDeviceOrientationDidChangeNotification object:nil];

// ハンドラ


- (void)changedOrientation:(NSNotificationCenter*)center {
    switch ([UIDevice currentDevice].orientation) {
        case UIDeviceOrientationLandscapeLeft:
        case UIDeviceOrientationLandscapeRight:
        case UIDeviceOrientationPortrait:
        case UIDeviceOrientationPortraitUpsideDown:
            _orientation = [UIDevice currentDevice].orientation;
            _splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeAutomatic;
            break;
        default: //FaceUp及びFaceDownでは何もしない。
            break;
    }
}

で_orientationを取得した上で、今までどおり、ゆっくり隠したいので0.3秒を指定。

// メニューを押した後に、以下を実装


switch (_orientation) {
    case UIDeviceOrientationPortrait:
    case UIDeviceOrientationPortraitUpsideDown: {
        [UIView animateWithDuration:0.3 animations:^{
        _splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryHidden; }];
        break; }
    default:
        break;
}

2. 「戻る」ボタンを表示する


以下のようなものをviewDidLoadに実装するだけで、あとは自動的にやってくれる

navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem;

はずだったのだが、起動時の一発目に、左上の文字がでないことがあった。
白紙から作り直せばうまくいくのだが、そういう訳にもいかないので、viewDidAppearに以下を実装。

navigationItem.leftBarButtonItem.title = @"●●●";






2019年11月28日木曜日

[swift4] iOSアプリで暗号化したMQTT通信(subscribe)をやりたい

まずPodfileです。


platform :ios, '11.0'
target 'YourProject' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!
  # Pods for YourProject
  pod 'Moscapsule', :git => 'https://github.com/flightonary/Moscapsule.git', :branch => 'swift4'
  pod 'OpenSSL-Universal'
end

次に実装を示します。

import Moscapsule した上で、以下のようにFirstViewController等に記述して下さい。

{
    ...

    private var _timerTimer?
    private var mqttConfigMQTTConfig!
    private var mqttClientMQTTClient!
    
    // MARK: - オーバーライド
    override func viewDidLoad() {
        super.viewDidLoad()
        //MQTTコールバック登録
        moscapsule_init()
        mqttConfig = MQTTConfig(clientId: NSUUID().uuidString, host: "ホスト名", port: ポート番号, keepAlive: 60)
        mqttConfig.mqttAuthOpts = MQTTAuthOpts(username: "", password: "")
        mqttConfig.cleanSession = true
        if let path = Bundle.main.path(forResource: "ファイル名", ofType: "crt") {
            mqttConfig.mqttServerCert = MQTTServerCert(cafile: path, capath: nil)
        }
        mqttConfig.onMessageCallback = { mqttMessage in
            if mqttMessage.topic == "トピック" {
                if let msg = mqttMessage.payloadString {
                    DispatchQueue.main.async { self.onMQTT(msg: msg) }
                }
            }
        }
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        //MQTT接続、一定時間毎にsubscribe発行したいため、タイマーを用いる
        wakeupTimer()
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        mqttClient.unsubscribe("トピック", requestCompletion: nil)
        sleepTimer()
    }
        
    // MARK: - MQTT発行、受信
    func wakeupTimer() {
        mqttClient = MQTT.newConnection(mqttConfig)
        _timer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(onTimer), userInfo: nil, repeats: true)
        _timer?.fire()
    }

    func sleepTimer() {
        mqttClient.disconnect()
        _timer?.invalidate()
    }

    @objc func onTimer() {
        if mqttClient.isConnected {
            mqttClient.subscribe("トピック", qos: 0)
        else {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
                self._timer?.fire()
            })
            print("1秒タイマー動作")
        }
    }
    
    func onMQTT(msg m: String) {
        mqttClient.unsubscribe("トピック", requestCompletion: nil)
        let ary: [String] = m.components(separatedBy: "}")
        for item in ary {
            let data: [String] = item.components(separatedBy: ",")
            if (data.count > 個数) {
                お好きなように!
            }
        }
    }
}

1. 概要

すでにサーバーでbroker(mosquitto)が動作していることが前提となります。
そのbrokerに対して、一定時間(ここでは10秒)ごとに、subscribeし、onMQTTで文字列を受け取ります。
通信が途切れた場合は、1秒後に再接続を試みます。

2. セキュリティ

平文を避けるため、brokerではTLSを動作させるものとします。
たぶんmosquittoの仕様だと思うのですが、TLSでアクセスするには、サーバー証明書が要るようです。
moscapsuleを使った本実装例は、コマンドラインからの
 mosquitto_sub -h ホスト -p ポート -t トピック --cafile ファイル
の実行と、等価なはずです。

3. さらなるセキュリティ

やってないですが、mqttClientCertで、クライアント証明書を指定すればいいようです。
mosquitto_subのオプション--certと--keyを使うことと、等価なはずです。

4. 受信文字列

文字列はカンマ区切りの羅列を想定しています。
JSONを使うのが一般的なようですが、オレオレサーバー<=>アプリ間なら、単なるカンマ区切りがおすすめです。

5. タイマー

別にタイマーである必要はないのですが、iOSアプリでMQTTを受信して何かさせたいとなると、こうする以外ないような気がします。
アプリがバックグラウンドに隠れたり、アプリ内でタブを切り替えたりすることに備えて、AppDelegateに以下の実装も加えて下さい。

    func applicationDidEnterBackground(_ application: UIApplication) {
        デリゲート.sleepTimer()
    }
    func applicationWillEnterForeground(_ application: UIApplication) {
        デリゲート.wakeupTimer()

    }

6. 謝辞

@fuku2014さんの記事を参考にさせていただきました。
https://qiita.com/fuku2014/items/2b8f76c6e60bc9ff14db

なお、brokerの立て方、証明書の作り方等は、以下に詳しいです。
https://ficus-forum.myvnc.com/t/mosquitto-tls-ssl/154
https://qiita.com/udai1532/items/c0f58e73f76900a8469f

2019年11月26日火曜日

[swift4] クライアント証明書が埋めこまれたiOSアプリだけに、httpsリクエストを許す


自分で立てたサーバーに、信頼できるiOSアプリだけにアクセスさせたい。
こんな時にどうするか。

登場する技術要素としては、
1. HTTPS
2. サーバー証明書
3. クライアント認証
があるわけですが、

1.は言わずとしれた暗号化になります。
これは他人の通信を垣間見ることはできなくなる、というだけの話であり、HTTPSで暗号化したから「とにかく大丈夫」という話にはなりません。

よ〜く考えていただきたいのですが、暗号化されたのは電話で言うところの「音声」だけであり、盗聴防止にはなっていますが、そこの電話番号にガンガンいたずら電話をかけることはできるのです。
無言電話を何万回もかけるいたずらもできるし、オレオレ詐欺みたいに家庭の事情をいろいろ聞き出す電話もできます。

電話がかかってくるのは仕方がないが、知らない電話番号には出ない方法はあって、これが3.になります。
クライアント証明書をもっていないiOSアプリからのアクセスについては、サーバーは拒否することができます。

ここではすでにサーバー側が仕上がっているとして、サーバーから払い出したクライアント証明書をiOSアプリに仕込む方法を記述します。

@yosshi_0511さんの記事を参考にさせていただきました。
https://qiita.com/yosshi_0511/items/b0a584166f9ab0dbb94f



ここではエッセンスだけをswift 4で示します。

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
    switch (challenge.protectionSpace.authenticationMethod) {
    case NSURLAuthenticationMethodServerTrust:
        if let trust = challenge.protectionSpace.serverTrust {
            let credential = URLCredential(trust: trust)
            completionHandler(.useCredential, credential)
            print("Serverはなんでも信じる")
            return
        }
    case NSURLAuthenticationMethodClientCertificate:
        if let path = Bundle.main.path(forResource: "ファイル名", ofType: "pfx") {
            if let data = NSData(contentsOf: URL(fileURLWithPath: path)) {
                var items: CFArray?
                let options = [kSecImportExportPassphrase: "パスコード"]
                if SecPKCS12Import(data, options as CFDictionary, &items) == errSecSuccess {
                    if let cfarr = items {
                        if let certEntry = (cfarr as Array).first as? [String: AnyObject] {
                            let identityRef = certEntry[kSecImportItemIdentity as String] as! SecIdentity
                            let credential = URLCredential(identity: identityRef, certificates: nil, persistence: .none)
                            completionHandler(.useCredential, credential)
                            print("Client認証成功")
                            return
                        }
                    }
                }
            }
        }
    default:
        print("その他")
    }
    completionHandler(.cancelAuthenticationChallenge, nil)
    print("キャンセル")
}

これで主題は達成できるのですが、「Serverはなんでも信じる」が気になると思います。
これが冒頭の2.の話です。
あやしいサーバーは排除したい場合は、ここでサーバー証明書のチェックを行います。
フィッシング詐欺サイト等、なりすましサーバーに対する対策となります。

が、自作のiOSアプリは、あらかじめ仕込んだサーバーだけにアクセスするはずなので、このチェックは「普通」は省略できるはずです。
むろん、アクセス先をユーザーが自由に書き換えることができる仕様なら、チェックは必要と思います。