20210111のSwiftに関する記事は14件です。

独学でAppStoreにアプリを公開するまで

はじめに

AppStoreにiOSアプリを公開するまでにやったこと・大変だったことを振り返りたいと思います。
これからiOSアプリ開発に挑戦したいと思っている方の参考になればと思います。

自己紹介

私は、実務未経験で独学でプログラミングを学習してます。
2020年の2月末から夏頃まではWebアプリ開発を学習していました。

作ったアプリの紹介記事↓
独学半年の実務未経験がRails+Nuxt.jsでSPA作ったので見て欲しい

秋頃は資格取ったりしてました。
10月末からiOSアプリ開発を学習し始めました。
12月末までに2つアプリをAppStoreに公開することができたので、アプリを紹介したいと思います。

アプリ紹介

学習から開発、申請までを時系列で振り返える前に、アプリをシンプルに紹介しておきます。

一つ目

一つ目は「タグメモ」です。
タグをつけて管理できるシンプルなメモ帳アプリです。

AppStore
リポジトリ
new_iphone12.001.jpeg

11月7日から制作を始めて同月の17日にリリースしました。

二つ目

二つ目は「StudyLevel」です。
毎日の勉強時間を記録するとレベルが上がっていく学習習慣支援アプリです。

AppStore
Untitled.png
iphone12_keynote_StudyLevel.001.jpeg

こちら、下記で紹介記事を書いているWebアプリのiOS版リメイクになります。
独学半年の実務未経験がRails+Nuxt.jsでSPA作ったので見て欲しい

11月17日から開発を始めて、12月29日にリリースしました。

時系列で振り返る

まずは学習

Twitterを振り返ってみると、10月20日から学習を始めたようです。

使用した主なリソース

まずはSwiftの基本を学習したいと思い、最初の方に購入した書籍です。
よくある説明不足の初心者向け書籍とは異なり、基礎的な部分から詳しく説明されていました。
後半部では、「どういう場面で役に立つのか」といった実践的な内容まで解説されており、学んだのどこで使えばいいのかわからないということがありませんでした。
「Webサービスとの連携」の章もあり、APIとの通信を頻繁に行う「StudyLevel」の開発の際にはとても役に立ちました。

紹介したアプリは二つともSwiftUIを使用して開発しました。
SwiftUIの基本的な考え方から、リファレンス的な内容まで詳しく説明されていました。
SwiftUIでUIKitの部品を使う方法まで載っており、SwiftUIでできないことをカバーする方法まで説明されています。
開発中にも何度も読み返しました。

どのくらいインプットに時間をかけるか

私は学習を始めてから3週間弱でアプリの開発を始めました。( 一日平均5時間ぐらい )
振り返ってみると、少し時間をかけすぎてしまったように感じます。
やはり実際にコードを書かないと頭に残りませんし、基礎的なことすらアウトプットできていないのに実践的な知識を取り入れようとしても意味がないなと感じました。
最初はさらっと学習して、開発していく中で疑問に思ったことを再度学習するのが効率的だと思います。

メモアプリを開発し始める

とりあえず簡単なアプリでいいのでAppStoreに公開してみたい!という意気込みで開発を始めました。
シンプルで機能が少ないものでもAppStoreに公開されており、これなら自分にも作れそうだ!という理由でメモアプリを作ることにしました。( しょうもない理由ですいません )

メモの保存にはRealmを使用しました。
開発を始めてから学習しましたが、高度なことしていないこともあり、特に困りませんでした。

開発は順調に進み、一週間ほどで完成させることができました。

申請する

AppStoreへの申請は、手順が多く大変です。(特に一番始めは)
「 AppStore 申請 」で検索すると、手順を説明している記事がたくさんヒットするので、参考にしながら準備を進めました。

画像の加工をあまりしたことがなかったので、アイコンやスクリーンショット(アプリの紹介用の画像)を作成するのに少し苦労しました。
スクリーンショットはKeynoteで作成しています。

参考になった記事↓

iOSアプリを登録、申請して公開するまで
App Store提出用のスクリーンショットを無料で自作する

結果を待つ

申請に関する記事を読んでいると、機能不足でリジェクトされている方が多く、恐怖していました。
が、次の日の昼ごろには審査通過の通知が届き、拍子抜けしました。

シンプルなアプリですが、AppStoreに公開されているのをみたときは感動しました。

学習管理アプリを開発し始める

Webアプリを作成している頃から、ネイティブアプリ版を作成してみたいと思っていたので、作成に取り掛かることにしました。
APIはWebアプリと同じものを流用しています。iOS版用に機能を追加したりはしました。
APIと通信して、画面に表示させるだけですむ部分も多かったので想定よりもスムーズに進みました。

大変だったこと

  • メモリ管理

それぞれの投稿にアイコン画像を表示しているため、画面に表示される画像の量が多くなってしまい、メモリ不足が発生していました。
画像を圧縮してサイズを小さくすることによって、解決しました。
加えて、通信量を減らすために画像データをRealmを使ってキャッシュするようにしました。

  • SwiftUIでは実装できない

まだ新しいフレームワークであるため、サポートされていない機能があります。
ただ、UIKitの部品をSwiftUIで使用することができるので、全く実装できないということはありませんでした。

二回目の申請

二回目なので、スムーズに準備が進みました。
メモアプリに比べて力を入れているので、アイコンやスクリーンショットの完成度を高めることにしました。
前回の時は、アイコンの手を抜いていましたが、今回は少し力を入れました。

参考にした記事↓

イラレやフォトショ要らず!アプリアイコンをCacooで作ろう

リジェクトを経験する

リジェクトされました。ただ、想定済みでした。
というのも、ユーザー投稿機能があるアプリには「ブロック・通報機能」をつける必要があるのです。
先に実装方法を考えておいたので、すぐに実装・テストして、即日再申請しました。

申請が通る

まだ不十分と言われたらどうしようかと思っていましたが、杞憂でした。
無事にAppStoreに公開され、アプリが稼働しているのを確認できました。

まとめ・反省

  • アイデアについて

メモアプリは飽和していますし、学習管理アプリもWebアプリ版の焼き直しみたいなものなので、オリジナリティに欠けると感じています。
もっと実際的なアプリを開発したいと考えているので、企画を練る段階を大事にしたいと思いました。

  • 独学について

独学の限界を感じることが多くなってきました。問題解決を全て自分でしないといけないのは、メンタル的になかなかしんどいです。調べて解決方法が出てくるならいいですが、全く原因がわからないエラーに遭遇すると絶望します。
まあ、なんとかアプリは完成できているのでどうにかして解決しているのですが、勉強効率は悪いと思います。

  • 開発のルールについて

Webアプリ版の記事でも書いているのですが、一人で開発しているとルールの徹底が難しいと感じました。コミットやブランチを切る時の粒度、開発順序、コーディングルールなどは自分の裁量と気分で簡単にねじ曲げられるので、徐々に雑になりがちです。
初期段階でルールを考えておくべきだと感じました。

これからについて

いいアイデアが浮かんだら、またアプリ作りたいと思います。
とりあえず、今月は基本情報の本番なので気を引き締めて頑張ります。
4月の応用情報も受けれたらと思っているので、そちらも勉強を進めていきます。
また、UIKitとStoryBordを使用したアプリ開発も学習したいと考えています。
良さそうな学習リソースが見つからないので、ご存知の方コメントしていだけると幸いです。

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

Apple Silicon MacでiOSアプリ開発は可能なのか?

こんにちは。都内でiOSエンジニアをやっております、 @zrn-ns です。

Apple Silicon搭載MacBook Airを使用し始めて、半月ほどが経過しました。
使うたび、その挙動の機敏さや排熱の少なさ、バッテリー持ちに感動します。

しかし、開発用途で使い始めると、これまでのIntel製チップでは発生しなかった多くの問題にぶち当たり、その対処に多くの時間を割く羽目になりました。
(既存のプロジェクトをビルドできる状態にするまでに1週間ほどを費やしました)

これからApple Silicon搭載Macを買うiOSエンジニアの方向けに、レポートを残そうと思います。

?‍♂️TL;DR

M1 Macはまだ手放しではおすすめできない。
iOSアプリ開発をこれから始める人やあまり熟練度の高くない人はIntel Macを購入したほうが無難。

オフィスワーク用途としてのコスパは申し分ない

オフィスワークやネットサーフィン等の用途であれば、このMacは間違いなく『買い』です。
普段のネットサーフィンや文字書き、オフィスワーク程度の用途であれば、間違いなく今まで出会ったノートPCの中でも最高のノートPCだと思います。

Appleが初めてリリースしたApple Silicon搭載Macであるにも関わらず、ハードウェアの不具合は非常に少なく(全く無いわけではないですが)、驚くほど快適に動作します。

ハードウェアの制約

多くはないのですが、ディスプレイ周りの不具合に遭遇したので、記載しておきます。

スリープ解除時に外付けディスプレイの挙動が安定しない

僕はMacBook AirをCalDigit TS3 Plus(Thunderbolt 3接続のドッキングステーション)に接続して利用しています。
このDockはBelkinなどの他社のDockと比べてもかなり安定して動作するのでかなり気に入って使っているのですが、このDockを経由して外付けディスプレイを接続した場合、スリープ復帰時にディスプレイの接続ができないことがあります。(体感で3日に1回ほどの頻度で発生します)

Dockの問題である可能性もありますが、手元にあるIntel Macでは発生しないため、モニター側の問題でないのは確かです。

DDC/CI系のユーティリティがうまく動作しない

僕は普段Lunarというユーティリティを使用しています。
これはDDC/CIというプロトコルを使用することで、Mac側から外部ディスプレイのコントラストや明るさを調整できるもので、これを使用することでMac側の画面の明るさと外部ディスプレイの明るさを同期させることができます。

しかしApple Silicon搭載Macでは現状DDC/CIに対応していないらしく、これらのユーティリティは動作しないようです。
M1チップの制限なのかはわかりませんが、今後解消することを祈ります。

Apple Silicon搭載MacでiOS開発はできるのか?

少なくとも僕の環境では、M1 Macでアプリ開発ができる状態が整いました。

しかしすべてネイティブで動作しているわけではなく、それなりに痛みを伴っています。

基本的にターミナルはRosettaで起動して使っている

ターミナルをRosettaを使用して開くことで、多くのツールはM1 Macで動作します。
BundlerやFastlane, CocoaPodsについてはRosettaを噛ませることで問題なく動作しました。
(一応オーバーヘッドが発生するはずですが、ほぼ体感できないレベルです)

Homebrewは基本的にRosettaを使用したほうがよさそう

実はHomebrewはM1対応のものがすでにbetaですがリリースされており、利用できる状態になっています。
しかしHomebrewがM1に対応していたとしても、Homebrew経由でインストールするツールにはM1に対応していないものがまだ多く、M1対応のHomebrewをメインで使うのはまだ厳しい印象です。

基本的には、これからしばらくの間は、HomebrewはRosettaを使用して使用することをおすすめします。

Firebase関連はSwiftPackageManagerに移した

FirebaseはこれまでCocoaPodsで管理していたのですが、M1対応は専用のブランチでbeta対応中らしく、Apple SiliconとIntel両方でビルドできるようにするために、FirebaseをSwift Package Managerに移行しました。

SwiftPM版はすでにApple Siliconへの対応が行われていますが、まだいくつか問題があります。

NimbleのアサーションマッチャーはApple Siliconでは動作しない

マッチャーライブラリのQuick/Nimbleの機能であるAssertion Matcherは、x64-86アーキテクチャでしか動作しないため、arm64アーキテクチャを採用するApple Siliconでは動作しないようです。
今後解消される可能性はありますが、fatalError()の検証ができないのは少し不便ですね。

Carthageについては多分うまく動かない

今回のプロジェクトではCarthageを使用していないため検証できていないのですが、CarthageではまだApple Siliconへの対応が不十分なようなので、うまく動作しないと思われます。

具体的には、Xcode12でCarthageを使用するには公式が提示しているワークアラウンドスクリプトを適用する必要があるのですが、これを使用するとApple Silicon Mac向けのバイナリが削除されるため、うまく動作しなくなるはずです。

その他は特に問題なさそう

Mint, CocoaPods, Swift Package Managerを使用しているプロジェクトでは、概ね正常にビルドができる環境が整いました。(おそらく同様の構成であれば、同じように動作させることは可能かと思います)

参考までに、僕の環境で正常に動作したツールの一覧を記載しておきます。

Rosetta2で動作

  • Homebrew
    • vim
    • ggrep
    • Mint
      • SwiftLint
      • LicensePlist
      • periphery
  • Bundler
    • Fastlane
    • CocoaPods
      • Quick/Nimble

ネイティブで動作

  • Xcode12, SwiftPM
    • Alamofire
    • SDWebImage
    • SwiftyJSON
    • RxSwift
    • Firebase
    • RSKImageCropper

総括

iOSアプリ開発において、現行のすべてのMacの中でもかなり上位のパフォーマンスを誇るApple Silicon搭載Macですが、しばらく使ってみて、やはりまだ手放しにおすすめはできないと感じました。

iOSアプリ開発をするとき、Xcodeから吐き出される意味不明なエラーに苦しめられることがよくあります。多くの場合、先人が同様の問題にぶち当たり、何らかのワークアラウンドが提供されていることが多いのですが、Apple Silicon搭載Macで表示されるエラーはその限りではありません。

問題の解決そのものを楽しめる人(もしくはその問題を誰かが解消するまで待てる人)でなければ、メインの開発機としてApple Silicon Macを購入するのはやめたほうが無難かもしれません。
(特にアプリ開発初心者の方にはハードルが高い可能性がありますので、もう少し様子を見ることをおすすめします。)

ただし、よくわからないエラーに多く見舞われる分、問題の調査や原因の切り分け能力はつくかと思います。
たびたび発生する意味不明なエラーを、自力で解決する気概がある方にはおすすめしますので、是非購入を検討してみてはいかがでしょうか。

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

【Swift】LINEログイン機能の実装方法

はじめに

個人アプリ作成時にLINEログイン機能を実装したので、
備忘録がてらコマンドの共有をさせていただきます。

GitHubにコードを載せていますのでぜひご覧ください。
-> https://github.com/onishi-app/LINELogin

環境
・Apple Swift version 5.3
・XCode version 12.3
・LINE Developers 5.2

完成形

ezgif.com-gif-maker.gif

実装方法

実装方法について1から説明していきます。

下記サイトを参考に機能を実装してので皆さんも参考にしていただければと思います。
-> 参考サイト

基本的にこちらを参照すれば作成出来ると思いますが
私が作成した時の流れも下に記載しておきます。
(Developerにチャネルを追加する流れやエラーの対処など)

LINE Developers

まずはじめに、LINE Developerにアクセスします。
右上のログインから自分のLINEアカウントにログインします。
スクリーンショット 2021-01-11 8.53.55.png

下にスクロールし、新規プロバイダーを作成します。
スクリーンショット 2021-01-11 8.53.58.png

適当な名前でいいのでプロバイダーを作成します。
スクリーンショット 2021-01-11 8.54.08.png

するとプロバイダーが作成されるので、
作成されたプロバイダーからLINEログインチェネルを作成します。
スクリーンショット 2021-01-11 8.54.21.png

必要な項目を入力しチャネルを作成します。
作成するとチャネルIDが定義されます。
スクリーンショット 2021-01-11 8.56.41.png

チャネルを作成後にアプリのbundle IDを入力する必要があるので、
LINEログイン機能を実装するアプリを作成します。

bundle IDは下画像のBundle Identifierがbundle IDが記載されています。
スクリーンショット 2021-01-11 9.05.30.png

チャネルのタブをLINEログイン切り替えると、iOSバンドルIDを記入する箇所があるので、
そこに先ほど作成したアプリのbundle IDを入力します。
スクリーンショット 2021-01-11 9.06.20.png

LINE Developerはいったんここまでです。

次にターミナルを開きます。
※ CocoaPods と Homebrew をインストールしておく必要があるので
そちらのインストールを事前にお願いします。

CocoaPods

ディレクトリの移動を行います。

cd /**/**/**/アプリフォルダ

ターミナルの操作方法が分からない方は、
cdの後にFinderからアプリフォルダをドラッグ&ドロップすると自動で入力されます。

Podfileの作成を行います。

$ pod init 

Podfileに下記内容を追記します。
(Finderからの操作でもviコマンドからの操作でも構いません。)

pod 'LineSDKSwift', '~> 5.0'

podのインストールを行います。

pod install

Carthage

Cartfileを作成し、編集します。
(Finderからの操作でもviコマンドからの操作でも構いません。
今回はviからの操作を記述しておきます。)

$ vi Cartfile

「 i 」を押すと編集モードになりますので
下記コマンドを入力後「 esc 」 -> 「 :wq 」を押します。
vi コマンド 検索すると操作方法が出てくると思います。)

github "line/line-sdk-ios-swift" ~> 5.0

画面が戻ったらアップデートを行います。

$ carthage update line-sdk-ios-swift

この時、下記のエラーが出力される方は、コチラの記事を参考にしてください。

A shell task (/usr/bin/xcrun xcodebuild -workspace /Users/ryosuke/Desktop/project/LINELogin/Carthage/Checkouts/line-sdk-ios-swift/LineSDK.xcworkspace CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= CARTHAGE=YES -list) failed with exit code 72:
xcrun: error: unable to find utility "xcodebuild", not a developer tool or in PATH

エラー対処後再度アップデートをお願いします。

私の場合は少し進みましたが、さらにエラーが発生しました。
下記エラーが出力された方はコチラの記事を参考にしてください!
記事の「How to make it work」から下の部分を行えば上手くいきます。

Build Failed
    Task failed with exit code 1:
    /usr/bin/xcrun lipo -create /Users/ryosuke/Library/Caches/org.carthage.CarthageKit/DerivedData/12.3_12C33/line-sdk-ios-swift/5.7.0/Build/Intermediates.noindex/ArchiveIntermediates/LineSDK/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/LineSDK.framework/LineSDK /Users/ryosuke/Library/Caches/org.carthage.CarthageKit/DerivedData/12.3_12C33/line-sdk-ios-swift/5.7.0/Build/Products/Release-iphonesimulator/LineSDK.framework/LineSDK -output /Users/ryosuke/Desktop/project/LINELogin/Carthage/Build/iOS/LineSDK.framework/LineSDK

This usually indicates that project itself failed to compile. Please check the xcodebuild log for more details: /var/folders/4s/408j5zx12f1b7my_mjvm6xw80000gn/T/carthage-xcodebuild.FJHOKm.log

上手くいくと下記のようなログが出力されます。

MacBook-Pro:LINELogin ユーザ名$ carthage.sh bootstrap --platform iOS --cache-builds
*** Checking out line-sdk-ios-swift at "5.7.0"
*** No cache found for line-sdk-ios-swift, building with all downstream dependencies
*** xcodebuild output can be found in /var/folders/4s/408j5zx12f1b7my_mjvm6xw80000gn/T/carthage-xcodebuild.YksYHO.log
*** Downloading line-sdk-ios-swift.framework binary at "5.7.0"
*** Building scheme "LineSDKObjCBinary" in LineSDK.xcworkspace
*** Building scheme "LineSDKObjC" in LineSDK.xcworkspace
*** Building scheme "LineSDK" in LineSDK.xcworkspace
MacBook-Pro:LINELogin ユーザ名$ 

ディレクトリ内を確認するといくつかのファイルが作成されていると思います。
CartfileやPodfileが作成されていると思います。

LINELoginは、今回私が作成したアプリの名前になります。

$ ls -al
total 48
drwxr-xr-x  14 *******  staff   448  1 11 09:45 .
drwxr-xr-x@  8 *******  staff   256  1 11 09:04 ..
-rw-r--r--@  1 *******  staff  6148  1 11 09:32 .DS_Store
drwxr-xr-x  12 *******  staff   384  1 11 09:11 .git
-rw-r--r--   1 *******  staff     0  1 11 09:19 5.0
-rw-r--r--   1 *******  staff    40  1 11 09:44 Cartfile
-rw-r--r--@  1 *******  staff    41  1 11 09:45 Cartfile.resolved
drwxr-xr-x   3 *******  staff    96  1 11 09:45 Carthage
drwxr-xr-x   8 *******  staff   256  1 11 09:04 LINELogin
drwxr-xr-x@  5 *******  staff   160  1 11 09:10 LINELogin.xcodeproj
drwxr-xr-x@  5 *******  staff   160  1 11 09:10 LINELogin.xcworkspace
-rw-r--r--@  1 *******  staff   265  1 11 09:09 Podfile
-rw-r--r--@  1 *******  staff   330  1 11 09:10 Podfile.lock
drwxr-xr-x   8 *******  staff   256  1 11 09:10 Pods

ターミナルを使うのはここまでになります!お疲れ様です。

LineSDK.frameworkファイルのリンク

ここまで上手くいくと、下記の階層にLineSDK.frameworkが作成されます。
プロジェクト > Carthage > Build > iOS > LineSDK.framework

それを、XCode > プロジェクト名 > General > Frameworks ,Libraries, and ・・・に
ドラッグ&ドロップします。
スクリーンショット 2021-01-11 11.33.59.png

先ほどGeneralを選択していましたが、
Build Phasesを選択し、からNew Run Script Phaseを選択します。
スクリーンショット 2021-01-11 11.39.10.png

作成されたされた Run Script に下記の3つを追加します。
/usr/local/bin/carthage copy-frameworks
$(SRCROOT)/Carthage/Build/iOS/LineSDK.framework
$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/LineSDK.framework

追加する場所は下画像を参考にしてください。
スクリーンショット 2021-01-11 11.49.08.png

Info.plist

アプリにInfo.plistファイルが作られているはずなので、
controlキーを押しながらクリック > Open As > Source Code の順に選択します。
スクリーンショット 2021-01-11 11.59.20.png

以下のスニペットを最後のタグの直前に挿入します。

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <!-- LINEからアプリに戻る際に利用するURLスキーマを追加 -->
            <string>line3rdp.$(PRODUCT_BUNDLE_IDENTIFIER)</string>
        </array>
    </dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
    <!-- アプリからLINEを起動する際に利用するURLスキーマを追加 -->
    <string>lineauth2</string>
</array>

追加するとこんな感じです。
スクリーンショット 2021-01-11 12.03.11.png

iOSアプリにLINEログインを組み込む

インストールなどは行えたので、次に組み込み作業を行います。
参考にした記事はコチラです。

コードを書くだけなので参考サイトと同じ内容しか書くことができないため省略します。

さいごに

なかなか大変な工程でしたがなんとかログインできました。

私が遭遇したエラーについてはある程度記述しましたが、
もしかしたらこの記事以外のエラーが発生するかもしれません。

エラーが発生したので諦めるのではなく、
エラーの文章で検索をかけて見てください!意外と出てきます!(笑)

以上、最後までご覧いただきありがとうございました。

参考

インストール方法
インストール時エラー:ユーティリティ不明
インストール時エラー:アーキテクチャ重複回避策
組み込み関連
コンパイル時エラー(Multiple commands produce)
コンパイル時エラー回避策実施後に出たエラーに対する対処
-> ファイル「XXX.entitlements」を開ませんでした。

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

iOS14以降のPHAuthorizationStatus

はじめに

iOSでは、カメラロールにアクセスするためには、ユーザに対して適切に権限をリクエストする必要があります。
iOS14になり、PHAccessLevel や PHAuthorizationStatus.limited が追加され、挙動が複雑になったので、調べたことの備忘録です。
「写真のアクセスを許可」の状態に応じた PHAuthorizationStatus と、できることのまとめです。

iOS13以下の場合

確認: iOS13.4.1

「写真のアクセスを許可」 authorizationStatus() requestAuthorization()
未確認 .notDetermined ○ (表示される)
読み出し/書き込み .authorized × (表示されない)
許可しない .denied ×

iOS14以降の場合

確認: iOS14.2.1

未確認

「写真のアクセスを許可」 authorizationStatus() authorizationStatus
(for: .addOnly)
authorizationStatus
(for: .readWrite)
requestAuthorization
(for: .addOnly)
requestAuthorization
(for: .readWrite)
未確認 .notDetermined .notDetermined .notDetermined

requestAuthorization(for: .addOnly) のみ確認

  • 設定画面の状態

  • authorizationStatusの結果
「写真のアクセスを許可」 authorizationStatus() authorizationStatus
(for: .addOnly)
authorizationStatus
(for: .readWrite)
requestAuthorization
(for: .addOnly)
requestAuthorization
(for: .readWrite)
写真の追加のみ
(requestAuthorization(for: .addOnly)で「OK」)
.notDetermined .authorized .notDetermined ×
なし
(requestAuthorization(for: .addOnly)で「許可しない」)
.notDetermined .denied .notDetermined ×

requestAuthorization(for: .readWrite) のみ確認

  • 設定画面の状態

  • authorizationStatusの結果
「写真のアクセスを許可」 authorizationStatus() authorizationStatus
(for: .addOnly)
authorizationStatus
(for: .readWrite)
requestAuthorization
(for: .addOnly)
requestAuthorization
(for: .readWrite)
選択した写真
(requestAuthorization(for: .readWrite)で「選択した写真」)
.authorized .limited .limited × ×
すべての写真
(requestAuthorization(for: .readWrite)で「全ての写真へのアクセスを許可」)
.authorized .authorized .authorized × ×
なし
(requestAuthorization(for: .readWrite)で「許可しない」)
.denied .notDetermined .denied ×

requestAuthorization(for: .addOnly) と requestAuthorization(for: .readWrite) の両方を確認

  • 再現手順
    authorizationStatus(for: .addOnly) と authorizationStatus(for: .readWrite) のどちらか片方だけが .notDetermined の時に、.notDetermined の方のAccessLevel で requestAuthorization する

  • 設定画面の状態

  • authorizationStatusの結果
「写真のアクセスを許可」 authorizationStatus() authorizationStatus
(for: .addOnly)
authorizationStatus
(for: .readWrite)
写真の追加のみ .denied .authorized .denied
選択した写真 .authorized . limited . limited
すべての写真 .authorized .authorized .authorized
なし .denied .denied .denied

iOS14以降にできるだけアルバムに写真を保存する

やりたいこと

  1. アルバムを指定して保存できる時は、アルバムを指定して保存する
  2. アルバムを指定して保存できない時は、カメラロールに保存する
  3. カメラロールにも画像を保存できない時は、何もしない

アルバムを指定して保存できる時は、アルバムを指定して保存する

readWrite の AuthorizationStatus が .authorized の時に保存できる

PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized

アルバムを指定して保存できない時は、カメラロールに保存する

addOnly の AuthorizationStatus が .authorized もしくは .limited の時に保存できる

PHPhotoLibrary.authorizationStatus(for: . addOnly) == .authorized or . limited

判定器

class AuthorizationStatusChecker {
    enum PhotoLibraryAuthorizationStatus {
        case readWrite
        case addOnly
        case denied
    }
    static func checkPhotoLibrary(handler: @escaping (PhotoLibraryAuthorizationStatus) -> Void) {
        let readWrite = PHPhotoLibrary.authorizationStatus(for: .readWrite)
        let addOnly = PHPhotoLibrary.authorizationStatus(for: .addOnly)
        switch (readWrite, addOnly) {
        case (.authorized, _):
            handler(.readWrite)
        case (.notDetermined, _):
            PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_: PHAuthorizationStatus) in
                Self.checkPhotoLibrary(handler: handler)
            }
        case (_, .authorized), (_, .limited):
            handler(.addOnly)
        case (_, .notDetermined):
            PHPhotoLibrary.requestAuthorization(for: .addOnly) { (_: PHAuthorizationStatus) in
                Self.checkPhotoLibrary(handler: handler)
            }
        default:
            handler(.denied)
        }
    }
}

使い方

AuthorizationStatusChecker.checkPhotoLibrary { [weak self] (status) in
    switch status {
    case .readWrite:
        self?.saveToAlbum(name: "MyAlbum")
    case .addOnly:
        self?.saveToCameraRoll()
    case .denied:
        break
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TableViewなどの上にスクロールで動かないボタンを配置する方法

はじめに

 「NavigationBarItemなどで上に配置してもいいけど、デザイン的に何か違う...」、「機能が二つだからタブで分けるほどじゃない...」こんな時にはビュー上にスクロールで動かないボタンを配置したくなります。ここではコードベースで動かないボタンを実装していきます。なお、コードベースでレイアウトを組む時に便利なSnapKitを利用しています。(snpがついているところ)本来CGRect()やCGSize()で指定するところなので、必要に応じて読み替えて下さい。なお、storyboardを使う場合はこちらが参考になると思います。
 ちなみにFlutterやAndroidではFloatingButtonというらしいです。Flutterで新規アプリを作成するとはじめにできてるカウントアプリでも使われてたはず...

Simulator Screen Shot - iPhone 12 - 2021-01-11 at 14.10.05.png

Viewの準備

クラスの作成

FloatingButtonViewというUIViewを継承したクラスを作成し、初期化の準備とUIButton, UITableViewの定義をします。

import UIKit
import SnapKit

class FloatingButtonView: UIView {

    let tableView =  UITableView()
    let floatingButton = UIButton()

    override init(frame: CGRect) {
        super.init(frame: frame)

    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

tableViewの準備

setupTableViewという関数のなかで大きさや配置の定義を行います。

private func setupTableView() {
        addSubview(tableView)
        tableView.snp.makeConstraints {
            $0.center.equalToSuperview()
            $0.size.equalToSuperview()
        }
    }

floatingButtonの準備

setupFloatingButtonという関数のなかで大きさや配置の定義を行います。
せっかくなのでlayer.cornerRadiusを使い丸くしています。一辺の長さの半分にすることで綺麗な円になります。

private func setupFloatingButton() {
        floatingButton.backgroundColor = .blue
        addSubview(floatingButton)
        floatingButton.snp.makeConstraints {
            $0.width.equalTo(50)
            $0.height.equalTo(50)
            $0.right.equalToSuperview().offset(-30)
            $0.bottom.equalToSuperview().offset(-30)
        }
        floatingButton.layer.cornerRadius = 25
    }

全体のソースコード

import UIKit
import SnapKit

class FloatingButtonView: UIView {

    let tableView =  UITableView()
    let floatingButton = UIButton()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupTableView()
        setupFloatingButton()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupTableView() {
        addSubview(tableView)
        tableView.snp.makeConstraints {
            $0.center.equalToSuperview()
            $0.size.equalToSuperview()
        }
    }
    private func setupFloatingButton() {
        floatingButton.backgroundColor = .blue
        addSubview(floatingButton)
        floatingButton.snp.makeConstraints {
            $0.width.equalTo(50)
            $0.height.equalTo(50)
            $0.right.equalToSuperview().offset(-30)
            $0.bottom.equalToSuperview().offset(-30)
        }
        floatingButton.layer.cornerRadius = 25
    }
}

ViewControllerの準備

準備といっても、Viewdで作ったものを貼り付けるだけですが。

クラスの作成

import UIKit

class FloatingButtonViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

loadViewの準備

loadViewのなかで自身の持つviewを先ほど作ったFloatingViewにします。

override func loadView() {
        view = FloatingButtonView()
}

全体のソースコード

import UIKit

class FloatingButtonViewController: UIViewController {

    override func loadView() {
        view = FloatingButtonView()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

完成!

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

[Swift]Decodable / Generics を利用して API を叩く

この記事で書くこと

・Swift で API を叩く方法

この記事を書いた理由

・忘れた時に確認するため

コード

Sample.swift
import Foundation

// T: Decodable プロトコルを準拠させた構造体
internal struct Sample {
    static func fetchAPI<T:Decodable>(url url:String,completion: @escaping (T) -> Void){

        guard let urlComponents = URLComponents(string: url) else { return }

        // クエリを使用しデータを絞り込む場合は queryItems プロパティに設定
        //urlComponents.queryItems = [URLQueryItem(name: "name", value: "value"),]

        let task = URLSession.shared.dataTask(with: urlComponents.url!){ data,response,error in
            guard let jsonData = data else { return }

            do {
                let decodedData = try JSONDecoder().decode(T.self,from: jsonData)
                completion(decodedData)
            } catch {
                print(error.localizedDescription)
            }  
        }
        task.resume()
    }
}
HowToUse.swift
fetchAPI(url:"https://sample.com",completion:{(data) in /* ここで data に対して処理を実行 */ })
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iPhoneアプリにログイン機能をつけてもappleログインがないと審査が通らない

firebaseを使って、TwitterログインとGoogleログインはつけたけれど、
appleログインができないと、リジェクトされる。
知らなかった?
審査メッセージ↓
スクリーンショット 2021-01-11 12.11.57.png

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

【Swift5】メモ:既存のプロジェクトにCoreDataを追加する

はじめに

アプリ作成時に「Use Core Data」にチェックをつければ自動でこれらが追加されますが、後から使いたくなった時は少し面倒だったので、コピペで使えるようにまとめておきます。

AppDelegate.swift

import CoreData

iCloudにデータを保管できるようにする場合はNSPersistentCloudKitContainer、そうでない場合はNSPersistentContainerを使います。

// MARK: - Core Data stack

lazy var persistentContainer: NSPersistentCloudKitContainer = {
    /*
    The persistent container for the application. This implementation
    creates and returns a container, having loaded the store for the
    application to it. This property is optional since there are legitimate
    error conditions that could cause the creation of the store to fail.
    */
    let container = NSPersistentCloudKitContainer(name: "coredataSample")
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

            /*
            Typical reasons for an error here include:
            * The parent directory does not exist, cannot be created, or disallows writing.
            * The persistent store is not accessible, due to permissions or data protection when the device is locked.
            * The device is out of space.
            * The store could not be migrated to the current model version.
            Check the error message to determine what the actual problem was.
            */
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()

// MARK: - Core Data Saving support

func saveContext () {
    let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
      }
}

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

[Xcode]UIScrollViewを最低限動かすための手順・やり方

iOS開発の学習をしています。

最近学んだ中で、特にUIScrollViewの扱いについてはかなり苦労しました。
そのUIScrollViewについて最低限動かすということに焦点を絞って備忘録的に書いています。

「なんだか良く分からないけど、誰かの記事見てそのまま書いて、動いたからオッケー!」
とはならないように、初学者なりに理屈を考えながら実装を試みました!
足りない部分も多々ありますが、私と同じような初学者の方々の参考になれば幸いです。

概要

新規プロジェクトを作成後、storyboradを使用しUIScrollViewをスクロールするところまでを実践する。

環境

  • macOS Catalina 10.15.7
  • Xcode - 11.3.1

完成品

Image from Gyazo

内容

  • UIScrollView
  • UIView
  • UILabel * 2

実践

①ViewにScrollViewを設置

Main.storyboard→View Controller→Viewを選択し、
ViewにScrollViewを設置する。

scroll-1.png

後ほど制約をつけるのでどこでも良いです。ちょっと大きめにしておいた方が見やすいかも。

②ScrollViewに制約をつける

先ほど設置したScrollViewにAutoLayoutで制約を加えていきます。
Add New Constraintsから設定していきます。
ScrollViewのサイズを決める制約となります。
親Viewに対しての余白を決めることで、サイズを決めています。

数字はなんでも良いですが今回は上下に50,左右に0とします。

scroll-2.png

Add 4 Constraintsを選択すると、ScrollViewに以下の4つの制約が追加されます。

- Safe Area.trailing = Scroll View.trailing
- Safe Area.bottom = Scroll View.bottom + 50
- Scroll View.leading = Safe Area.leading
- Scroll View.top = Safe Area.top + 50

また、ScrollViewに対し赤いラインが表示され"Scrollable Content Size Ambiguity"という制約エラーが出ます。

scroll-3.png

scroll-4.png

これは直訳の通り"スクロール可能なコンテンツのサイズが曖昧"という警告です。

補足:Scrollable Content Size Ambiguityとは?

では、このエラーは一体なんなのか?ということを考えていきます。

エラー文によると、

The scrollable size of a UIScrollView is computed automatically based upon the constraints of its subviews.

訳:UIScrollViewのスクロール可能なサイズは、そのサブビューの制約に基づいて自動的に計算されます。
スクロール可能なコンテンツの幅と高さを完全に定義するには、制約が必要です。

という内容が記載されています。
どうやらエラー文で指摘を受けたスクロール可能なコンテンツのサイズを定義するためにサブビューを制約する必要があるようです。

補足2:サブビューとは?

Xcodeのレイアウトはオブジェクトを上に重ねていくような構造になっており、ユーザから見て手前にあるビューをサブビュー(subview)、奥にあるビューをスーパービュー(superview)というようです。

scroll-debug.png

こんな感じ。(画像は完成品のレイアウト)

エラーは一旦スルーで

上記の理由から、現状ScrollViewはサブビューを持っていないのでエラーを解消することは出来ません。
ScrollViewの中にContentsViewなどを設置してからの対応となります。

③ScrollViewの中にViewを設置

scroll-5.png

ただのViewです。
分かりやすくするために、ScrollViewを水色、新たに設置したViewを灰色にしています。

このViewですが、名前は慣例的に"ContentsView"などとするのが良いらしいです。
本記事でも、以後ContentsViewと記載します。

④ContentsViewとContentLayoutGuideに制約をつける

こちらの記事がとても分かりやすく、大変参考になりました。
【Swift】Align制約の使い方。複数部品を整列する制約を追加する。(Swift 2.1、XCode 7.2)

scroll-6.png

コマンドキー押して2つ同時選択し、AutolayoutのAlignを選択。
Add New Alignment Constraintsから上下左右を固定します。

上4つを0にしてチェックを入れます。

  • Leading Edges
    部品の左端から他の部品の左端までの水平方向の距離を指定
  • Trailing Edges
    部品の右端から他の部品の右端までの水平方向の距離を指定
  • Top Edges
    部品の上端から他の部品の上端までの垂直方向の距離を指定
  • Bottom Edges
    部品の下端から他の部品の下端までの垂直方向の距離を指定

以下の制約が追加されます。

- ContentsView.top = Content Layout Guide.top
- ContentsView.leading = Content Layout Guide.leading
- ContentsView.bottom = Content Layout Guide.bottom
- ContentsView.trailing = Content Layout Guide.trailing

この制約手法は、複数のオブジェクトを選択した場合のみ使える制約となっており、一つしか選択していない状態だと、チェックを入れることが出来ません。

ではこれらはどういう制約なのか考えてみます。
Leading Edges 部品の左端から他の部品の左端までの水平方向の距離を指定とあるように2つの部品の距離を指定するものです。

例えばLeading Edgesを0にすることより、
(部品A)ContentLayoutGuideの左端と、(部品B)ContentsViewの左端の
距離を指定した"0"にすることができる。
要するにくっつくと言うことです。

※あくまで現時点の自分の理解では、オブジェクトの開始位置を、他のオブジェクトに依存して決める設定というイメージ。

4箇所指定することで、ContentLayoutGuideとContentsViewの4隅の位置を固定する制約となっている。

⑤FrameLayoutGuideとContentsViewの横幅を合わせる

上記④ではContentLayoutGuideに合わせてContentsViewの位置を設定したが、
ContentsView自身のサイズを決めていないので、こちらも設定していきます。(当然エラー状態となっている)

はじめにContentsViewの横幅を設定します。
高さはContentsView自身で定める必要があるが、横幅はScrollViewの枠に合わせる形が良いと思われます。(※一般的な縦のみスクロールの場合)

ContentsViewからcontrolキーを押しながらFrameLayoutGuideへドラッグし、Equal Widthsを選択。(青いガイドラインが出る)

scroll-7.png

追加された制約

- ContentsView.width = 0.57971 × Frame Layout Guide.width
  (数値は現在のContentsViewのサイズにより変動)

この時、元々のContentsViewの横幅を元に上記制約が作られてしまいます。
直すには制約のSize inspectorからMultiplier1に設定することで、ContentsViewをScrollViewの横幅目一杯に広げることができます。

scroll-8.png

⑥ContentsViewの高さについて制約をつける

続いて高さを決めていきます。
ContentsViewを選択し、AddNewConstraintsへ。
上の余白を0、高さを1000とした。

scroll-9.png

追加された制約

- ContentsView.top = Frame Layout Guide.top

これに加えてContentsView自身にもheight = 1000という制約が追加され、これで高さ&位置を確定することが出来ます。

これで制約エラーは解消されます!

ContentsViewを選択した状態でスクロールしてみてください。
動くハズです!

⑦Labelを配置する

ここからはおまけです。
ContentsViewになんでも好きなものを配置して動かしている気分を味わっていきましょう。

scroll-10.png

完成!

これで冒頭の完成品となりました!

後語り

いかがだったでしょうか?
まだまだ理解出来ていない部分も多いので、学習を進めていく過程で随時更新していきたいと考えています。

また、ContentLayoutGuideFrameLayoutGuideに関しては全然理解が追いついていないので、今後の課題と思ってしっかり学習していこうと思います!

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

[Xcode]UIScrollViewを最低限動かすための手順

iOS開発の学習をしています。

最近学んだ中で、特にUIScrollViewの扱いについてはかなり苦労しました。
そのUIScrollViewについて最低限動かすということに焦点を絞って備忘録的に書いています。

「なんだか良く分からないけど、誰かの記事見てそのまま書いて、動いたからオッケー!」
とはならないように、初学者なりに理屈を考えながら実装を試みました!
足りない部分も多々ありますが、私と同じような初学者の方々の参考になれば幸いです。

概要

新規プロジェクトを作成後、storyboradを使用しUIScrollViewをスクロールするところまでを実践する。

環境

  • macOS Catalina 10.15.7
  • Xcode - 11.3.1

完成品

Image from Gyazo

内容

  • UIScrollView
  • UIView
  • UILabel * 2

実践

①ViewにScrollViewを設置

Main.storyboard→View Controller→Viewを選択し、
ViewにScrollViewを設置する。

scroll-1.png

後ほど制約をつけるのでどこでも良いです。ちょっと大きめにしておいた方が見やすいかも。

②ScrollViewに制約をつける

先ほど設置したScrollViewにAutoLayoutで制約を加えていきます。
Add New Constraintsから設定していきます。
ScrollViewのサイズを決める制約となります。
親Viewに対しての余白を決めることで、サイズを決めています。

数字はなんでも良いですが今回は上下に50,左右に0とします。

scroll-2.png

Add 4 Constraintsを選択すると、ScrollViewに以下の4つの制約が追加されます。

- Safe Area.trailing = Scroll View.trailing
- Safe Area.bottom = Scroll View.bottom + 50
- Scroll View.leading = Safe Area.leading
- Scroll View.top = Safe Area.top + 50

また、ScrollViewに対し赤いラインが表示され"Scrollable Content Size Ambiguity"という制約エラーが出ます。

scroll-3.png

scroll-4.png

これは直訳の通り"スクロール可能なコンテンツのサイズが曖昧"という警告です。

補足:Scrollable Content Size Ambiguityとは?

では、このエラーは一体なんなのか?ということを考えていきます。

エラー文によると、

The scrollable size of a UIScrollView is computed automatically based upon the constraints of its subviews.

訳:UIScrollViewのスクロール可能なサイズは、そのサブビューの制約に基づいて自動的に計算されます。
スクロール可能なコンテンツの幅と高さを完全に定義するには、制約が必要です。

という内容が記載されています。
どうやらエラー文で指摘を受けたスクロール可能なコンテンツのサイズを定義するためにサブビューを制約する必要があるようです。

補足2:サブビューとは?

Xcodeのレイアウトはオブジェクトを上に重ねていくような構造になっており、ユーザから見て手前にあるビューをサブビュー(subview)、奥にあるビューをスーパービュー(superview)というようです。

scroll-debug.png

こんな感じ。(画像は完成品のレイアウト)

エラーは一旦スルーで

上記の理由から、現状ScrollViewはサブビューを持っていないのでエラーを解消することは出来ません。
ScrollViewの中にContentsViewなどを設置してからの対応となります。

③ScrollViewの中にViewを設置

scroll-5.png

ただのViewです。
分かりやすくするために、ScrollViewを水色、新たに設置したViewを灰色にしています。

このViewですが、名前は慣例的に"ContentsView"などとするのが良いらしいです。
本記事でも、以後ContentsViewと記載します。

④ContentsViewとContentLayoutGuideに制約をつける

こちらの記事がとても分かりやすく、大変参考になりました。
【Swift】Align制約の使い方。複数部品を整列する制約を追加する。(Swift 2.1、XCode 7.2)

scroll-6.png

コマンドキー押して2つ同時選択し、AutolayoutのAlignを選択。
Add New Alignment Constraintsから上下左右を固定します。

上4つを0にしてチェックを入れます。

  • Leading Edges
    部品の左端から他の部品の左端までの水平方向の距離を指定
  • Trailing Edges
    部品の右端から他の部品の右端までの水平方向の距離を指定
  • Top Edges
    部品の上端から他の部品の上端までの垂直方向の距離を指定
  • Bottom Edges
    部品の下端から他の部品の下端までの垂直方向の距離を指定

以下の制約が追加されます。

- ContentsView.top = Content Layout Guide.top
- ContentsView.leading = Content Layout Guide.leading
- ContentsView.bottom = Content Layout Guide.bottom
- ContentsView.trailing = Content Layout Guide.trailing

この制約手法は、複数のオブジェクトを選択した場合のみ使える制約となっており、一つしか選択していない状態だと、チェックを入れることが出来ません。

ではこれらはどういう制約なのか考えてみます。
Leading Edges 部品の左端から他の部品の左端までの水平方向の距離を指定とあるように2つの部品の距離を指定するものです。

例えばLeading Edgesを0にすることより、
(部品A)ContentLayoutGuideの左端と、(部品B)ContentsViewの左端の
距離を指定した"0"にすることができる。
要するにくっつくと言うことです。

※あくまで現時点の自分の理解では、オブジェクトの開始位置を、他のオブジェクトに依存して決める設定というイメージ。

4箇所指定することで、ContentLayoutGuideとContentsViewの4隅の位置を固定する制約となっている。

⑤FrameLayoutGuideとContentsViewの横幅を合わせる

上記④ではContentLayoutGuideに合わせてContentsViewの位置を設定したが、
ContentsView自身のサイズを決めていないので、こちらも設定していきます。(当然エラー状態となっている)

はじめにContentsViewの横幅を設定します。
高さはContentsView自身で定める必要があるが、横幅はScrollViewの枠に合わせる形が良いと思われます。(※一般的な縦のみスクロールの場合)

ContentsViewからcontrolキーを押しながらFrameLayoutGuideへドラッグし、Equal Widthsを選択。(青いガイドラインが出る)

scroll-7.png

追加された制約

- ContentsView.width = 0.57971 × Frame Layout Guide.width
  (数値は現在のContentsViewのサイズにより変動)

この時、元々のContentsViewの横幅を元に上記制約が作られてしまいます。
直すには制約のSize inspectorからMultiplier1に設定することで、ContentsViewをScrollViewの横幅目一杯に広げることができます。

scroll-8.png

⑥ContentsViewの高さについて制約をつける

続いて高さを決めていきます。
ContentsViewを選択し、AddNewConstraintsへ。
上の余白を0、高さを1000とした。

scroll-9.png

追加された制約

- ContentsView.top = Frame Layout Guide.top

これに加えてContentsView自身にもheight = 1000という制約が追加され、これで高さ&位置を確定することが出来ます。

これで制約エラーは解消されます!

ContentsViewを選択した状態でスクロールしてみてください。
動くハズです!

⑦Labelを配置する

ここからはおまけです。
ContentsViewになんでも好きなものを配置して動かしている気分を味わっていきましょう。

scroll-10.png

完成!

これで冒頭の完成品となりました!

後語り

いかがだったでしょうか?
まだまだ理解出来ていない部分も多いので、学習を進めていく過程で随時更新していきたいと考えています。

また、ContentLayoutGuideFrameLayoutGuideに関しては全然理解が追いついていないので、今後の課題と思ってしっかり学習していこうと思います!

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

Xcodeのよく使う便利機能・コマンド

はじめに

逆効率厨の僕がインターン中にトレーナーに教えてもらった便利機能とコマンドを紹介します!

今開いているファイルをProject Navigatorで表示する

めちゃくちゃでかいプロジェクトファイルを触っている時、どのグループのファイルか行方不明になったこと、ありませんか?そう、僕ですね。
そんな時、Command + Shift + jを押せば今開いているファイルをProject Navigatorで表示することができます。

検索

プロジェクト内でファイル検索する
Command + Shift + o
Xcodeの検索バーから検索するとコードも表示されるため、ファイルを探したいときはこちらの方が便利です。

ファイル内で検索する
Command + f

シュミレーターをshake

Cmd + Ctr + z
デバッグのときに便利

インデントを表示する

Editor < Invisibles
インデントが・・・で表示されて見やすくなります.
スクリーンショット 2021-01-11 3.13.04 (1).png

スペースのみの行を空行にする

Xcode > Preference > Text Editing > While Editingにチェック
ここにチェックを入れるとSwiftLint警察によってslackを荒らす回数が減ります。
ちなみに範囲選択してCmd +iするとインデントも修正してくれます。
スクリーンショット 2021-01-11 3.18.36.png

該当のコードを書いた人を探す

スクリーンショット 2021-01-11 3.26.11.png
ここから
スクリーンショット 2021-01-11 3.26.28.png
誰がいつ書いたコードかを表示することができます。

いい機能を見つけたらもっと追加していきます!
便利だと思ったものがあったらLGTMくれると嬉しいです!!!

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

【Swift】xibを使ってUITableViewを実装する

はじめに

UITableViewを使用する際にカスタムセルを.xibを使用して実装する方法について書きます。
xibを使用することでカスタムセルのcalss内の記述をStoryboardベースで実装することができます。

この記事でやること

UITableViewCellのxibにあるLabelのテキストを変更する
Simulator Screen Shot - iPhone 12 - 2021-01-11 at 02.00.41.png

実装

TableViewの追加

画面いっぱいにTableViewを追加します。
スクリーンショット 2021-01-11 2.03.14.png

xibファイルを作成

Cocoa Touch Classを選択し、
スクリーンショット 2021-01-11 2.05.40.png

UITableViewCellを選択、Also Create XIB fileにチェックを入れ、ファイルを作成する
スクリーンショット 2021-01-11 2.07.02.png

Labelの追加

CellにLabelを追加し、Outlet接続する
スクリーンショット 2021-01-11 2.16.03.png

TableViewを作成

TableViewとコードをOutlet接続し、extensionに一般的なTableViewのコード()を書きます
スクリーンショット 2021-01-11 2.16.51.png

ここで一般的なTableViewと違うのがRegisterを記述する点と

ViewController.swift
tableView.dataSource = self
tableView.delegate = self
// registerでxibをidentifierとして設定する
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")

ここで設定したidentifierを使用する点です。

ViewController.swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
    cell.titleLabel?.text = "aaa"

    return cell
}

ビルドしてみる

これで冒頭の通りxibを使用してUITableViewCellのテキストを変更することができたかと思います。
もし参考になればLGTMくれると嬉しいです!

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

[iOS] SourceryとAspectsを組み合わせて自動でタップイベントをトラッキングする

はじめに

継続的に機能改善を行うにはユーザの行動分析は欠かせません.そのため,多くの企業がユーザがアプリ内で開いた画面やタップしたボタンなどのインタラクションをトラッキングしているのではないでしょうか.

しかし,トラッキング処理は有益な一方でコードの見通しを悪くする要因の1つでもあります.なぜなら,トラッキングの処理はアプリの価値を表す主要な機能(以降,ビジネスルールと呼ぶことにします)とは一切関係が無いからです.それなのにも関わらず,トラッキング処理はビジネスルールが存在するのと同じ上位のレイヤーに現れることが多いため,尚更ノイズに感じてしまうのです.

こういった乱雑な処理は,どうにか隠蔽して意識せず開発を進めたいものです.この記事では,iOSアプリ上のボタンのタップイベントを対象にして,その方法を模索および提案します.

目的

あるViewControllerが持つUIButtonがタップされたときに,ログイベントを貯めているサーバに問い合わせを行います.このとき,実装者がトラッキングの処理を一切書かないことと見ないことを目指します.

実装

それでは,早速どのように実現するかについて説明します.

方針

先述した目的を実現するためには次の2つの処理が必要です.

1.ボタンのタップイベントを自動でハンドルして,タップイベントをサーバへ送信する.
2.対象とするViewController全てで1の処理を実行する.

1の処理はメタプログラミングによって実現できそうです.これは,対象とするViewControllerのプロパティとして保持している全てのUIButtonに対してaddTarget(_:action:for:)を呼び出すイメージです.
2の処理はAspectsをによって実現できそうです.これは,メタプログラミングをした1の処理をViewControllerの任意のライフサイクルにフックさせて実行させるイメージです.

実装例

方針だけではイメージしずらい部分もあると思うので,ここからはコードを交えつつ詳細な説明を行います.

メタプログラミングによるタップイベントのハンドリング

まず最初にメタプログラミングで実現したゴールを説明します.タップイベントをトラッキングしたいと思っている,2つのUIButtonを内部に持つAviewControllerを考えることにします.

class AViewController: UIViewController {
    let hogeButton = UIButton()
    let fugaButton = UIButton()
}

AviewControllerに対して次のようなコードを生成するのが一旦のゴールです.生成されたAviewController.registerTabEventLogger()は,ViewControllerとUIButtonの組み合わせによる文字列をパラメータとしたタップイベントをトラッキングするように各UIButtonに命令します.

extension AViewController {
    fileprivate func registerTabEventLogger() {

        hogeButton.addTarget(self, action: #selector(didTaphogeButton), for: .touchUpInside)
        fugaButton.addTarget(self, action: #selector(didTapfugaButton), for: .touchUpInside)

    }

    @objc private func didTaphogeButton() {
    Logger.send(event: .tap(button: "AViewController.hogeButton"))
    }

    @objc private func didTapfugaButton() {
    Logger.send(event: .tap(button: "AViewController.fugaButton"))
    }
}

ゴールを明示したところで,早速実装の説明に移ります.Swiftにおいて,こういったメタプログラミングをするのにはSourceryが有用です.Sourceryを使って上記のようなコード生成を行うために,次のようなProtoclとテンプレートファイルを記述しました.SourceryのREADMEに従ってセットアップを行いビルドを走らせると,TapEventLoggableControllerに準拠したクラスに対応するコードが自動生成されます.これが,タップイベントをサーバへ伝える処理を自動で生成する処理です.

protocol TapEventLoggableController { }

// 全てのUIViewControllerへ適応させる
extension UIViewController: TapEventLoggableController {}
// あるいは特定のUIViewControllerへ適応させる
extension AViewController: TapEventLoggableController {}
import UIKit

<%_ for vc in types.classes.filter { $0.implements["TapEventLoggableController"] != nil } { -%>
extension <%= vc.name %> {
    fileprivate func registerTabEventLogger() {

    <%_ for button in vc.variables.filter { $0.typeName.name == "UIButton" } { -%>
        <%= button.name %>.addTarget(self, action: #selector(didTap<%= button.name %>), for: .touchUpInside)
    <% } -%>

    }
    <%_ for button in vc.variables.filter { $0.typeName.name == "UIButton" } { -%>

    @objc private func didTap<%= button.name %>() {
        Logger.send(event: .tap(button: "<%= vc.name %>.<%= button.name %>"))
    }
    <% } -%>
}
<% } -%>

Aspectsによるタップイベントの監視の開始

registerTabEventLogger()を呼び出すことによって,タップイベントがサーバへ伝達されるようになりました.それではこのメソッドは誰が呼ぶのでしょうか?ViewControllerの中で決まりごととして毎回呼びますか?それは呼び忘れのミスに繋がるし,トラッキング処理の隠蔽がしきれていません.

今回はAspectsを使ってViewControllerの.viewDidAppear(_:)をhookしてregisterTabEventLogger()を呼び出すようにしました.結果が次のコードです.

// Generated using Sourcery 1.0.0 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT

import UIKit
import Aspects

extension AViewController {    
    fileprivate func registerTabEventLogger() {

        hogeButton.addTarget(self, action: #selector(didTaphogeButton), for: .touchUpInside)
        fugaButton.addTarget(self, action: #selector(didTapfugaButton), for: .touchUpInside)

    }

    @objc private func didTaphogeButton() {
        Logger.send(event: .tap(button: "AViewController.hogeButton"))
    }

    @objc private func didTapfugaButton() {
        Logger.send(event: .tap(button: "AViewController.fugaButton"))
    }
}

enum AspectTracker {
    private static var token: AspectToken?

    static func setup() {
        guard token == nil else { return }

        token = try? UIViewController.hook(
            #selector(UIViewController.viewDidAppear(_:)),
            with: .positionBefore,
            using: { info, animated in
        // 抽象化したい気持ちが湧いてきますが,型の継承関係は暗黙的に変えたくないので,やっていません.
                if let vc = info?.instance() as? AViewController {
                    vc.registerTabEventLogger()
                }
            }
        )
    }
}

import UIKit
import Aspects

<%_ for vc in types.classes.filter { $0.implements["TapEventLoggableController"] != nil } { -%>
extension <%= vc.name %> {
    fileprivate func registerTabEventLogger() {

    <%_ for button in vc.variables.filter { $0.typeName.name == "UIButton" } { -%>
        <%= button.name %>.addTarget(self, action: #selector(didTap<%= button.name %>), for: .touchUpInside)
    <% } -%>

    }
    <%_ for button in vc.variables.filter { $0.typeName.name == "UIButton" } { -%>

    @objc private func didTap<%= button.name %>() {
        Logger.send(event: .tap(button: "<%= vc.name %>.<%= button.name %>"))
    }
    <% } -%>
}
<% } -%>

enum AspectTracker {
    private static var token: AspectToken?

    static func setup() {
        guard token == nil else { return }

        token = try? UIViewController.hook(
            #selector(UIViewController.viewDidAppear(_:)),
            with: .positionBefore,
            using: { info, animated in
                <%_ for vc in types.classes.filter { $0.implements["TapEventLoggableController"] != nil } {-%>
                if let vc = info?.instance() as? <%= vc.name %> {
                    vc.registerTabEventLogger()
                }
                <% } -%>
            }
        )
    }
}

これでAspectTracker.setup()を任意の場所で一回呼べば,それ以降は自動トラッキングが走るようになりました.

デモ

以上までで説明した処理を結合すると動画に示すような処理が可能となります.これは,画面内に有る青いボタンをタップするとトラッキングのログがコンソールへ吐き出されている様子を表しています.

画面内に有る青と赤の領域にあるボタンをタップするとトラッキングのログが吐き出されている様子

改善

実運用するにあたって必要となってきそうなことと,その解決方法の考えを明記しておきます.

タップ時にサーバへ送信するイベントを柔軟に変えたい

ポリモーフィズムを使えば良い

protocol TapEventLoggableController {
    var tapEventMap: [UIButton: Event] { get }
}

extension TapEventLoggableController {
    var tapEventMap: [UIButton : Event] {
        [:]
    }
}

class AViewController: UIViewController {
    let hogeButton = UIButton()
    let fugaButton = UIButton()

    var tapEventMap: [UIButton : Event] {
        [hogeButton: .tapHogeButton]
    }
}

@objc private func didTaphogeButton() {
    if let event = tapEventMap[hogeButton] {
        Logger.send(event: event)
    } else {
        Logger.send(event: .tap(button: "BViewController.hogeButton"))
    }
}

サーバへイベントを送信するクラスをDIしたい

ViewControllerが保持すれば良い.

protocol TapEventLoggableController {
     var logger: Logger { get }
}

おわりに

この記事では自動でタップイベントをトラッキングする方法を紹介しました.この方法を応用すれば,タップイベント以外のイベント,例えばインプレッションのイベントも容易にトラッキングすることが可能だと思います.ぜひ試してみてください.

参考文献

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

Swiftにおけるfinal修飾子とその強制

はじめに

Swiftには他の言語と同じようにfinal修飾子があります.
この修飾子はLanguage Guideに次のような説明がされています.

You can prevent a method, property, or subscript from being overridden by marking it as final. Do this by writing the final modifier before the method, property, or subscript’s introducer keyword (such as final var, final func, final class func, and final subscript).

Any attempt to override a final method, property, or subscript in a subclass is reported as a compile-time error. Methods, properties, or subscripts that you add to a class in an extension can also be marked as final within the extension’s definition.

You can mark an entire class as final by writing the final modifier before the class keyword in its class definition (final class). Any attempt to subclass a final class is reported as a compile-time error.

https://docs.swift.org/swift-book/LanguageGuide/Inheritance.html

要約すると,この修飾子を用いることにより,プロパティやメソッドのオーバーライドおよびクラスの継承を禁止することができます.

継承を禁止する2つの利点

さて,このfinal修飾子を使う利点とはなんでしょうか.
私は大きく分けて次の2つが挙げられると考えています.

  1. パフォーマンスの向上
  2. 変更の影響範囲を限定する

パフォーマンスの向上

これは,Appleの公開しているブログにも言及されています.

The final keyword is a restriction on a class, method, or property that indicates that the declaration cannot be overridden. This allows the compiler to safely elide dynamic dispatch indirection. For instance, in the following point and velocity will be accessed directly through a load from the object’s stored property and updatePoint() will be called via a direct function call. On the other hand, update() will still be called via dynamic dispatch, allowing for subclasses to override update() with customized functionality.

https://developer.apple.com/swift/blog/?id=27

final修飾子を使用してあるクラスの継承を禁止すると,そのクラスのプロパティやメソッドをDynamic Dispatchを介さずに直接読み込めるようです.
その結果,パフォーマンスが向上すると述べられています.

より具体的に考えてみましょう,
例えば,次のように,AnimalクラスとCatクラスがあるとします.
Animalクラスは継承可能となっているため,animal.eat()にて実際に呼び出されるのはAnimalクラスかそのサブタイプ(Catクラス)のeat()となります.
よって,コンパイル時に実際に何が呼ばれるのかを判定する処理(*1)が必要になります.

class Animal {
  func eat() { 
    print("eat")
  }
}

class Cat: Animal {
  override func eat() { 
    super.eat()
    print("cat food")
  }
}

let animal: Animal = Cat()
animal.eat()

しかし,Animalクラスにfinal修飾子が付いていたらどうでしょうか.
その場合はanimal.eat()で呼び出されるのはAnimalクラスのeat()になることが約束されます.
よって,コンパイル時に直接Animalクラスの`eat()を直接アクセスすれば良くなるため,パフォーマンスが向上します.

変更の影響範囲を限定する

final修飾子を使用することは,そのクラスの変更による影響を最小限にすることに繋がります.
結果的に,意図せずバグを生んでしまったり,コードの可読性を向上させることに寄与します.

先述したCatとAnimalを例にとります.
CatはAnimalを継承し,そのメソッドをオーバーライドし,その中でAnimalのメソッドを呼び出しています.
そのため,Animalのメソッドに変更を加えた場合,Catのメソッドの振る舞いが変わります.
つまり,これはAnimalに変更を加える度に,そのサブタイプが破壊されていないか確認する必要があることを示しています.
こういった,不用意に受ける影響の確認と修正を要されるくらいなら,継承させる必要がないクラスにはfinal修飾子を付けた方が良いかもしれません.

final修飾子を強制する

先述したようなfinal修飾子による恩恵を最大限受けるために,継承されていないクラスにはfinal修飾子を付けましょう.
しかし,いくら意識したところで,プログラミング中のトライ&エラーの過程でfinal修飾子のことをすっかり忘れてしまうかもしれません.
もしくは,チームにそういった意識が根付いていないかもしれません.
どうにか,機械的に解決(final修飾子を強制)できないでしょうか?

この方法には次の2つがあると思います.

  • 静的解析ツールを使用して,継承していないクラスを指摘する.
  • 静的解析ツールやフォーマッターを活用して,継承されていないクラスに対して自動的にfinal修飾子を付ける

今回はDangerを用いて,前者の解決方法を試みました.

Dangerプラグインの作成

継承されていないクラスを検知し,PRのコメントにてそれを指摘するDangerプラグインを作成しました.
なお,このプラグインが検知できるのは次のケースです.

  • 既にあるrclassへ付けてあったfinalが誤って外された時
  • 新しく定義したタイプにfinalが必要な時
  • 消去したタイプの親タイプにfinalが必要な時

普段rubyを書き慣れていないため,読みにくいのはご容赦下さい??
(また,何か例外があればご指摘頂けると嬉しいです)

module Danger
  class ForceSwift < Plugin
    Class = Struct.new(:parent_type, :self_type, :is_final?)
    LinePosition = Struct.new(:file_path, :line_number)

    def warn_if_needed(target_paths)
      all_classes = []
      modified_classes = []
      deleted_classes = []
      @position = {}

      diff_swift_files = (git.modified_files + git.added_files)
                         .filter { |file| File.extname(file) == '.swift' }
                         .map { |file| git.diff_for_file(file).patch }

      project_files = target_paths
                      .flat_map { |root| Dir[File.join("#{root}/**", '*.swift')] }

      diff_swift_files.each do |text|
        text.split("\n").each do |line|
          case line
          when /^\+/
            add_class_to_list(line, modified_classes)
          when /^\-/
            add_class_to_list(line, deleted_classes)
          end
        end
      end

      project_files.each do |path|
        line_number = 0
        File.open(path)  do |file|
          file.each_line do |line|
            line_number += 1
            is_success = add_class_to_list(line, all_classes)
            @position[all_classes.last.self_type] = LinePosition.new(path, line_number) if is_success
          end
        end
      end

      modified_classes
        .each { |a_class| warn_if_final_is_needed(a_class, all_classes) }
      deleted_classes
        .flat_map { |a_class| parents(a_class, all_classes) }
        .each { |a_class| warn_if_final_is_needed(a_class, all_classes) }
    end

    private

    def children(reference_class, all_classes)
      all_classes.filter { |a_class| a_class.parent_type.include?(reference_class.self_type) }
    end

    def parents(reference_class, all_classes)
      all_classes.filter { |a_class| reference_class.parent_type.include?(a_class.self_type) }
    end

    def warn_if_final_is_needed(a_class, all_classes)
      children = children(a_class, all_classes)
      is_leaf = children.empty?
      if is_leaf && !a_class.is_final?
        warn(
          "おそらく#{a_class.self_type}にはfinal修飾子が必要です.",
          file: @position[a_class.self_type].file_path,
          line: @position[a_class.self_type].line_number
        )
      else
        children.each { |node| warn_if_final_is_needed(node, all_classes) }
      end
    end

    def add_class_to_list(line, list)
      class_difinication_match = line.match(/class (.+) {/)
      return false unless !class_difinication_match.nil? && !class_difinication_match[1].nil?

      is_final = !line.match(/.*final.+class/).nil?
      hierarchy_match = class_difinication_match[1].match(/(.+): (.+)/)
      if !hierarchy_match.nil? && !hierarchy_match[2].nil?
        parents = hierarchy_match[2].split(',').map { |line| line.strip }
        list << Class.new(parents, hierarchy_match[1], is_final)
      else
        list << Class.new([], class_difinication_match[1], is_final)
      end
      true
    end
  end
end

このプラグインを使用すると次のように,Botが指摘してくれます.

まとめ

  • final修飾子を付けることには2つのメリットがある
  • 付けるのを忘れがちならDangerプラグインを使うなどの解決方法がある

注釈

(*1) たぶんこれがDynamic Dispatch

参考文献

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