20200527のiOSに関する記事は11件です。

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が起動する。
image.png
しかし私はiPhoneで確認したい。
image.png
デフォルトのデバイスの設定を変更するにはどうすればよいのか?
利用可能なエミュレータデバイスの一覧を取得し、好みのデバイスの正式名称をみつけ、それをつかって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

とすればよい。コマンド名は自分に馴染むものであればなんでもよいと思うが、チームでやっている場合は共通でひとつ決めてしまえばよいと思う。

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

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 するようにしています。

参考記事

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

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があるので、こちらを使いました。

CocoaPod
pod "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でプレイヤーを制御するパラメーターが色々指定できるんですが、
実際やってみると想定どおり調整できませんでした。

YouTube 埋め込みプレーヤーとプレーヤーのパラメータ

まずautoplayは効かなかったです。
アプリ側のライフサイクルとかもあんのかな? と思ってますが、ちょっと謎です。
iv_load_policy(動画アノテーション)も効かなかったですね。
(動画アノテーション:動画の上に出るクリック可能な文字)

playsinline/controlsは効きました。
modestbranding(YouTubeロゴ非表示)は効かなかったんですが、
メソッドをload(withPlaylistId:)にするとなぜかYouTubeロゴが非表示になりました。

この辺の挙動はOSS内部の問題なのか、API側の問題なのか謎です。

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

俺の嫌いなXcodeが5秒でイチオシ開発環境に

iOS開発者の皆さん、毎日、ハッピーな開発ライフ過ごしてますでしょうか。

楽しい開発には快適な開発環境は欠かせませんが、もちろんiOS開発者の皆さんが使うのはXcodeですよね。
いや、俺はAppCodeという人はそっとタブを閉じましょう。

JetBrains製品、良いですよね。Android Studio も素晴らしい。どんな言語でも同じ様な操作感で。でもちょっともっさりしてるんだよな。
その点、Xcodeは動きはキビキビしていて玄人プログラマー好み。

でも一つ、すごく嫌なところがありました。これさえ直してくれれば最高なのに。

普通、Xcodeって編集する時、複数のタブ開くじゃないですか。自分の場合はこんな感じ。
スクリーンショット 2020-05-27 15.16.35.png
Storyboardと関連のソースファイルを幾つか。そして、デバッグする時は、気になってるところにブレークポイント張ります。

プログラム実行して、さて、気になるところに差し掛かると...
スクリーンショット 2020-05-27 15.16.53.png

あーこのタブでデバッグ状態になって違うファイル開かれたわー、確かに昔そこにブレークポイント張っとったわー。というか、そもそもなんのファイル見てたかも不明だわー。

ということが、多々ありました。(該当のタブで戻るボタン押せば戻るんですが)

ここが本当に嫌いだった。なんでAppleはこれで平気なの?

平気じゃなかった様です。全然普通に回避できました。いつからだろう。

Xcodeの「Preference ー Behaviors」または「Edit Behaviors」を開きましょう。
スクリーンショット 2020-05-27 14.44.20.png

RunningのPausesが、ブレークポイントで一時停止した時の設定の様です。

「Show tab named」のところをチェックして名称を入れましょう。
スクリーンショット 2020-05-27 14.45.22.png

すると、同じことしても、
スクリーンショット 2020-05-27 15.17.20.png

おー、さっきつけた名前で自動でタブが生成されて、編集してたタブはそのままです。

ちなみに、「Play sound」のところを設定すると、一時停止したときにサウンドがなります。同じ様に「Speak announcement using」を設定すると、映画に出てくるハッカーのコンピュータみたいでオススメです。

これ知らなかったでしょ?いや知ってたって!?もしかして、知らなかったの自分だけ??しかしなんで気づかなかったんだろう。

それでは素敵な開発ライフを!

参考

XcodeのBehaviorsを設定してデバッグ時にウインドウを自動で切り替える
https://techracho.bpsinc.jp/wingdoor/2019_12_04/83304


筆者参考

札幌圏でリモート開発、在宅勤務を中心としたシステム開発の会社を経営しています。

ローラハウス

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

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

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

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()

引っかかった原因

日本語のドキュメントのみ更新されていませんでした...

Crashlytics 公式ドキュメント(日本語)

Crashlytics 公式ドキュメント(英語)

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

Wi-Fiオンオフ両方をこなせるショートカットを作ろう

前置き

iOSショートカットを使うと、Wi-Fiを完全にオフにできるので便利です。
ただ、「Wi-Fiオンの方も作ってホーム画面に配置する」となると二つもアイコン枠が必要なので少し邪魔です。
そんな問題を解決するために、「Wi-Fiオンオフを切り替えられる(=トグル式)ショートカット」を昔作ったので備忘録として書いておきます。

レシピ

4EF57D4E-F0CF-4C43-8C29-84AD064E93D0.jpeg

ショートカットからホーム画面に戻る方法はいくつかありますが、supermamonさんのSpringboardを使って戻る方法がぶっちぎりで早いのでおすすめです。

実際に使ってみた

48892E2E-2329-4C2D-8C2B-3F9F2331A60C.gif

今回使ったショートカットのURLはこちら

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

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が得られなかったので、同じように詰まっている人がいたら試してみてください。

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

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

Qiita の記事の閲覧数とLGTMの数を表示するiOSアプリを作成

この記事の内容について

Screen Shot 2020-05-26 at 4.54.30 PM.png

Open-Source Source code / オープンソース: https://github.com/mszmagic/Qiita-Contribution-Counter

この記事では、いくつかのヒントや私がアプリで使用した技術について説明しています。どちらかというとケーススタディのようなものです。

  • ローカルデバイス上でユーザーのトークンを保存する
  • 既存のウェブクッキーを使用してモバイルアプリケーションにユーザーをログインさせる
  • URLSession を使用して GET requests を作る
  • Swift でJSONレスポンスを解析する
  • リモートサーバーから画像をロードする

もちろん、ここで私はすでにあるものを再発明しませんでした、いくつかの既存のオープンソースフレームワークを使用しました。記事の次の部分でそれについて紹介します。

Screen Shot 2020-05-26 at 4.54.30 PM.png

概要

Githubと同じように Qiita コントリビューションを表示したかったのです。

Screen Shot 2020-05-26 at 3.59.42 PM.png

そこで次のことができるオープンソースiOSアプリを作りました。

1.各記事の合計読み取り回数と読み取り回数を提供する

2.各記事の合計 LGTMs と views を提供する

3.Githubに似たコントリビューションブロックを表示して、当月内に記事を公開した日を表示する

コード構造

View Controller

Screen Shot 2020-05-26 at 3.59.42 PM.png

https://github.com/mszmagic/Qiita-Contribution-Counter/blob/master/QiitaContributionReport/ViewController.swift

ここで、ユーザーは 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

Screen Shot 2020-05-26 at 3.59.42 PM.png

https://github.com/mszmagic/Qiita-Contribution-Counter/blob/master/QiitaContributionReport/userTableView.swift

リモート画像フェッチ

ここでは、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

https://github.com/mszmagic/Qiita-Contribution-Counter/blob/master/QiitaContributionReport/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 コントリビューションを表示したかったのです。

screenshot1.png

ここでは、LSHContributionView というオープンソースのフレームワークを使用しました。

https://github.com/lucashoeft/LSHContributionView

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

"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の名前を指定している箇所を見つけた。
スクリーンショット 2020-05-27 0.19.25.png
Application Scene Manifest

Scene Configuration

Application Session Role

Item 0 (Default Configuration)

Storyboard Name

これがMainになっていたので、Sampleに変更すると、無事にビルドできた。
ファイル名変えるだけなのにこんなトラップがあるとは...

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