20190625のiOSに関する記事は14件です。

iOS Appバンドル公開時のメモ

  • 単体アプリと同様の審査が必要。
  • 審査通過後は“販売可能“のOnでリリースとなるので、審査提出時は”販売可能”をOffにしておいた方が良さそう。
  • 審査通過後もバンドル名、説明、URLなどは変更可能。
  • スクショは単体アプリのを全部取ってくるが、バンドル内では変更ができない。
  • バンドルの価格は、一番高いアプリの価格から全バンドルアプリの合計価格の範囲内で設定可能。
  • バンドル内のアプリを所有している場合は、差額がバンドル価格となる。既に支払額がバンドル価格を超えている場合は 価格表示が”complete” となり、無料でバンドルを購入可能。
  • 単体アプリページで購入してしまうと、バンドル価格は適応されないので注意。その際はユーザーが Apple に申し出れば返金はしてくれるとのこと。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

bind(バインディング)の概念について(Rxswift)

bind(バインディングってそもそも何)

Rxswiftの.bindなどで用いられるバインディングですがプログラムに置き換えると結局どういうことなの?
と感じたのでまとめました
・バインディングは.bindしなくてもそもそもできるもの
Timerと似たようなもんで監視対象が時間ではなくViewなどになっただけ

// 1秒'たったら'getInfoを実行する
let timerGetInfo = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(getInfo), userInfo: nil, repeats: true)
timerGetInfo.fire()
// hugaを満たしたらhogeにバインディング
.map { huga }
.bind(to: hoge) 

・まぁ用は紐付けるぐらいに考えとけばいい
→深く掘り下げすぎなくても分からなければならなくなってからやってったらいい、キャリア積んだ人でもこの辺のことは探り探りで進めていくことも多い

結論

あんま気にすんな

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

bind(バインディング)の概念について(Rxswift)(kboy流)

bind(バインディングってそもそも何)

Rxswiftの.bindなどで用いられるバインディングですがプログラムに置き換えると結局どういうことなの?
という疑問がググってもなかなか消化できずkboyさんにお聞きしたところ
・バインディングは.bindしなくてもそもそもできるもの
Timerと似たようなもんで監視対象が時間ではなくViewなどになっただけ

// 1秒'たったら'getInfoを実行する
let timerGetInfo = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(getInfo), userInfo: nil, repeats: true)
timerGetInfo.fire()
// hugaを満たしたらhogeにバインディング
.map { huga }
.bind(to: hoge) 

・まぁ用は紐付けるぐらいに考えとけばいい
→深く掘り下げすぎなくても分からなければならなくなってからやってったらいい、キャリア積んだ人でもこの辺のことは探り探りで進めていくことも多い

結論

あんま気にすんな

kboyさん

ARKit開発は国内トップの凄腕iOSエンジニア
MENTAというサービス内でiOSアプリ開発のメンターをされています(1万円/月)

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

iPhoneでテザリング時にUIの表示崩れを見かけたので原因を探ってみる

iPhoneでのテザリング

iPhoneでのテザリング時には画面上部にOS側で使用する領域が増える。
インターネット共有: X台接続中の分、画面が縦に狭くなる。

通常時 テザリング時
Screen Shot 2019-06-25 at 17.01.05.png Screen Shot 2019-06-25 at 17.00.28.png

これが影響し、以下のような挙動のアプリを割と見かける。
- スプラッシュ画面のロゴの中段が潰れる
- スクロールが最上部/最下部までスクロールできない
- 画面上部が潰れる
- フッタボタン群が一部しか表示されない
- 下スクロール時にフッタボタン群が隠れるような動作をする場合、上スクロールでフッタボタン群が一部しか表示されない

通常時 テザリング時
Screen Shot 2019-06-25 at 16.54.36.png Screen Shot 2019-06-25 at 16.55.00.png

原因を推測してみる

表示だけの問題で、UI操作自体には問題ないようなので、
高さ取得に問題があると推測する。

検証

コードで通常時/テザリング時の画面の高さを取得する。
検証時の端末:iPhone 8 Plus(iOS12.3.2)

高さ取得 コード 通常時 テザリング時
画面の高さ UIScreen.main.bounds.size.height 568 568
ディスプレイの高さ UIScreen.main.nativeBounds.size.height 1481 1481
基底viewの高さ ViewController#view.frame.height 568 548

違いがあるのは、基底viewの高さのみ。

iPhone Xシリーズでの検証をしていませんが、
見た目では、テザリング時にOSが使用する領域は変わらないため、
表示上の問題はなさそうです。
(テザリング時はSafe Areaの表示領域が変わらず見た目が変わるだけです)

対策案

(実際にコードを書いて対策案を検証した訳ではないため、現段階では案のみです)

画面の大きさを取得し、それを元に表示領域を判定する
テザリング時に表示領域が狭くなった場合に、
画面自体の大きさは変わっていないため、
表示やUI操作に問題が発生する可能性があります。

また、アプリ画面表示中に別のiPhoneからの操作により、
突然テザリング時の表示に切り替わるため、
画面初期表示時の基底viewの高さもあてになりません。

これらに柔軟に対応するためには、
AutoLayoutNSLayoutConstraintで画面を定義し、
自動的にサイズや位置を調整したり、
NSLayoutConstraintで上限/下限などを定義などの対策になると思われます。

追記:対策案2

  • UIApplication.didChangeStatusBarFrameNotification

OSが使用するステータスバーの表示領域変更を検知できるので、
アプリ画面表示中の突然のテザリング表示への切り替わりに対応できます。

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

TwitterのiOSアプリにおけるEmpty StateのUIまとめ

これは?

Empty StateのViewを作るにあたってTwitterを参考にしたのでその備忘録。

そもそもEmpty Stateとは?

表示するコンテンツが無い状態のことで、UIの設計において見落とされがちだが新規ユーザーに対してはかなり重要な役割を担う部分です。

参考: https://material.io/design/communication/empty-states.html

TwitterにおけるEmptyState

何もオプショナルな情報を入力しなかったテストユーザーを作成して色々見ていきます。

ホームと通知

ホーム 通知(すべて) 通知(@ツイート)
IMG_1125.PNG IMG_1126.PNG IMG_1127.PNG
アイコンとかあっても良さそうだが、意外とシンプル 他のアプリだと運営などからの「ようこそ」的なメッセージがデフォルトで入ってたりもしますよね 通知(すべて) と同じ

メッセージとフォロー/フォロワー

メッセージ フォロー中 フォロワー
IMG_1128.PNG IMG_1129.PNG IMG_1130.PNG
悲しい一文とともに友達を探すための導線が表示される アカウントを探す導線が表示される 特に導線はない

ユーザー

ツイート

最初はプロフィールの入力が促される
IMG_1131.PNG

プロフィール入力が終わるとツイートを促すボタンに変わる
フラグとかがあるんでしょうね、丁寧

ツイート ツイートと返信 メディア いいね
IMG_1139.PNG IMG_1140.PNG IMG_1137.PNG IMG_1138.PNG
ツイートへの導線が表示される 同じく 少し文言は違うがツイートへの導線が同様に表示される 特に導線はない

他人から見た時のユーザー

フォロー/フォロワー

フォロー中 フォロワー
IMG_1144.PNG IMG_1143.PNG
普通 普通

ツイート

ツイート ツイートと返信 メディア いいね
IMG_1141.PNG IMG_1142.PNG IMG_1135.PNG IMG_1136.PNG
おすすめのユーザーが表示されるだけ(他のユーザーでも表示される)で特に何も無い 同じく 普通 普通

参考

Pinterestで「Empty State」で検索をかけると色々出てきますのでそちらも見てみると良さそうです。
https://www.pinterest.jp/search/pins/?q=empty%20state&rs=typed&term_meta[]=empty%7Ctyped&term_meta[]=state%7Ctyped

まとめ

Empty Stateの表現として、 「アイコン + 文言 (+ あれば導線)」 というのが一般的な気がしました。
Twitterは意外とシンプルな表現でしたがしっかりと新規を導く導線が張られていて参考になりそうです。

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

2019/06/20 CICDTestNight #4 行ってきた

=====

2019/06/20(木)
19:00 〜 22:00
@渋谷ヒカリエ21F DeNA

https://testnight.connpass.com/event/130465/

ハッシュタグ #cicd_test_night

地味なテーマ(司会者談)ながら、回を重ねるごとに参加者数が伸びている、らしい。私は初参加。

先に所感

  • 『パイプラインファースト』広めたい
    • 『至高のCI/CDパイプラインを実現する5つの約束』は皆様読んで欲しい
  • モバイルアプリ界隈の話題が多かった、勉強にはなった
    • Bitrise とかMacOSビルドとか
  • SETやSREの人ばかりが登壇
    • 総人口は多くないにせよ、アピールしてくるなぁ(いいなぁ、羨ましいなぁ)
  • TEKTON ちょっと気になる

hisa9chi:「MacStadium使ってみた」

DeNA SWETグループ所属の井口恒志さん
CircleCI Japan Usesr Group コミュニティリーダー

https://speakerdeck.com/hisa9chi/macstadiumshi-tutemita

  • 課題
    • 物理リソース
    • 物理マシンの設置スペース
    • 電源容量問題
    • 調達速度
    • 開発者の手元に届くまで約1ヶ月待つこともあり
      • ハイスペック・カスタム時
    • 管理コスト
    • OS/SWのアップデート
    • 定期的な停電対応
  • そこで
    • オンプレの物理マシンではなく、クラウドサービスの活用
    • MacStadium 使えるのでは?
  • MacStadium
    • macOS物理マシンのホスティングサービス
    • 物理マシンお払い出し
    • 特徴
    • 他の類似サービスに比べ、ハイスペックマシンもOK!
    • マシン管理はWebUIのみ
    • サポートは24x7
    • 料金体系
    • 1マシンあたり1ヶ月定額(前払い)
    • 1台単位での月定額制
      • 追加したマシンを返却→追加すると2台分かかる
    • データセンター
    • 場所による料金の変動なし
    • Add-Ons
    • 追加IPやファイアウォールも可能
    • サポート
    • 電話
    • Live Chat
    • 問い合わせチケット
  • 基本操作
    • マシンの払い出し
    • ログイン:パスワードは払い出し時に生成されるチケットに記載される
    • WebUI上から、Macの起動停止ができる
    • 利用料金の確認
    • ユーザの追加・ロールの設定
  • Jenkins Slave環境構築CI
    • SWETでは、社内的にJenkinsのつらみを軽減する施策を実施中
    • MacStudioでJenkinsスレーブ用環境構築用のマシンを調達
    • アクセス制御
    • plistを使った(ファイアウォールのお金を今回はケチった)
    • アクセスはVNC or SSH
    • OpenVPNでMacStudioからGHEにアクセス
    • AWS上にVPNサーバを用意、ふむふむ
    • 動作速度は(ハイスペックなので)快適だった
  • まとめ
    • 試してみただけ、なので実運用に耐えるかは引き続き評価

Macのホスティングサービスというのも出てきているんですね。今回はMacOS系のビルドサーバ用途ということでしたが、弊社でもニーズがあるのでしょうか?
社外からのGHEアクセスは事例は興味深いものがありました。セキュアに、そういった利便性を確保していきたいですね。

manabusakai:「CI/CD パイプラインを最速で組み立てるための 4 つのポイント」

https://speakerdeck.com/manabusakai/four-points-to-assemble-the-ci-cd-pipeline-fastest

  • freee のSREの人
  • 資料
    • TBD
  • 昨年のリリース数 278件(ほぼ毎営業日)
  • 「パイプラインファースト」をやっているか?
    • まずはアプリを作り始める前に、パイプラインを組み立てる
    • Web TBD
  • 今回はCircleCI前提
    • ポイント1:いきなりCIサービス上で試そうとしない
    • ローカル環境の circleci コマンドで動作確認/デバッグできる
    • 詳細はQiitaに記事がある
    • ポイント2:依存関係のインストールは事前に済ませる
    • パイプラインの中で依存関係のインストールをしない
    • キャッシュなど、設定ファイルが複雑化する
    • ビルドの依存関係をすべてインストール済みのDockerイメージを用意して pull するだけ
    • ポイント3:車輪の再発明やコピペを避ける
    • 既存の設定ファイルのコピペが散乱
    • CircleCI Orbsを活用する(共通機能がパッケージとして提供される)
    • ポイント4:CIとCDを適切に分離する
    • CIの延長線上でCDパイプラインを構築しがち、複雑化
    • 迅速にRevertするのが難しい
    • CIサービスに強力な権限を付与しないといけない
    • CIからのWebhookをトリガーにCDをキックする
      • Kubernetes環境なら GitOps
  • まとめ
    • (ポイントの列挙)

『パイプラインファースト』という言葉があるんですね。言わんとしているところは激しく同意です。クリーンビルド&デプロイができる環境が整ってからアプリ開発を始めようぜ、と。
プレゼン内で紹介されている 至高のCI/CDパイプラインを実現する5つの約束 も、ぜひ社内に広めていきたい考え。

TaiheiMishima:「Voicy iOSのCI環境をゼロから整えた話」

https://speakerdeck.com/tihimsm/iosfalsecihuan-jing-wozerokaragou-zhu

  • Voicy「音声xテクノロジー」の会社
  • CI環境を整備した経緯
    • iOSアプリの開発をしていたが、CI環境がなかった...
    • 壮絶な手作業によるリリース
    • 新規開発が迫ってくるヤバイ
    • CI環境を作るぞ
  • やったこと
    • 順番めちゃくちゃ、1.5ヶ月くらいかかった(CI素人だったので)
  • まずはここから整えよう
    • テスト配信/プロダクション配信の自動化
    • PM/QAに対して、開発者ローカルで動作確認をする必要があったので優先
    • 簡単に使える Bitrise を使った
    • ビルドコンフィグ系
    • 証明書まわり
    • iOSの設定まわりは面倒くさい
    • 証明書の更新とかとか
    • 静的解析
    • SwiftLint / Danger
  • まとめ
    • ↓の順番でやるべき
    • ミスをしちゃだめなところ
    • 工数が大きいところ
    • コミュニケーションコストがかかるところ

この後のプレゼンでも度々出てくる Bitrise - Mobile Continuous Integration and Delivery は全然知りませんでしたが、モバイルアプリ向けのCI/CDツールとして定評があるみたい。
モバイルアプリ界隈の知見はほとんど持っていないので、ふむふむ、な内容が大半。

ikesyo:「Travis CIのBuild Matrixを活用して、Swift製ライブラリをLinux対応させる」

https://speakerdeck.com/ikesyo/travis-cifalsebuild-matrixwohuo-yong-site-swiftzhi-raiburariwolinuxdui-ying-saseru

  • はてな の人
    • Swiftコミッター
  • Swift のライブラリ作成
  • Swift Package Manager(SwiftPM)
  • Server-Side Swift
    • Swift on Linux
    • Swiftは実はLinuxで動く
    • 普通のSwiftプログラマーは macOS + Xcodeで開発してる
    • ローカル開発はDocker
      • swift-docker
    • 複数のSwiftバージョンでテスト
      • 手動ではやってられないのでCI
    • Travis CI
    • Build Matrix
      • 2x2x2 の8通りのOSバージョン/Swiftバージョン/環境でテストする、みたいな
    • ビルド環境にLinuxだけじゃなくmacOSも使える
    • バージョンマネージャ(swiftenv)
      • Travis CIのジョブでDockerを使うのは結構面倒だから使ってる
  • Bitrise
  • CircleCI
  • Azule Pipeline
    • でも使えるよ

SwiftはLinux上でも動くんだ、ふ〜ん。Travis CIのBuild Matrix的な仕組みは、昨今どのCIサービスでも似た機能が実装されているので、多様な環境でのビルドテストが必要な場合に重宝しそう。

ShuheiKitagawa:「5分でわかった気になるTEKTON」

  • freeeの人、SRE
  • CD.FOUNDATION
    • にホストされることになったプロジェクト TEKTON
  • TEKTON
    • Knativeのサブプロジェクト、Knative Build pipeline
  • Tekton Pipeline
    • k8sのネイティブリソースを用いたCI/CDコンポーネント
    • Pipelineを既存のPipelineの実行エンジンとして使える
    • パイプライン実行部分の標準化(CIサービスごとの際を無くしたい)
  • 現在
    • v0.4
    • 2019年中には v1.0
    • 今はまだ機能が不足しており、実務で使うには時期尚早
    • 今なら開発者として参加するのがおすすめ

Kobito.wNCbUr.png

資料が公開されていませんが、気になったセッション。まだ実用化に足る段階ではないものの、CI/CDパイプラインのエンジン部分を標準化しちゃおうぜ、というもの。これが実現できると、どのCI/CDサービスを使っていても同じ定義で実現できるようになるので、利用者としては嬉しい一方で、サービスの差別化も難しくなってきそうだな、と。

Yuki Tamazawa:「ソフトウェア品質を支えるE2Eテストのビルドパイプライン作り」

https://speakerdeck.com/srea/sohutoueapin-zhi-wozhi-eru-e2etesutofalsepaipurainzuo-ri

  • JapanTaxi の人, SET
  • SETは3名体制(社員)
  • JapanTaxiの紹介
  • やっていること
    • QA体制の構築
    • テストプロセスの確率・運用・改善サイクル
    • 組織横断・複数プロダクト
    • シフトレフト
    • E2Eテストの自動化
    • メインシナリオが正しく動くことを常にチェック
  • 実現したいこと
    • ビルドは bitrise
    • E2Eテストの実行環境は、社内のMac
    • テストしたら結果をSlack通知
    • URLからテスト結果の詳細を確認
  • 行灯ラボ(Blog)をよろしく

ここでも出てきた Bitrise。そして TestRail、これはテスト管理ツールだそう。以前、弊社内でCATを試して花咲かなかったことがあったようですが、Excelでテスト仕様書を書くのはよいとしても、テスト管理はWeb化リアルタイム自動化したいところなので、これも検討してみてもいいかもしれない。

ikuma_hayasaka:「老後必要資金2000万円時代に送る、Azure Pipelinesによる継続的テスト」

https://speakerdeck.com/ihysk/continuous-appium-e2e-testing-on-azure-pipelines

  • SET @ Sony Interactive Entertainment
  • こんな時代にCI/CDにお金を払っている場合なのか
    • Privateリポジトリ運用はCIビルドにお金がかかる
  • そこでAzule Pipelines
    • Microsoftの優しさ
  • 個人開発者が無料枠で試してみた
    • 試験対象は個人開発のiOSアプリ
    • pipeline は yml ファイルで書く
    • 実際はGUIエディタでもできる
    • テストは失敗しがち、デバッグしづらい
    • テスト実行中の録画ができるAPIがある
    • Azule Pipelineなら無料で結構できる!

やはりモバイルアプリ開発なみなさまはmacosでビルドしたくてBitriseなんだなーとしみじみ。Azule Pipelineを推している人は、他の登壇者でもいたので、今後ぐぐっと勢力を広げてくるかも、ですね。

mats:「expoアプリ開発におけるCI/CD」

(残念ながら登壇者が欠席)

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

Asset Catalogで同じ名前の別画像を利用する

はじめに

  • Xcodeのアセットカタログ(デフォルトではAssets.xcassets)は、画像などのリソースを管理します
  • アセットカタログ内で、フォルダ分けも可能です
  • その際、別のフォルダに同じ名前の画像を置きたくなることもあります
  • その場合の扱い方です

検証環境

  • Xcode 10.2.1
  • iOS 12.2
  • Swift 5

フォルダにNamespaceを付与する

デフォルトの状態(Namespaceなし)

no_namespace.png

  • この画像の例では、birdという画像がforestフォルダとseaフォルダの両方に配置されています
  • ですが、デフォルトの状態ではフォルダ名は無視されるので、これらの画像にはbirdという名前でアクセスすることになり、区別ができません

フォルダ名付きでのアクセス(Namespaceあり)

with_namespace.png

  • アセットカタログ内でフォルダを選択し、Attributes InspectorからProvides Namespaceにチェックを入れると、そのフォルダ名がNamespaceとして利用されます
    • フォルダの色も黄色から水色に変わっていますね
  • この画像の例では、それぞれforest/birdsea/birdという名前で区別されます
  • こんな感じで、コード内でもInterface Builderでも参照できます

    imageView.image = index == 0 ? UIImage(named: "forest/bird") : UIImage(named: "sea/bird")
    

    image_ib.png

まとめ

参考

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

Flutter製iOSアプリのFABで、スペース区切りのlabelが改行される

事象

FABのlabelに渡したTextが、スペースで改行される。
(ちなみにスペースを2つ入れると改行されません。)

image.png

FloatingActionButton.extended(
  icon: Icon(Icons.gamepad),
  label: const Text("START GAME"),
  shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(16)),
);

暫定対応

TextにTextStyle(letterSpacing: 1)を設定することで、事象が改善されました。

image.png

FloatingActionButton.extended(
  icon: Icon(Icons.gamepad),
  label: const Text("START GAME", style: TextStyle(letterSpacing: 1)),
  shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(16)),
);

根本原因

内部実装など追えていないため、何故改行されるのか、何故letterSpacingを入れると解消されるのかはまだ判明していません。
分かり次第追記します。

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

FlutterのAdMobプラグインを入れたiOSアプリが起動時にクラッシュする

事象

アプリ起動時に画面が真っ暗になり、アプリがクラッシュする。

クラッシュログ

*** First throw call stack:
(
    0   CoreFoundation                      0x000000010c2956fb __exceptionPreprocess + 331
    1   libobjc.A.dylib                     0x000000010b839ac5 objc_exception_throw + 48
    2   CoreFoundation                      0x000000010c295269 -[NSException raise] + 9
    3   Runner                              0x000000010248bd91 GADVerifyApplicationID + 148
    4   Runner                              0x000000010248c9c0 GADVerifyApplicationID + 3267
    5   libdispatch.dylib           <…>
Lost connection to device.

原因

GADApplicationIdentifier(AdMobアプリID)Info.plistに追記していなかった。

AndroidでAdMobプラグインを導入する際には、AndroidManifest.xmlにAdMobアプリIDを記載する必要がありますが、iOSでも同様の処理が必要だったようです。
こちらの公式ドキュメントに記載がありました。
https://firebase.google.com/docs/admob/ios/quick-start?hl=en#update_your_infoplist

解決策

Info.plistに下記の<key>, <string>を追記します。

Info.plist
<dict>
  <key>GADApplicationIdentifier</key>
  <string>[MY-ADMOB-APP-ID]</string>
</dict>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift] 初心者向け 失敗しないScrollViewの設定のしかた

はじめに

初めてScrollviewの設定をする際に、少々手こずったので自分のためにまとめておきました。
自分なりに一番納得した手順を説明します。何か指摘や質問があれば気軽にコメントください。

手順

1.ScrollViewの追加
 元からあるViewにScrollViewを追加します。
ScreemShot.png

2.AutoLayoutの設定
 ScrollViewに対し、top,bootom,left,rightそれぞれを以下のように0に設定する。
ScreenShot2.png
以下のように対象がSafeAreaになっていることを確認します。
ScreenShot3.png

3.ScrollViewの上にViewを追加
1と同じようにScrollViewの上にViewを追加します。

4.追加したViewに対しAutoLayoutを設定
 まず、2と同じように追加したviewのtop,bottom,left,rightを0に設定します。
 次に、commandキーを押しながら追加したviewとScrollViewを選択し、以下のようにequal Widthに設定します。
 (注意)equal Heightには設定しないようにしてください。
スクリーンショット 2019-06-25 5.46.51.png

5.スクロールできるviewの高さを決める。
追加したviewの高さを好きな値に設定して完成。
(今回は1000にしました。)
スクリーンショット 2019-06-25 6.02.37.png

StoryBoardの設定

storyboardでUIの作成を行う時には以下の設定をして、StoryBoardに表示されるviewの大きさを変更します。
1. main.storyboardでviewControllerを選択
2. 右のウィンドウからsimulatedSizeをFreedomに設定する。
  以下のようにsimulatedSizeをFreedomに設定し、Heightの値をview全体が表示される値に設定します。
スクリーンショット 2019-06-25 6.13.03.png

3.以上

さいごに

ScrollViewの設定についてまとめました。
次は、僕がお気に入りのライブラリについて紹介していきます。
気軽にコメントや質問ください。

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

iOS12 以下をサポートするプロダクトで SwiftUI の恩恵を得る方法

はじめに

SwiftUI に注目が集まる一方、iOS13 未満をサポートバージョンから切り捨てるまで導入が困難なのも事実です。今回はこの問題点に着目し、 iOS12 以下も今までどおりサポートしつつ SwiftUI と Xcode11 を利用したプレビュー機能を実装する方法をご紹介します。

参考に

AkkeyLab/StoryboardPreviewsBySwiftUI
この記事は、サンプルからの抜粋となります。詳細はこちらをご覧ください。

Storyboard Preview by SwiftUI
WWDC 報告会にて同様の内容で発表も行っております。ぜひご覧ください。

環境

Xcode 11 β
macOS Catalina β

実現すること

  • iOS12 以下をサポートしつつ Xcode Previews を利用する
  • 既存の Storybord や xib ファイルをプレビューさせる

実現方法

  • Xcode Previews 専用のターゲットを作る
    • プレビューさせる画面に関連するソースコードとプレビュー用の swift ファイルが含まれており、 iOS13 以降をターゲットバージョンとする
  • 対象のクラスを UIViewRepresentable に準拠させる
    • updateUIView メソッド内で施した変更がプレビューに即時反映される
  • PreviewProvider に準拠させた struct を定義
    • static var previews: some View でプレビューに必要な処理を施す

※ UIViewController をプレビューさせる場合はそれぞれ UIViewControllerRepresentable, updateUIViewController となります。

import SwiftUI
import UIKit

#if DEBUG
// Create new for preview
struct UserDetailBasicInfoCellPreviews: PreviewProvider {
    static var previews: some View {
        Group {
            // Sets the format of the preview
            UserDetailBasicInfoCell()
                .previewLayout(.fixed(width: 320, height: 100))
                .previewDevice(PreviewDevice(rawValue: "iPhone SE"))
            UserDetailBasicInfoCell()
                .previewLayout(.fixed(width: 414, height: 100))
                .previewDevice(PreviewDevice(rawValue: "iPhone XS Max"))
        }
    }

    static var platform: PreviewPlatform? = .iOS
}

// UserDetailBasicInfoCell.swift
// UserDetailBasicInfoCell.xib
extension UserDetailBasicInfoCell: UIViewRepresentable {
    typealias UIViewType = UserDetailBasicInfoCell

    func makeUIView(context: Context) -> UserDetailBasicInfoCell {
        return Self.instantiate()
    }

    func updateUIView(_ uiView: UserDetailBasicInfoCell, context: Context) {
        // Make parameter change for preview
    }
}
#endif

利点

  • 表示・非表示が切り替わるパーツが多い場合のデバッグ強化
  • 複数端末(画面サイズ)でのレイアウト確認が一度に行える

サンプルの README を見ればもっとイメージしやすいかもしれません。

欠点

  • 前準備がある程度必要になる

最後に

今年の秋が楽しみですね!

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

iOSアプリからOutlookの予定表を取得する方法(Swift)

はじめに

Microsoft Graph APIを使って、iOSアプリからOutlookの予定表を取得する方法を紹介します。

「Microsoft Graph API」とは?

Microsoft 365のデータにアクセスするためのAPIです。

詳細は公式ドキュメントをご参照ください。
https://docs.microsoft.com/ja-jp/graph/overview

前提条件

手順

公式ドキュメントに沿って実装します。
https://docs.microsoft.com/ja-jp/azure/active-directory/develop/quickstart-v2-ios#option-2-register-and-manually-configure-your-application-and-code-sample

選択肢1(自動)はやり方がわからなかったので、今回は選択肢2(手動)で行います。

アプリケーションの登録

まず、Azureにアプリを登録します。

以下のページにアクセスし、[+新規登録]をクリックします。
https://aka.ms/MobileAppReg
スクリーンショット_2019-06-24_22_17_59.jpg

名前を入力し、[登録]をクリックします。
スクリーンショット_2019-06-24_22_24_23.jpg

[サポートされているアカウントの種類]では、「この組織のディレクトリ内のアカウントのみ」を選択するとシングルテナントアプリ、他の2つを選択するとマルチテナントアプリとなります。

これでアプリの登録は完了です。

Bundle IDの更新

次に、登録したアプリにBundle IDを追加します。
※公式ドキュメントの手順がわからなかったので、異なる方法で行っています。

[クイック スタート] > [iOS]をクリックします。
スクリーンショット_2019-06-24_22_57_34.jpg

[Step 1: Configure your application]にある[Make this change for me]をクリックします。
スクリーンショット 2019-06-24 23.01.49.png

[バンドル ID]にiOSアプリのBundle Identifierを入力し、[更新する]をクリックします。
スクリーンショット_2019-06-24_23_02_32.jpg

後述するサンプルコードのBundle IDは com.microsoft.identitysample.MSALiOS です。
スクリーンショット_2019-06-24_23_02_59.jpg

更新が完了したら緑のチェックが付きます。
スクリーンショット 2019-06-24 23.04.47.png

※一度Bundle IDを変更すると、[Make this change for me]が非表示になるため、再度Bundle IDを変更できなくなります。
変更する方法を知っている方がいらっしゃったら教えてください:bow:

こちらのページの kClientIDkAuthority は後で使うので、ページは開いたままにしてください。

実装

サンプルコードのダウンロード

以下からサンプルコードをダウンロードします。
https://github.com/Azure-Samples/active-directory-ios-swift-native-v2/archive/master.zip

クライアントIDと認証URLの更新

MSALiOS.xcworkspaceをXcodeで開きます。

クライアントIDと認証URLを、先ほど開いたページに記述されている値に変更します。

ViewController.swift
- let kClientID = "{デフォルトのクライアントID}"
+ let kClientID = "{自アプリのクライアントID}"
…
let kAuthority = "https://login.microsoftonline.com/common" // マルチテナントアプリ
let kAuthority = "https://login.microsoftonline.com/{自アプリのディレクトリ(テナント)ID}" // シングルテナントアプリ

kAuthority はマルチテナントアプリとシングルテナントアプリで指定する値が異なります。

マルチテナントやシングルテナントについては、公式ドキュメントをご参照ください。
https://docs.microsoft.com/ja-jp/azure/active-directory/develop/howto-convert-app-to-be-multi-tenant

権限の追加

必要に応じて権限を追加します。
私は予定表を取得したいので、 Calendars.Read を追加しました。

予定表の取得に必要な権限は、以下に記載されています。
https://docs.microsoft.com/ja-jp/graph/api/user-list-calendarview?view=graph-rest-1.0&tabs=cs

ViewController.swift
let kScopes: [String] = ["https://graph.microsoft.com/User.Read",
                         "https://graph.microsoft.com/Calendars.Read"]

URLスキームの変更

Info.plistに記述されているURLスキームを以下のように書き換えます。
サンプルアプリだとこのままでも動作しますが、書き換えておくのがいいです。

Info.plist
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
-           <string>msauth.com.microsoft.identitysample.MSALiOS</string>
+           <string>msauth.$(PRODUCT_BUNDLE_IDENTIFIER)</string>
        </array>
    </dict>
</array>

デバッグ

ここまでできたらデバッグして動作確認します。

[Call Microsoft Graph API]をタップします。
スクリーンショット_2019-06-24_23_37_50.jpg

[Continue]をタップします。
スクリーンショット_2019-06-24_23_39_26.jpg

Microsoftアカウントを入力し、[Next]をタップします。
スクリーンショット_2019-06-24_23_41_28.jpg

パスワードを入力し、[Sign in]をタップします。
スクリーンショット_2019-06-24_23_43_20.jpg

[Result from Graph:]にJSONデータが表示されたらログイン成功です!
スクリーンショット_2019-06-24_23_45_04.jpg

予定表の取得

自分の1週間の予定表を取得する処理を実装します。

GraphエクスプローラーにサインインしてAPIを試し、クエリやレスポンスボディを把握すると実装しやすいです。
https://developer.microsoft.com/ja-jp/graph/graph-explorer

サンプルコードの getContentWithToken() メソッドをコピぺし、メソッド名とURLのみ変更すればOKです。

ViewController.swift
    private func getCalendarWithToken() {

        let formatter = DateFormatter.fullISO8601
        let startDateTime = formatter.string(from: Date())
        let endDateTime = formatter.string(from: Date(timeIntervalSinceNow: 60*60*24*7))

        let urlString = kGraphURI + "calendarview?startdatetime=" + startDateTime + "&enddatetime=" + endDateTime

        // Specify the Graph API endpoint
        let url = URL(string: urlString)
        var request = URLRequest(url: url!)

        // Set the Authorization header for the request. We use Bearer tokens, so we specify Bearer + the token we got from the result
        request.setValue("Bearer \(self.accessToken)", forHTTPHeaderField: "Authorization")

        URLSession.shared.dataTask(with: request) { data, response, error in

            if let error = error {
                self.updateLogging(text: "Couldn't get graph result: \(error)")
                return
            }

            guard let result = try? JSONSerialization.jsonObject(with: data!, options: []) else {

                self.updateLogging(text: "Couldn't deserialize result JSON")
                return
            }

            self.updateLogging(text: "Result from Graph: \(result))")

            }.resume()
    }

}
DateFormatter+ISO8601.swift
import Foundation

extension DateFormatter {
    static let fullISO8601: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
        formatter.calendar = Calendar(identifier: .iso8601)
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        return formatter
    }()
}

getContentWithToken() メソッドを差し替え、デバッグします。(2箇所)

ViewController.swift
- self.getContentWithToken()
+ self.getCalendarWithToken()

予定表のデータがJSON形式で表示されたら成功です!
スクリーンショット_2019-06-25_0_32_06.jpg

おわりに

あとはこれらのコードを参考に製品アプリを実装すればOKです。

iOSアプリからMicrosoftのデータを取得するのはなかなか大変でした:sweat_smile:

参考リンク

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

ダークモード適用を回避する方法

はじめに

iOS13から遂にダークモードが導入されました。
iOS13 プレビュー記事

今後のiOSアプリ作成にはダークモードを適用が前提となっています。
しかし、アプリ独自のレイアウトを設定しているために、
すぐにはダークモードを適用できない場合があると思います。

そこで今回は、ダークモードを回避する方法を共有します。

UIViewControllerごとに設定

UIViewControllerに追加されたoverrideUserInterfaceStyleの値を指定します。
デフォルトはunspecified(指定なし)です。ユーザーの設定によって変化します。

説明
unspecified 指定なし(デフォルト)
light ライトモード(明るい外観)
dark ダークモード(暗い外観)

UIUserInterfaceStyle - UIKit | Apple Developer Documentation

サンプルコード

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // 常にライトモード(明るい外観)を指定することでダークモード適用を回避
        self.overrideUserInterfaceStyle = .light
    }

}

アプリ全体に適用する

アプリ全体に適用するにはInfo.plistUIUserInterfaceStyleパラメータをLightにします。
これで常にライトモードになります。
なおAutomatic(自動)Dark(ダークモード)を指定することもできます。
なお設定しない場合や不正な値が入力された場合は常にLightが採用されます。

まとめ

これらの方法でダークモードの適用を回避することができました。
しかし、Appleはダークモードの適用を推奨しているので、
いずれかのタイミングでアプリにダークモードを適用させる対策が必要です。

この対応はあくまで一時的な対応か外観を変化させたくない特段の事情がある場合に止めるべきでしょう。

参照

Choosing a Specific Interface Style for Your iOS App | Apple Developer Documentation
外観モードの変更方法について

Implementing Dark Mode on iOS - WWDC 2019 - Videos - Apple Developer
WWDC2019のダークモードに関するセッション。動画やサンプルコードがあります。

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

【Flutter】firebase_admobでdisposeしても広告が消えない

概要

広告が表示される前に画面遷移をすると、disposeしても広告が消えない。

再現例

mypage.dart
import 'package:firebase_admob/firebase_admob.dart';
import 'package:flutter/material.dart';

class MyPage extends StatefullWidget {
  @override
  _MyPageState createState() => new _MyPageState();
}

class _MyPageState extends State<MyPage> {
  final String appId = 'ca-app-pub-0000000000000000~0000000000';
  final String adUnitId = 'ca-app-pub-0000000000000000/0000000000';
  BannerAd _bannerAd;

  @override
  void initState() {
    super.initState();
    FirebaseAdMob.instance.initialize(appId: appId);
    _bannerAd = _createBannerAd()..load()..show();
  }

  @override
  void dispose() {
    _bannerAd?.dispose();
    _bannerAd = null;
    super.dispose();
  }

  BannerAd _createBannerAd() {
    return BannerAd(
      adUnitId: BannerAd.testAdUnitId,
      size: AdSize.smartBanner,
      listener: (MobileAdEvent event) {
        print("BannerAd event is $event");
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Text('MyPage'),
    );
  }
}

対処法

initStateではloadだけ行い、
BannerAdのload完了時にshowするようにする。

mypage.dart
  @override
  void initState() {
    super.initState();
    FirebaseAdMob.instance.initialize(appId: appId);
    _bannerAd = _createBannerAd()..load();
  }

  BannerAd _createBannerAd() {
    return BannerAd(
      adUnitId: BannerAd.testAdUnitId,
      size: AdSize.smartBanner,
      listener: (MobileAdEvent event) {
        print("BannerAd event is $event");
        if (event == MobileAdEvent.loaded) {
          if (mounted) {
            _bannerAd..show();
          } else {
            _bannerAd = null;
          }
        }
      },
    );
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む