- 投稿日:2020-05-27T20:21:50+09:00
tns run iosよ、あなたはなぜiPad Airを起動するのか?NativeScript-VueのiOSエミュレータの初期デバイスをiPhoneに変更する
TL;DR
package.json
に"scripts": { "ios-iphone": "tns run ios --device 'iPhone 8'" },を追加する
教えてくれた人
https://www.youtube.com/watch?v=o6CALjTUNSQ
tns run iosよ、あなたはなぜiPad Airを起動するのか
NativeScript−VueのアプリをMac OS Catalina10.15.4で開発中にiOSエミュレータを
tns run ios
(デフォルト)やnpm run serve:ios
(vue-cli-plugin-nativescript-vue
でコードシェアリングしている場合)で起動すると、iPad AirのSimulatorが起動する。
しかし私はiPhoneで確認したい。
デフォルトのデバイスの設定を変更するにはどうすればよいのか?
利用可能なエミュレータデバイスの一覧を取得し、好みのデバイスの正式名称をみつけ、それをつかってpackage.json
にnpmのコマンドを追加すればよい。デバイスの一覧を確認する
$ tns device ios --available-devices Available emulators ┌───────────────────────┬──────────┬─────────┬────────────────────┬────────────────────┬────────────┐ │ Device Name │ Platform │ Version │ Device Identifier │ Image Identifier │ Error Help │ │ iPhone 8 │ iOS │ 13.5 │ 77A88E1D-AE2F-4D55 │ 77A88E1D-AE2F-4D55 │ │ │ │ │ │ -91C6-443A655EC2AF │ -91C6-443A655EC2AF │ │ │ iPhone 8 Plus │ iOS │ 13.5 │ 4B3BE0B3-03D6-4D65 │ 4B3BE0B3-03D6-4D65 │ │ │ │ │ │ -997F-6EC1EAFB67BE │ -997F-6EC1EAFB67BE │ │ │ iPhone 11 │ iOS │ 13.5 │ E42203D6-B46A-4B32 │ E42203D6-B46A-4B32 │ │ │ │ │ │ -BC4C-E313010EC310 │ -BC4C-E313010EC310 │ │ │ iPhone 11 Pro │ iOS │ 13.5 │ D3C18B17-25B1-438E │ D3C18B17-25B1-438E │ │ │ │ │ │ -ACFD-9BE0AD63A688 │ -ACFD-9BE0AD63A688 │ │ │ iPhone 11 Pro Max │ iOS │ 13.5 │ FB6AB7FD-F383-4279 │ FB6AB7FD-F383-4279 │ │ │ │ │ │ -9D84-11D0C3600A07 │ -9D84-11D0C3600A07 │ │ │ iPhone SE (2nd │ iOS │ 13.5 │ D3C08BAB-44FF-4716 │ D3C08BAB-44FF-4716 │ │ │ generation) │ │ │ -9155-351ACA2C0C6A │ -9155-351ACA2C0C6A │ │ │ iPad Pro (9.7-inch) │ iOS │ 13.5 │ A2CD1BE4-6E7C-453E │ A2CD1BE4-6E7C-453E │ │ │ │ │ │ -834C-471AC302AB4B │ -834C-471AC302AB4B │ │ │ iPad (7th generation) │ iOS │ 13.5 │ AD6AE69D-351F-491B │ AD6AE69D-351F-491B │ │ │ │ │ │ -8500-275BAFEB5F24 │ -8500-275BAFEB5F24 │ │ │ iPad Pro (11-inch) │ iOS │ 13.5 │ 3E65C5CC-A8DE-4519 │ 3E65C5CC-A8DE-4519 │ │ │ (2nd generation) │ │ │ -AEC7-A7D06437A6A1 │ -AEC7-A7D06437A6A1 │ │ │ iPad Pro (12.9-inch) │ iOS │ 13.5 │ DC3C58C4-9A3D-48F9 │ DC3C58C4-9A3D-48F9 │ │ │ (4th generation) │ │ │ -A0CA-6F978AA4D3BD │ -A0CA-6F978AA4D3BD │ │ │ iPad Air (3rd │ iOS │ 13.5 │ E49C87CF-02ED-4BB1 │ E49C87CF-02ED-4BB1 │ │ │ generation) │ │ │ -9685-88A8F612033F │ -9685-88A8F612033F │ │ └───────────────────────┴──────────┴─────────┴────────────────────┴────────────────────┴────────────┘自分の好みのデバイスをひとつ
Device Name
から選ぶ。ここではiPhone 8
を使うことにしよう。package.jsonを編集する
つぎに、自分のNativeScript-Vueプロジェクト配下の
package.json
をテキストエディタで開く。scriptsのセクションがなければ追加する。あればそこに次の行を追加する。デバイス名は上述の通り'iPhone 8'
になる。"serve:iphone"
の部分は実際にコマンドラインで打ち込む際のオプションであり、好きなように変えてよい。"scripts": { "serve:iphone": "tns run ios --device 'iPhone 8'" },全文は例えばこうなる
package.json{ "nativescript": { "id": "org.nativescript.radsidenavigator", "tns-android": { "version": "6.5.0" }, "tns-ios": { "version": "6.5.0" } }, "scripts": { "serve:iphone": "tns run ios --device 'iPhone 8'" ← added! }, "description": "NativeScript Application", "license": "SEE LICENSE IN <your-license-filename>", "repository": "<fill-your-repository-here>", "dependencies": { "@nativescript/theme": "~2.3.3", "eslint": "^7.1.0", "eslint-loader": "^4.0.2", "eslint-plugin-prettier": "^3.1.3", "eslint-plugin-vue": "^6.2.2", "nativescript-ui-sidedrawer": "~8.0.0", "nativescript-vue": "~2.5.0", "nativescript-vue-navigator": "^1.2.0", "prettier": "^2.0.5", "rxjs": "^6.4.0", "tns-core-modules": "~6.5.0" }, "devDependencies": { "@babel/core": "~7.1.0", "@babel/preset-env": "~7.1.0", "babel-loader": "~8.0.0", "nativescript-dev-webpack": "~1.5.1", "nativescript-vue-template-compiler": "~2.5.0", "node-sass": "^4.7.1", "vue-loader": "~15.9.0" }, "gitHead": "2250137db8c1e0bd0eb543e8e4563cb71480c00d", "readme": "NativeScript Application" }Vue-CLIのコードシェアリング機能を使ってプロジェクトをはじめた場合は
"serve:iphone": "npm run setup-webpack-config && tns run ios --device 'iPhone 8' && npx vue-devtools",を追加すればよい。その場合、scriptsのセクションは次のようになる。
package.json"scripts": { "lint": "vue-cli-service lint", "build:android": "npm run setup-webpack-config && tns build android --env.production && npm run remove-webpack-config", "build:ios": "npm run setup-webpack-config && tns build ios --env.production && npm run remove-webpack-config", "build:web": "vue-cli-service build --mode production.web", "clean:android": "rimraf platforms/android", "clean:ios": "rimraf platforms/ios", "clean:platforms": "rimraf platforms", "debug:android": "npm run setup-webpack-config && tns debug android --env.development", "debug:ios": "npm run setup-webpack-config && tns debug ios --env.development", "preview:android": "npm run setup-webpack-config && tns preview --env.development --env.android", "preview:ios": "npm run setup-webpack-config && tns preview --env.development --env.ios", "remove-webpack-config": "node ./node_modules/vue-cli-plugin-nativescript-vue/lib/scripts/webpack-maintenance post", "serve:android": "npm run setup-webpack-config && tns run android --env.development", "serve:ios": "npm run setup-webpack-config && tns run ios --env.development", "serve:iphone": "npm run setup-webpack-config && tns run ios --device 'iPhone 8' && npx vue-devtools", "serve:web": "vue-cli-service serve --mode development.web", "setup-webpack-config": "node ./node_modules/vue-cli-plugin-nativescript-vue/lib/scripts/webpack-maintenance pre" },実際にiPhoneエミュレータで起動してみる
以降は、
tns run ios
ではなく先述の"serve:iphone"
の部分を使って$ npm run serve:iphone
などとすればiPhone 8のエミュレータが起動する(iPad Airは起動しない)。
コードシェアリング機能を使って上記のように設定した場合も同様にnpm run serve:iphoneとすればよい。コマンド名は自分に馴染むものであればなんでもよいと思うが、チームでやっている場合は共通でひとつ決めてしまえばよいと思う。
- 投稿日:2020-05-27T19:00:18+09:00
UIWebViewを使っているFrameworkを探す
アプリにUIWebViewが含まれている場合、App Store Connectへのアップロード時にITMS-90809の警告がメールで送信されたりするかと思います。
ITMS-90809: Deprecated API Usage - App updates that use UIWebView will no longer be accepted as of December 2020. Instead, use WKWebView for improved security and reliability. Learn more (https://developer.apple.com/documentation/uikit/uiwebview).
警告にもある通り、既存アプリでUIWebViewを利用している場合は2020年12月で受け付けがされなくなるようなので、それまでに対応が必要です。
(参考: ウェブビューを使ったAppのアップデート - ニュース - Apple Developer)アプリ本体のコードに含まれている場合は "UIWebView" のようなキーワードでgrepして探せばいいのですが、依存しているFrameworkの中にあると探しづらいです。
ここでは、nm
コマンドを使ってシュッと探す方法をご紹介します。方法
FRAMEWORK_DIRS=$(find . -name '*.framework') for framework in $FRAMEWORK_DIRS; do fname=$(basename $framework .framework) echo $framework/$fname nm $framework/$fname | grep UIWeb doneこんな感じのシェルスクリプトを作って実行すれば楽に探せます。
解説
nm
コマンドは、オブジェクト内のシンボルをリストするコマンドです。
(参考: nmコマンドでオブジェクトからシンボルのリストを表示 - Qiita)このコマンドを使ってFramework内にUIWebViewのシンボルがあるかを調べることができます。
アプリで使われているFrameworkは複数あることが多く、CocoaPodsやCarthageなどで管理されていることも多いと思います。そのため、シェルスクリプトにしてプロジェクトディレクトリ配下のFramework全てにnm
するようにしています。参考記事
- 投稿日:2020-05-27T17:40:12+09:00
iOSアプリにYouTube動画を埋めこもう2020
iOSアプリにYouTube動画を埋めこんだので、その覚書です。
基本
YouTubeの公式ドキュメントを見ると、YouTube-Player-iOS-HelperというOSSを使うのを推奨されます。
基本WebViewをベースに、いい感じのサイズにしたYouTube動画を埋めこんで、コントローラーをつけるという感じです。(公式ドキュメント)
Embed YouTube Videos in iOS Applications with the YouTube Helper Library(わかりやすいチュートリアル)
[Swift] YouTube-Player-iOS-Helper を使って YouTube 動画を再生してみる2020年に直面した問題
YouTube-Player-iOS-Helperなんですが、メンテナンスされていないっぽいです。
最近AppleからUIWebView完全廃止の連絡があって、WKWebViewに移行したと思うんですが、
YouTube-Player-iOS-HelperはUIWebViewを使っているので、これ使っているとReject理由になります。
(YouTube公式に対応して欲しいですが、その辺はGoogleとAppleなので、あんまり息があってない感じはします)幸いOSSなので、有志の方がWKWebView移行したライブラリを作ってくれていて、YoutubePlayer-in-WKWebViewがあるので、こちらを使いました。
CocoaPodpod "YoutubePlayer-in-WKWebView", "~> 0.3.0"サムネイル画像が自由に調整できない問題
で、ここから実装にあたって苦労したこと。
サムネ画像をバーンってやっぱ表示したかったんですけど、
最初200*200ぐらいでつくろうとしたら、トリミングされた形で表示されました。
YouTubeのサムネイル画像は、下記のサイズで提供されるみたいです。
- 高クオリティ(480x360) ※ width x height
- 中クオリティ(320x180)
- 標準クオリティ(120x90)
- HQ動画の標準クオリティ(640x480)
- FULLHDのクオリティ(1920x1080)
基本iPhoneのサイズを意識すると、中クオリティ or 標準クオリティが多いでしょうか。
(iPadが入ってくると、それ以上のサイズもあるかと思われます)
このサイズに合っていないと、それより一つ大きいサイズのサムネをとってきて、よしなにトリミングするっぽいです。
YouTube-Player-iOS-Helper(から派生したYoutubePlayer-in-WKWebView)だと、
たとえば400x200のViewに中クオリティ(320x180)を引き伸ばして表示、みたいなことはできない模様です。パラメーターが効かない問題
load(withPlaylistId:playerVars:)
のplayerVarsでプレイヤーを制御するパラメーターが色々指定できるんですが、
実際やってみると想定どおり調整できませんでした。まず
autoplay
は効かなかったです。
アプリ側のライフサイクルとかもあんのかな? と思ってますが、ちょっと謎です。
iv_load_policy
(動画アノテーション)も効かなかったですね。
(動画アノテーション:動画の上に出るクリック可能な文字)
playsinline
/controls
は効きました。
modestbranding
(YouTubeロゴ非表示)は効かなかったんですが、
メソッドをload(withPlaylistId:)
にするとなぜかYouTubeロゴが非表示になりました。この辺の挙動はOSS内部の問題なのか、API側の問題なのか謎です。
- 投稿日:2020-05-27T15:30:14+09:00
俺の嫌いなXcodeが5秒でイチオシ開発環境に
iOS開発者の皆さん、毎日、ハッピーな開発ライフ過ごしてますでしょうか。
楽しい開発には快適な開発環境は欠かせませんが、もちろんiOS開発者の皆さんが使うのはXcodeですよね。
いや、俺はAppCodeという人はそっとタブを閉じましょう。JetBrains製品、良いですよね。Android Studio も素晴らしい。どんな言語でも同じ様な操作感で。でもちょっともっさりしてるんだよな。
その点、Xcodeは動きはキビキビしていて玄人プログラマー好み。でも一つ、すごく嫌なところがありました。これさえ直してくれれば最高なのに。
普通、Xcodeって編集する時、複数のタブ開くじゃないですか。自分の場合はこんな感じ。
Storyboardと関連のソースファイルを幾つか。そして、デバッグする時は、気になってるところにブレークポイント張ります。プログラム実行して、さて、気になるところに差し掛かると...
あーこのタブでデバッグ状態になって違うファイル開かれたわー、確かに昔そこにブレークポイント張っとったわー。というか、そもそもなんのファイル見てたかも不明だわー。
ということが、多々ありました。(該当のタブで戻るボタン押せば戻るんですが)
ここが本当に嫌いだった。なんでAppleはこれで平気なの?
平気じゃなかった様です。全然普通に回避できました。いつからだろう。
Xcodeの「Preference ー Behaviors」または「Edit Behaviors」を開きましょう。
RunningのPausesが、ブレークポイントで一時停止した時の設定の様です。
「Show tab named」のところをチェックして名称を入れましょう。
おー、さっきつけた名前で自動でタブが生成されて、編集してたタブはそのままです。
ちなみに、「Play sound」のところを設定すると、一時停止したときにサウンドがなります。同じ様に「Speak announcement using」を設定すると、映画に出てくるハッカーのコンピュータみたいでオススメです。
これ知らなかったでしょ?いや知ってたって!?もしかして、知らなかったの自分だけ??しかしなんで気づかなかったんだろう。
それでは素敵な開発ライフを!
参考
XcodeのBehaviorsを設定してデバッグ時にウインドウを自動で切り替える
https://techracho.bpsinc.jp/wingdoor/2019_12_04/83304
筆者参考
札幌圏でリモート開発、在宅勤務を中心としたシステム開発の会社を経営しています。
- 投稿日:2020-05-27T14:44:43+09:00
SwiftのReverse GenericsというかOpaque Typeの紹介(社内勉強会用)
Swiftとは
Wikipedia より
Swift(スウィフト)は、アップルのiOSおよびmacOS、Linuxで利用出来るプログラミング言語。Worldwide Developers Conference (WWDC) 2014で発表された。アップル製OS上で動作するアプリケーションの開発に従来から用いられていたObjective-CやObjective-C++、C言語と共存することが意図されている。
swift.org Language Guide より
Swift is a type-safe language, which means the language helps you to be clear about the types of values your code can work with.
Type-safe
var id: Int = 123 var name: String = "abc" name = 456 // Compile error: cannot assign value of type 'Int' to type 'String var ids: Array<Int> = [] ids.append(id) ids.append("xyz") // Compile安全に、呼び出し側が指定した型で、Arrayを使用することができる (Generics)
Genericsを使わないと、
class IntStack { var items = [Int]() func push(_ item: Int) { items.append(item) } func pop() -> Int { return items.removeLast() } } var ids = IntStack() ids.push(id) ids.push(name) // Compile error: cannot convert value of type 'String' to expected argument type 'Int'
StringStack
など、利用する型ごとにクラスが必要?
Genericsを使うと
class Stack<Element> { var items = [Element]() func push(_ item: Element) { items.append(item) } func pop() -> Element { return items.removeLast() } } var ids = Stack<Int>() ids.push(id) ids.push(name) // Compile error: cannot convert value of type 'String' to expected argument type 'Int' var names = Stack<String>() names.push(name)
でも逆に、呼び出し元が決めたい場合も
// これを公開し、実際のstructは隠蔽する protocol ChatRoom { var id: Int { get } var name: String { get } } // DM: チャットルーム名は参加者名のカンマ区切り、 fileprivate struct DMChat: ChatRoom { var id: Int var name: String { memberNames.joined(separator: ", ") } var memberNames: Array<String> } // チーム: 名前やアバターアイコンを持つ fileprivate struct TeamChat: ChatRoom { var id: Int var name: String var memberIDs: Array<Int> var avatarIcon: String } func loadDMChat(id: Int) -> ChatRoom { return DMChat(id: id, memberNames: ["Taro"]) } func loadTeamChat(id: Int) -> ChatRoom { return TeamChat(id: id, name: "ACCESS", memberIDs: [100,101,102], avatarIcon: "file") }
loadDMChat()
ではChatRoom
として内部の型は隠蔽したいんだけど、実際に返るのは常にDMChat
。これは擬似コードfunc loadDMChat(id: Int) -> <C: ChatRoom> C { return DMChat(id: id, memberNames: ["Taro"]) }こんな感じに、Reverse Genericsしたい。
Returning an Opaque Type
-func loadDMChat(id: Int) -> ChatRoom { +func loadDMChat(id: Int) -> some ChatRoom { return DMChat(id: id, memberNames: ["Taro"])こう書くことで、コンパイル時にこの戻り値は
DMChat
型とみなされる。// Compile error: cannot convert value of type 'some ChatRoom' to specified type 'DMChat' private let dm: DMChat = loadDMChat(id: 1)仮に
DMChat
型が呼び出し元に見えていたとしても、その型で受け取ることを許可しているわけではない。
何が嬉しいのか
- 型の隠蔽
- オーバーヘッドがない
- どんなオーバーヘッド?
Value type
var name: String = "abc" print(name) // abc var name2 = name name.append("1") print(name) // abc1 print(name2) // abc
- 実は、SwiftのStringはStructで、値型
- 値渡し、つまり、メモリの確保、コピーなどが行われれる
- 実際には、Copy-On-Writeなど最適化されていはいる
https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html
In fact, all of the basic types in Swift—integers, floating-point numbers, Booleans, strings, arrays and dictionaries—are value types, and are implemented as structures behind the scenes.参照型のシャローコピーにより引き起こされる問題(コピー元も変更されてしまう)の解決として、値の変更の容易さや、イミュータブルクラスを都度まるごと作り直すオーバーヘッドを考え、値型がよいという考え方らしい(?)
終わり
参考URL
- 投稿日:2020-05-27T13:29:26+09:00
FirebaseCrashlyticsで強制クラッシュさせる時の方法が変わっていたお話
環境
- Xcode 11.4
- Swift 5.2
- FirebaseCrashlytics 4.0.0-beta.5
概要(2020/05/27時点)
FirebaseCrashlyticsで強制クラッシュをさせようとしたら、以下の二点で微妙に詰まったのでメモ書きとして残しています。
- シングルトンインスタンス取得の方法が変わっていた
- 強制クラッシュを起こすメソッドが消えていたシングルトンインスタンス取得
以前のver
Crashlytics.sharedInstance()現在のver
Crashlytics.crashlytics()クラッシュさせる
以前のver
Crashlytics.sharedInstance().crash()現在のver
公式ドキュメントによると
fatalError()
を使えとのこと。fatalError()引っかかった原因
日本語のドキュメントのみ更新されていませんでした...
- 投稿日:2020-05-27T13:27:52+09:00
Wi-Fiオンオフ両方をこなせるショートカットを作ろう
前置き
iOSショートカットを使うと、Wi-Fiを完全にオフにできるので便利です。
ただ、「Wi-Fiオンの方も作ってホーム画面に配置する」となると二つもアイコン枠が必要なので少し邪魔です。
そんな問題を解決するために、「Wi-Fiオンオフを切り替えられる(=トグル式)ショートカット」を昔作ったので備忘録として書いておきます。レシピ
ショートカットからホーム画面に戻る方法はいくつかありますが、supermamonさんのSpringboardを使って戻る方法がぶっちぎりで早いのでおすすめです。
実際に使ってみた
今回使ったショートカットのURLはこちら
- 投稿日:2020-05-27T12:17:31+09:00
Flutter + Firebase (FCM) でiOSのPush通知の実装
Flutter + FCM (Firebase Cloud Messaging) でiOSのpush通知を実装したのでその際のメモ。
参考にしたドキュメント
基本はFlutterのFCMプラグインのドキュメント通りに進めればできる。
https://firebaseopensource.com/projects/firebaseextended/flutterfire/packages/firebase_messaging/readme/Githubにexampleもある。
https://github.com/FirebaseExtended/flutterfire/tree/master/packages/firebase_messaging/exampleこちらのDevelopers.IOの記事には具体的な実装例が書いてあって、参考になった。
https://dev.classmethod.jp/articles/flutter_fcm_push1/デバッグ
自分の場合はドキュメント通りに進めても何故かPush通知が成功せず。。。
デバッグの仕方もわからず途方に暮れていたが、こちらのGoogle Developersのブログ「 iOS で Firebase Cloud Messaging をデバッグする」 が助けになった。特に以下の2点によってデバッグが効率的に進んだ。
① 以下のコードをAppDelegate.mに追加してdeviceToken(FCM Tokenとは別物)を取得することfunc application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { var readableToken: String = "" for i in 0..<deviceToken.count { readableToken += String(format: "%02.2hhx", deviceToken[i] as CVarArg) } print("Received an APNs device token: \(readableToken)") }② FCMを通さずに、①のdeviceTokenを用いて直接curlでAPNを叩くステップを挟むこと
> curl --http2 --cert ./証明書.pem \ -H "apns-topic: com.example.yourapp.bundleID" \ -d '{"aps":{"alert":"Hello from APNs!","sound":"default"}}' \ https://api.development.push.apple.com/3/device/デバイスID補足
自分の場合はなぜかiPhoneを再起動しないとdeviceTokenが得られなかったので、同じように詰まっている人がいたら試してみてください。
- 投稿日:2020-05-27T11:50:32+09:00
Xcode11でのsetting.bundleのバージョン更新スクリプト
Xcode11からVersionとBuildは、MarketingVersionと、CurrentProjectVersionで管理されるようになりました。
これにより、Settings.bundleのバージョン更新スクリプト変更しないと悲しい状態になるので対応したメモ。APP_VERSION="$MARKETING_VERSION" /usr/libexec/PlistBuddy -c "Set :PreferenceSpecifiers:1:DefaultValue ${APP_VERSION}" "${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}/Settings.bundle/Root.plist" BUILD_NUMBER="$CURRENT_PROJECT_VERSION" /usr/libexec/PlistBuddy -c "Set :PreferenceSpecifiers:2:DefaultValue ${BUILD_NUMBER}" "${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}/Settings.bundle/Root.plist"
- 投稿日:2020-05-27T07:08:46+09:00
Qiita の記事の閲覧数とLGTMの数を表示するiOSアプリを作成
この記事の内容について
Open-Source Source code / オープンソース: https://github.com/mszmagic/Qiita-Contribution-Counter
この記事では、いくつかのヒントや私がアプリで使用した技術について説明しています。どちらかというとケーススタディのようなものです。
- ローカルデバイス上でユーザーのトークンを保存する
- 既存のウェブクッキーを使用してモバイルアプリケーションにユーザーをログインさせる
URLSession
を使用してGET requests
を作る- Swift でJSONレスポンスを解析する
- リモートサーバーから画像をロードする
もちろん、ここで私はすでにあるものを再発明しませんでした、いくつかの既存のオープンソースフレームワークを使用しました。記事の次の部分でそれについて紹介します。
概要
Githubと同じように
Qiita
コントリビューションを表示したかったのです。そこで次のことができるオープンソースiOSアプリを作りました。
1.各記事の合計読み取り回数と読み取り回数を提供する
2.各記事の合計 LGTMs と views を提供する
3.Githubに似たコントリビューションブロックを表示して、当月内に記事を公開した日を表示する
コード構造
View Controller
ここで、ユーザーは
Qiita APIページに進む
をクリックして、新しいAPIトークンを作成できます。Safariのクッキーを自動的に使用することにご注意ください。
ASWebAuthenticationSession
ユーザーの既存のブラウザーセッションのクッキーをここで利用できるよう、
ASWebAuthenticationSession
を用います。そうすればユーザーは再度ログインしなくてもよくなります。guard let authURL = URL(string: "https://qiita.com/settings/tokens/new") else { return } let session = ASWebAuthenticationSession(url: authURL, callbackURLScheme: "") { callbackURL, error in // Handle the callback. } session.presentationContextProvider = self session.start()また、
presentationContextProvider
をセットアップして、ASWebAuthenticationSession
がビューを表示する場所を認識できるようにする必要があります。/* ASWebAuthenticationSession がどこにビューを表示すべきか判断するためです */ extension ViewController: ASWebAuthenticationPresentationContextProviding { func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { return view.window! } }ユーザーのトークンをキーチェーンに保存して、次回ユーザーがトークン値を再度入力する必要がないようにします
通常、アプリは機密情報(トークンなど)をキーチェーンに保存します。キーチェーンの使用に関するAppleの公式ドキュメントは以下のとおりです:https://developer.apple.com/documentation/security/keychain_services
キーチェーンサービスを使用するにはある程度のコードが必要なので、私はGithub上のオープンソースのキーチェーンヘルパーを使用しました。
https://github.com/evgenyneu/keychain-swift
let keychain = KeychainSwift() keychain.set(tokenTextField.text ?? "", forKey: "qiitaToken") tokenTextField.text = keychain.get("qiitaToken") ?? ""userTableView
リモート画像フェッチ
ここでは、
Kingfisher
というオープンソースのフレームワークを使用しました。https://github.com/onevcat/Kingfisher
@IBOutlet weak var profileImageView: UIImageView! //画像をダウンロードして読み込みます if let imagePath = profileImagePath, let convertedURL = URL(string: imagePath) { DispatchQueue.main.async { self.profileImageView.kf.setImage(with: convertedURL) } }self.profileImageView.kf.setImage(with: convertedURL)requestHelper.swift
ここでは、
URLSession
を使ってリクエストしています:let sessionConfig = URLSessionConfiguration.default let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil) guard let URL = URL(string: "https://qiita.com/api/v2/authenticated_user/items") else { delegate?.onTaskFailed(reason: "URL convertion failed!") return } var request = URLRequest(url: URL) request.httpMethod = "GET" // Headers request.addValue("Bearer \(userID ?? "")", forHTTPHeaderField: "Authorization") /* Start a new Task */ let task = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in if let httpResponse = response as? HTTPURLResponse { let statusCode = httpResponse.statusCode if error == nil && statusCode >= 200 && statusCode < 400 { //成功 let allItems = try! JSON(data: data!).array for item in allItems ?? [] { if let id = (item.dictionary?["id"])?.stringValue { self.fetchIndividualArticle(id: id) } } return } } // 失敗 self.delegate?.onTaskFailed(reason: error?.localizedDescription ?? "Unknown error. Please check your token and try again.") }) task.resume() session.finishTasksAndInvalidate()受信結果を解析:
if let fetchedData = data { if let parsedData = try? JSON(data: fetchedData).dictionary { //ユーザー名 let name = parsedData["name"]?.stringValue //説明 description let description = parsedData["description"]?.stringValue //プロフィール画像のURLパス let profileImage = parsedData["profile_image_url"]?.stringValue // completionHandler(profileImage, name, description, nil) } }ここでは、
SwiftyJSON
というオープンソースのフレームワークを使用しました。https://github.com/SwiftyJSON/SwiftyJSON
Githubと同じように
Qiita
コントリビューションを表示したかったのです。ここでは、
LSHContributionView
というオープンソースのフレームワークを使用しました。
- 投稿日:2020-05-27T00:28:11+09:00
"Could not find a storyboard named ‘Main’ in bundle NSBundle"に遭遇した時
環境
- macOS Catalina 10.15.4
- Xcode11.4
- TargetSDK iOS13.4
いつ遭遇したか
iOSアプリを作る際に、最初に表示したい画面の名前を変えたかったので、
Main.storyboard
のファイル名をSample.storyboard
に変えてビルドすると、画面が描画されずにCould not find a storyboard named ‘Main’ in bundle NSBundleと表示されてクラッシュした。
Could not find a storyboard named 'Main' in bundle NSBundle 対処方法を参考に、info,plistの
Main storyboard file base name
を消したり、Sample
に変えても効果なし。解決方法
Xcode全体に
Main
で検索をかけてみると、info.plist内にもう一箇所Storyboardの名前を指定している箇所を見つけた。
Application Scene Manifest
↓
Scene Configuration
↓
Application Session Role
↓
Item 0 (Default Configuration)
↓
Storyboard Nameこれが
Main
になっていたので、Sample
に変更すると、無事にビルドできた。
ファイル名変えるだけなのにこんなトラップがあるとは...