20200318のiOSに関する記事は8件です。

「ViewControllerをたくさん重ねると、いかにもメモリに悪そう」ということをXcodeを使って視覚的に表現してみた

1 はじめに

この記事は技術的な内容というよりは、実験系の内容になります。

ViewControllerを重ね合わせると、メモリに悪いというのは周知の事実ですが、視覚的に「いかにも悪そうだ〜」という記事は、私が探した限りでは見つけられなかったので掲載します。

2 試したこと

まず、最初に3つのViewControllerを用意します。
MakeManyVC.001.png

次にUIBUttonを用意して、下記のようなコードを書きます。
(UIButtonは省略します)

ViewCotrollerを一つ用意して、画面遷移する(firstVC→secondVC)
@objc func goSecondVC() {
        //secondVCを実体化させる
        let secondVC = SecondVC()
        //用意したVCへ画面遷移
        self.present(secondVC, animated: true, completion: nil)
       }
ViewControllerを一つ用意して、画面遷移する(secondVC→thirdVC)
@objc func goThirdVC() {
        //thirdVCを実体化させる
        let thirdVC = ThridVC()
        //用意したVCへ画面遷移
        self.present(thirdVC, animated: true, completion: nil)
       }
ViewControllerを1つ用意して画面遷移する(thirdVC→firstVC)
@objc func goFirstVC() {
        //firstVCを実体化させる
        let firstVC = FirstVC()
        //用意したVCへ画面遷移
        self.present(firstVC, animated: true, completion: nil)
       }

このコードでXcodeのシミュレターを立ち上げて、次々と画面遷移していきます。
MakeManyVC.002.png

動画だとこんな感じになります。
manyViewController.gif

これを7回繰り返すと、こんな感じにViewが現れます。
スクリーンショット 2020-03-18 20.53.00.png

これだけでも、
メモリをたくさん使っているように見えますね。

さらに画面遷移を重ねていきます。
スクリーンショット 2020-03-18 20.49.14.png

ここまでくると、明らかにメモリを使ってそうだというのがわかります。

 
ちなみに下記サイトがメモリに関してわかりやすく説明されていました。
https://wa3.i-3-i.info/word16066.html

3. メモリを圧迫しないためには

ViewControllerを消す必要があります。

例えば、thirdVCからfirstVCへ画面遷移するときに、このようなコードを書くことでViewControllerが破棄出来ます。
Swift 二つ前の画面に戻る方法を参考にさせていただきました。

2つ前の画面に戻って、ViewControllerを破棄する(thirdVC→firstVC)
@objc func dismissTwoVC() {
    //2つ前のVCへ戻り、子VC(secondVC,thirdVCを削除)
    self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)       }

イメージはこのようになります。
(間違っていた場合は、ご教授いただけますと幸いです)
MakeManyVC.003.png
MakeManyVC.004.png
 
 
こうすることで視覚的にも「メモリに良さそう」な実装になります。
スクリーンショット 2020-03-19 0.23.38.png

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

Azure PipelinesのYAMLでiOSアプリのApp Center配布パイプラインを構築する方法

「Azure PipelinesのYAMLでiOSアプリのCI/CD環境を構築する」は3部構成です。
記事を順番に読み進めると、Azure PipelinesでiOSアプリのCI/CD環境が構築できるようになります。

はじめに

Azure Pipelinesを使い、iOSアプリをApp Centerへ配布するCDを構築します。

本記事で書かないこと

  • Azure Pipelinesの概要や基本的な操作方法
    私が以前書いた記事が参考になると思います
  • Visual Studio App Centerの概要や基本的な操作方法
  • CI環境の構築で説明した内容
    以前説明した内容を説明するのは冗長なのでしません

App Centerのコネクションを追加

Azure DevOpsのプロジェクトに、App Centerを接続します。

Azure DevOps > Project Settings > Pipelines - Service connections > 「New service connection」をクリック
スクリーンショット_2020-03-18_17_43_19.jpg

以下のURLからでもアクセスできる
https://dev.azure.com/{Organization}/{Project}/_settings/adminservices

「Visual Studio App Center」を選択し、「Next」をクリック
スクリーンショット 2020-03-18 17.53.26.png

API TokenにApp CenterのAPIトークン、Service connection nameに「App Center」など適当な名前を入力し、「save」をクリック
スクリーンショット 2020-03-18 17.55.14.png

これでApp Centerの接続は完了です。

証明書とProvisioning Profileのアップロード

証明書とProvisioning ProfileをAzure Pipelines内の安全な場所へアップロードします。

Azure DevOps > Pipelines > Library > [Secure files]タブ > 「+ Secure file」をクリック
スクリーンショット_2020-03-18_17_59_51.jpg

ファイルをドラッグ&ドロップし、「OK」をクリック
スクリーンショット 2020-03-18 18.03.10.png

これをファイル数分繰り返せば、証明書とProvisioning Profileのアップロードは完了です。

P12ファイルのパスワードの追加

P12ファイルのパスワードをAzure Pipelinesの安全な場所で保持します。

まず変数のグループを作成し、その中に変数を追加します。

Azure DevOps > Pipelines > Library > [Variable group]タブ > 「+ Variable group」をクリック
スクリーンショット_2020-03-18_18_15_32.jpg

Variable group nameに適当な名前を入力し、VariablesにP12ファイルのパスワードを追加し、「Save」をクリック
スクリーンショット_2020-03-18_18_39_13.jpg

パスワードの変数名は適当な名前でOKです。
Standardのみ契約している場合、単純に P12Password でいいと思います。

これでP12ファイルのパスワードの追加は完了です。

Makefileの作成

アーカイブやIPAファイルの作成などの実行コマンドをまとめたMakefileを作成します。
CI環境の構築で紹介したコマンドは省略します。

Makefile

例はAd Hocなので、In-Houseの場合は変数名などを適当に修正してください。

DEVELOPMENT_TEAMPRODUCT_BUNDLE_IDENTIFIERADHOC_PROVISIONING_PROFILE_SPECIFIER は自分のプロジェクトの値に置き換えてください。

ADHOC_EXPORT_OPTIONS_PATH := ./ExportOptions/ExportOptionsAdHoc.plist としているので、このパスに.plistファイルを配置してください。
Xcode上からアーカイブ→エクスポートすると.plistが生成されるので、それをそのまま使うのがオススメです。

Makefile
SDK := iphoneos
CONFIGURATION := Release
ARCHIVE_PATH := ./build/${PRODUCT_NAME}.xcarchive
EXPORT_PATH := ./output/${SDK}/${CONFIGURATION}

DEVELOPMENT_TEAM := XXXXXXXXXX
PRODUCT_BUNDLE_IDENTIFIER := com.example.Foo
ADHOC_PROVISIONING_PROFILE_SPECIFIER := Foo_AdHoc
ADHOC_EXPORT_OPTIONS_PATH := ./ExportOptions/ExportOptionsAdHoc.plist

.PHONY: generate-ipa-adhoc
generate-ipa-adhoc: # Generate IPA file for Ad Hoc
    $(MAKE) archive-adhoc
    $(MAKE) export-archive-adhoc

.PHONY: archive-adhoc
archive-adhoc:
    $(MAKE) archive PROVISIONING_PROFILE_SPECIFIER=${ADHOC_PROVISIONING_PROFILE_SPECIFIER}

.PHONY: export-archive-adhoc
export-archive-adhoc:
    $(MAKE) export-archive EXPORT_OPTIONS_PATH=${ADHOC_EXPORT_OPTIONS_PATH}

.PHONY: archive
archive:
    set -o pipefail && \
xcodebuild \
-sdk ${SDK} \
-configuration ${CONFIGURATION} \
-workspace ${WORKSPACE_NAME} \
-scheme ${SCHEME_NAME} \
-archivePath ${ARCHIVE_PATH} \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="iPhone Distribution" \
PROVISIONING_PROFILE_SPECIFIER=${PROVISIONING_PROFILE_SPECIFIER} \
DEVELOPMENT_TEAM=${DEVELOPMENT_TEAM} \
PRODUCT_BUNDLE_IDENTIFIER=${PRODUCT_BUNDLE_IDENTIFIER} \
clean archive \
| bundle exec xcpretty

.PHONY: export-archive
export-archive:
    set -o pipefail && \
xcodebuild \
-exportArchive \
-archivePath ${ARCHIVE_PATH} \
-exportPath ${EXPORT_PATH} \
-exportOptionsPlist ${EXPORT_OPTIONS_PATH} \
| bundle exec xcpretty

アーカイブ時に CODE_SIGN_STYLE=Manual を指定し、手動で署名しているのがポイントです。
これにより、開発時に自動署名していても確実に署名できます。

今回のMakefileは、本記事で使うコマンドのみを抜粋および編集して紹介しています。
私が普段使っているMakefileの全容はGitHub Gistにあるので、よかったら参考にしてください。

設定ファイルの構成

CI環境の構築で紹介したので省略します。

各項目の紹介

各項目を上から順に紹介します。

trigger

私は手動でApp Centerへ配布したいため、トリガーをOFFにしています。

trigger: none

pool

CI環境の構築と同様なので省略します。

variables

先ほど追加した変数を使うには、変数グループを明記する必要があります。

variables:
- group: Foo-iOS

steps

パイプライン内のステップを1つずつ紹介します。
ライブラリのインストールなど、CI環境の構築と同様のステップは省略します。

証明書のインストール

証明書をインストールします。

標準で用意されているタスクを使います。
certSecureFile にP12ファイルの名前、 certPwd に先ほど追加したP12ファイルのパスワードの変数を指定します。

- task: InstallAppleCertificate@2
  inputs:
    certSecureFile: 'Certificate.p12'
    certPwd: $(P12Password)
    keychain: 'temp'

Provisioning Profileのインストール

Provisioning Profileをインストールします。

証明書のインストールと同様、標準で用意されているタスクを使います。
provProfileSecureFile にProvisioning Profileの名前を指定します。

- task: InstallAppleProvisioningProfile@1
  inputs:
    provisioningProfileLocation: 'secureFiles'
    provProfileSecureFile: 'Foo_AdHoc.mobileprovision'

IPAファイルの作成

IPAファイルを作成します。

- script: make generate-ipa-adhoc
  displayName: Generate IPA file for Ad Hoc

アーカイブとIPAファイルをアーティファクトのステージングへコピー

配布に問題があった場合に調査するため、アーカイブとIPAファイルをダウンロードできるようにします。

そのためには「ステージングへコピー→アップロード」と2つのタスクを実行します。

- task: CopyFiles@2
  inputs:
    Contents: |
      **/*.xcarchive/**/*
      **/output/iphoneos/Release/*
    TargetFolder: '$(Build.ArtifactStagingDirectory)'

Contents では、Makefileで定義した ARCHIVE_PATHEXPORT_PATH をワイルドカードで指定しています。
.xcarchiveファイルはフォルダ扱いのため、末尾を **/* としています。

アーティファクトへアップロード

直前のタスクでステージングへコピーしたので、本タスクでアップロードします。

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'
    publishLocation: 'Container'

これにより、パイプラインの実行結果からアーカイブとIPAファイルがダウンロードできるようになります。

App Centerへ配布

作成したIPAファイルをApp Centerへ配布します。

serverEndpoint は先ほど追加したコネクションの名前を指定します。
appSlug は自分のApp Centerの値に置き換えてください。
releaseNotesInput はテストアプリの配布なのでいろいろな情報を表示しています。必要に応じて変更してください。
最後の $(ReleaseNote) はパイプラインの手動実行時に値を入れることで、リリースノートを書けるようにしています。

- task: AppCenterDistribute@3
  inputs:
    serverEndpoint: 'App Center'
    appSlug: '{Organization}/{App}'
    appFile: '**/*.ipa'
    releaseNotesOption: 'input'
    releaseNotesInput: |
      - Environment:'Ad Hoc'
      - Configuration:Release
      - Certificate:$(APPLE_CERTIFICATE_SIGNING_IDENTITY)
      - Provisioning Profile:$(APPLE_PROV_PROFILE_UUID)
      - Branch:$(Build.SourceBranchName)
      - Last Commit ID:$(Build.SourceVersion)
      - Last Commit Comment:$(Build.SourceVersionMessage)
      - Release Note:$(ReleaseNote)
    destinationType: 'groups'

設定ファイルの全体図

最後に設定ファイルの全体図を載せます。

trigger: none

pool:
  vmImage: 'macos-latest'

variables:
- group: Foo-iOS

steps:
# 環境変数のエクスポート
- script: |
    export DEVELOPER_DIR=/Applications/Xcode_11.3.1.app/Contents/Developer
    export MINT_PATH=mint/lib
    export MINT_LINK_PATH=mint/bin
  displayName: Export environment variables

# Xcodeの一覧出力
- script: ls /Applications | grep 'Xcode'
  displayName: Show Xcode list

# Xcodeのバージョン出力
- script: xcodebuild -version
  displayName: Show Xcode version

# Bundlerで管理しているライブラリのインストール
- script: make install-bundler
  displayName: Bundle install

# Mintのインストール
- script: brew install mint
  displayName: Install Mint

# Carthageで管理しているライブラリのインストール
- script: make install-carthage
  displayName: Install Carthage frameworks

# ライセンス情報の生成、プロジェクトファイルの生成、CocoaPodsで管理しているライブラリのインストール
- script: make generate-licenses
  displayName: Generate licenses, Xcode project, And Pod install

# 証明書のインストール
- task: InstallAppleCertificate@2
  inputs:
    certSecureFile: 'Certificate.p12'
    certPwd: $(P12Password)
    keychain: 'temp'

# Provisioning Profileのインストール
- task: InstallAppleProvisioningProfile@1
  inputs:
    provisioningProfileLocation: 'secureFiles'
    provProfileSecureFile: 'Foo_AdHoc.mobileprovision'

# IPAファイルの生成
- script: make generate-ipa-adhoc
  displayName: Generate IPA file for Ad Hoc

# アーカイブとIPAファイルをアーティファクトのステージングへコピー
- task: CopyFiles@2
  inputs:
    Contents: |
      **/*.xcarchive/**/*
      **/output/iphoneos/Release/*
    TargetFolder: '$(Build.ArtifactStagingDirectory)'

# アーティファクトへアップロード
- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'
    publishLocation: 'Container'

# App Centerへ配布
- task: AppCenterDistribute@3
  inputs:
    serverEndpoint: 'App Center'
    appSlug: '{Organization}/{App}'
    appFile: '**/*.ipa'
    releaseNotesOption: 'input'
    releaseNotesInput: |
      - Environment:'Ad Hoc'
      - Configuration:Release
      - Certificate:$(APPLE_CERTIFICATE_SIGNING_IDENTITY)
      - Provisioning Profile:$(APPLE_PROV_PROFILE_UUID)
      - Branch:$(Build.SourceBranchName)
      - Last Commit ID:$(Build.SourceVersion)
      - Last Commit Comment:$(Build.SourceVersionMessage)
      - Release Note:$(ReleaseNote)
    destinationType: 'groups'

おわりに

Azure PipelinesのYAMLでiOSアプリをApp Centerへ配布することができました!

Fastlaneを使えばもう少しかんたんに実現できそうですが、知識不足なのでできませんでした…。

参考リンク

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

Azure PipelinesのYAMLでiOSアプリのCI環境を構築する方法

「Azure PipelinesのYAMLでiOSアプリのCI/CD環境を構築する」は3部構成です。
記事を順番に読み進めると、Azure PipelinesでiOSアプリのCI/CD環境が構築できるようになります。

はじめに

Azure Pipelinesを使い、iOSアプリのビルドと単体テストを行うCIを構築します。

本記事で書かないこと

Makefileの作成

まず、ビルドや単体テストなどの実行コマンドをまとめたMakefileを作成します。
スクリプトを直接CIの定義ファイルに記述してもいいのですが、以下の理由から避けています。

  • ローカルでもかんたんに各コマンドを実行したい
  • 他のCIサービスに移行しやすくしたい

Makefileは長いので、折りたたみます。

Makefile

PRODUCT_NAME は自分のプロジェクトの製品名に置き換えてください。

Makefile
PRODUCT_NAME := Foo
SCHEME_NAME := ${PRODUCT_NAME}
WORKSPACE_NAME := ${PRODUCT_NAME}.xcworkspace
UI_TESTS_TARGET_NAME := ${PRODUCT_NAME}UITests

TEST_SDK := iphonesimulator
TEST_CONFIGURATION := Debug
TEST_PLATFORM := iOS Simulator
TEST_DEVICE ?= iPhone 11 Pro Max
TEST_OS ?= 13.3
TEST_DESTINATION := 'platform=${TEST_PLATFORM},name=${TEST_DEVICE},OS=${TEST_OS}'

.PHONY: install-bundler
install-bundler: # Install Bundler dependencies
    bundle config path vendor/bundle
    bundle install --jobs 4 --retry 3

.PHONY: install-cocoapods
install-cocoapods: # Install CocoaPods dependencies and generate workspace
    bundle exec pod install

.PHONY: install-carthage
install-carthage: # Install Carthage dependencies
    mint run carthage carthage bootstrap --platform iOS --cache-builds

.PHONY: generate-licenses
generate-licenses: # Generate licenses with LicensePlist and regenerate project
    mint run LicensePlist license-plist --output-path ${PRODUCT_NAME}/Settings.bundle
    $(MAKE) generate-xcodeproj

.PHONY: generate-xcodeproj
generate-xcodeproj: # Generate project with XcodeGen
    mint run xcodegen xcodegen generate
    $(MAKE) install-cocoapods

.PHONY: build-debug
build-debug: # Xcode build for debug
    set -o pipefail && \
xcodebuild \
-sdk ${TEST_SDK} \
-configuration ${TEST_CONFIGURATION} \
-workspace ${WORKSPACE_NAME} \
-scheme ${SCHEME_NAME} \
build \
| bundle exec xcpretty

.PHONY: test
test: # Xcode test # TEST_DEVICE=[device] TEST_OS=[OS]
    set -o pipefail && \
xcodebuild \
-sdk ${TEST_SDK} \
-configuration ${TEST_CONFIGURATION} \
-workspace ${WORKSPACE_NAME} \
-scheme ${SCHEME_NAME} \
-destination ${TEST_DESTINATION} \
-skip-testing:${UI_TESTS_TARGET_NAME} \
clean test \
| bundle exec xcpretty

.PHONY: show-devices
show-devices: # Show devices
    instruments -s devices

今回のメインはAzure Pipelinesなので、Makefileの内容は解説しません。
私が以前書いたGitHub Actionsの記事が参考になると思うので、よかったらご参照ください。

今回のMakefileは、本記事で使うコマンドのみを抜粋および編集して紹介しています。
私が普段使っているMakefileの全容はGitHub Gistにあるので、よかったら参考にしてください。

設定ファイルの構成

今回はYAMLファイルを使ってCI環境を構築します。
Classic Editorを使ってGUIで構築する場合、私が以前書いた記事が参考になると思います。

最初に全体の構成を説明します。

trigger:
  

schedules:
  

pool:
  vmImage: 'macos-latest'

steps:
  
名前 説明
trigger パイプライン実行のトリガー
schedules パイプライン定期実行のスケジュール
pool - vmImage 実行するマシン
iOSアプリはmacOSでしかビルドできないため macos-latest を指定している
steps パイプラインのステップ

各項目の紹介

各項目を上から順に紹介します。

trigger

パイプラインを実行するトリガーを設定します。

私は以下のブランチのプッシュをトリガーにしています。

  • master
  • release/*
  • develop

feature/* ブランチまで実行すると、料金が高くつくので避けています。

trigger:
  batch: true
  branches:
    include:
    - master
    - release/*
    - develop
    # - feature/*
  paths:
    exclude:
    - README.md
    - LICENSE
    - Rambafile

README.mdRambafile など、iOSアプリに直接関係ないファイルは、プッシュしてもCIが動かないようにしています。

schedules

パイプラインを定期実行するスケジュールを設定します。

私は毎日0時に以下のブランチをCIするようにしています。

  • develop
    ∵念のため
    プッシュ時にCIを実行していれば、定期実行はしなくてもいいと思います
  • feature/*
    ∵プッシュ時にCIしていないため
    develop ブランチへマージせずに帰宅しても、次の日の朝にCIの結果がわかるので便利です
schedules:
- cron: "0 15 * * *"
  displayName: Daily midnight build
  branches:
    include:
    # - master
    # - release/*
    - develop
    - feature/*
  always: false

cron式で指定する時間はUTCであり、日本時間にするには9時間足す必要があります。
15:00 UTC = 00:00 JST

alwaysで false を指定すると、前回の実行から変更があったブランチのみを対象にします。

pool

実行するマシンを設定します。

pool:
  vmImage: 'macos-latest'

「設定ファイルの構成」で紹介した内容がすべてです。

steps

パイプライン内のステップを1つずつ紹介します。

ステップは steps に記述します。

trigger:
  

schedules:
  

pool:
  vmImage: 'macos-latest'

steps:
  # ここにステップを記述する
  

チェックアウト

特別なことをしない限り、チェックアウトするリポジトリやブランチは指定する必要がありません。

環境変数のエクスポート

環境変数をエクスポートします。

- script: |
    export DEVELOPER_DIR=/Applications/Xcode_11.3.1.app/Contents/Developer
    export MINT_PATH=mint/lib
    export MINT_LINK_PATH=mint/bin
  displayName: Export environment variables

環境変数はYAMLで直接指定できそうですが、やり方がわからなかったので手動で行っています。

Xcodeの一覧出力(任意)

CIサーバー上にインストールされているXcodeの一覧を出力します。

- script: ls /Applications | grep 'Xcode'
  displayName: Show Xcode list

Xcodeのバージョン出力(任意)

指定したXcodeのバージョンを出力します。

- script: xcodebuild -version
  displayName: Show Xcode version

Bundlerで管理しているライブラリのインストール

Bundlerで管理しているライブラリをインストールします。

- script: make install-bundler
  displayName: Bundle install

Mintのインストール

Mintをインストールします。

- script: brew install mint
  displayName: Install Mint

Carthageで管理しているライブラリのインストール

Carthageで管理しているライブラリをインストールします。

- script: make install-carthage
  displayName: Install Carthage frameworks

ライセンス情報の生成、プロジェクトファイルの生成、CocoaPodsで管理しているライブラリのインストール

ライセンス情報の生成、プロジェクトファイルの生成、CocoaPodsで管理しているライブラリのインストールを行います。

- script: make generate-licenses
  displayName: Generate licenses, Xcode project, And Pod install

ビルド

ビルドします。

- script: make build-debug
  displayName: Xcode build for debug

単体テストの実行

単体テストを実行します。

- script: make test
  displayName: Xcode test

設定ファイルの全体図

最後に設定ファイルの全体図を載せます。

trigger:
  batch: true
  branches:
    include:
    - master
    - release/*
    - develop
    # - feature/*
  paths:
    exclude:
    - README.md
    - LICENSE
    - Rambafile

schedules:
- cron: "0 15 * * *"
  displayName: Daily midnight build
  branches:
    include:
    # - master
    # - release/*
    - develop
    - feature/*
  always: false

pool:
  vmImage: 'macos-latest'

steps:
# 環境変数のエクスポート
- script: |
    export DEVELOPER_DIR=/Applications/Xcode_11.3.1.app/Contents/Developer
    export MINT_PATH=mint/lib
    export MINT_LINK_PATH=mint/bin
  displayName: Export environment variables

# Xcodeの一覧出力
- script: ls /Applications | grep 'Xcode'
  displayName: Show Xcode list

# Xcodeのバージョン出力
- script: xcodebuild -version
  displayName: Show Xcode version

# Bundlerで管理しているライブラリのキャッシュ復元
# - task: Cache@2
#   inputs:
#     key: 'gems | "$(Agent.OS)" | Gemfile.lock'
#     restoreKeys: | 
#        gems | "$(Agent.OS)"
#        gems
#     path: $(Pipeline.Workspace)/vendor/bundle
#   displayName: Cache Gems

# Bundlerで管理しているライブラリのインストール
- script: make install-bundler
  displayName: Bundle install

# Mintのインストール
- script: brew install mint
  displayName: Install Mint

# Mintで管理しているライブラリのキャッシュ復元
# - task: Cache@2
#   inputs:
#     key: 'mint | "$(Agent.OS)" | Mintfile'
#     restoreKeys: | 
#        mint | "$(Agent.OS)"
#        mint
#     path: $(Pipeline.Workspace)/mint
#   displayName: Cache Mint packages

# Carthageで管理しているライブラリのキャッシュ復元
# - task: Cache@2
#   inputs:
#     key: 'carthage | "$(Agent.OS)" | Cartfile.resolved'
#     restoreKeys: | 
#        carthage | "$(Agent.OS)"
#        carthage
#     path: $(Pipeline.Workspace)/Carthage
#   displayName: Cache Carthage packages

# Carthageで管理しているライブラリのインストール
- script: make install-carthage
  displayName: Install Carthage frameworks

# CocoaPodsで管理しているライブラリのキャッシュ復元
# - task: Cache@2
#   inputs:
#     key: 'pods | "$(Agent.OS)" | Podfile.lock'
#     restoreKeys: | 
#        pods | "$(Agent.OS)"
#        pods
#     path: $(Pipeline.Workspace)/Pods
#   displayName: Cache Pods

# ライセンス情報の生成、プロジェクトファイルの生成、CocoaPodsで管理しているライブラリのインストール
- script: make generate-licenses
  displayName: Generate licenses, Xcode project, And Pod install

# ビルド
- script: make build-debug
  displayName: Xcode build for debug

# 単体テストの実行
- script: make test
  displayName: Xcode test

シンプルなYAMLファイルなので、慣れれば読みやすいと思います。

キャッシュ

cache タスクを使うと、ライブラリなどのキャッシュを取得できるのですが、エラーとなりうまく使えませんでした。
そのため、今回はキャッシュを使っていません。
コメントアウトしていますが、そもそも記述が間違っている可能性があります。

おわりに

Azure PipelinesのYAMLで基本的なiOSアプリのCIを回すことができました!

キャッシュが取れていなかったり、静的解析していなかったりと改善点もあるので、知っている方がいたら教えていただけると嬉しいです。

参考リンク

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

【Sign in with Apple】「xxxxx@privaterelay.appleid.com」へメールを送った際バウンスする

はじめに

Sign In with Apple(Appleログインと後述)が2019年に発表され、2020年4月には必須化になるなか導入の際にハマったインフラ系のことについてメモ書きしておきます。
私はiOSエンジニアではないですがPMやっていたときに遭遇しました。
ユーザ登録時にAppleログインで会員登録する際にメールアドレスをサービス提供側に公開するか非公開にするか選択があります。
非公開を選択した場合、サービス側が取得できるメールアドレスは「xxxx@privaterelay.appleid.com」になります。
本家サイトには「xxxx@privaterelay.appleid.com」に返信すれば登録ユーザに届くというようなことを書いているけどもGmailで送信した場合に下記の様にバウンスしてきました。これの解決方法です。

apple_bounse.png

本記事で力になれないこと

これ以上読んで頂いても目的のものが含まれていない可能性を提示します。

  • Gmailから発信されたメールの転送
  • Gsuiteからの発信されたメールの転送

本記事はDNS周りの設定がメインとなります。
Gsuiteからの送信についてはもしかしたらやりようがあるかもしれませんが、Apple側の設定ができず。。。Gmailがフリーメールであるため作成可能なため受信は難しいかもしれません。

解決策

SPF設定をしましょう(ドン!)

ここに書いてありあました。
https://developer.apple.com/documentation/signinwithapplejs/communicating_using_the_private_email_relay_service

メールドメインを登録する
プライベートメールアドレスを持つユーザーにメールを送信するには、アウトバウンドメールまたはメールドメインを登録し、Sender Policy Framework(SPF)を使用してアウトバウンドメールを認証する必要があります。これらのメカニズムにより、このチャネルを介してユーザーとのみ通信できるようになり、スパムの通過を防ぎます。

by Google翻訳

ネットワークで、認証系、、、ちょっとだけアレルギーがでました

SPFとは

細かい説明は下記サイトを見てください。
https://salt.iajapan.org/wpmu/anti_spam/admin/tech/explanation/spf/

おさえておきたい事柄として、

受信したメールのドメインが信頼されているサーバから発信されたものなのかを確認する機構です。

kamen_warui_businessman.png

メールの送信者ってプログラムで指定(もとい偽装)できますよね。下のスクショでいう送信元の話です。
SPFはそれがあっても安全さを担保する仕組みと思ってください。
スクリーンショット 2020-03-18 15.20.43.png

送信者側設定

DNSの設定になります。
やることは対象ドメインに下記のTXTレコードを追加します。これで終了。
「このドメインはここから送信されたメールに対しては保証します!」って言うことなんですね
※自分自身は設定できません
"v=spf1 include:[許可するドメイン名] ~all"

設定の方法にはいろいろあるみたいなので必要に応じてアレンジできますね。
紹介されているのはIPアドレス、ドメイン名、メールサーバ、あわせ技ななどなど。
https://www.naritai.jp/guidance_spf_example.html

チェックツールもいくつかありました。
下記サイトで紹介されているみたいなのでどうしてもうまく行かないときとかは使ってみてもいいかもしれません。
自ドメインの設定が正しいかや、メールの一斉配信ツールなどでメールサーバが外部にあるときとかはspfの設定があるかを確認することもできます。
https://asumeru.net/spf-record-check

余談ではありますが今回nslookupのオプションでDNSのレコードを漁るということを覚えました。
これで、対象のドメインのDNS設定を観ることができます
nslookup -type=[レコード名] [ドメイン名]

例えば、
nslookup -type=mx [ドメイン名]
で、対象ドメインのメールサーバがわかります。

Apple Developer側設定

下記の記事を参考にしてみてください。
やることとしては、受信可能にするドメインかメールアドレスを登録することだけです。
送信元とFromが一緒であればここの設定だけで大丈夫だと思います(多分) 
https://notes.tret.jp/sign-in-with-apple-register/

終わりに

Appleログインを実装する上では一定数ハマるひとがいなくなればといいなとおもいます。
セキュリティ周りをしっかりするのはいいけどもなかなか初見殺しで面倒に感じました。

あと、黒い画面でネットワークのコマンド打って、ちょっとかっこいいと感じてしまったのは秘密です。

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

WKWebViewへの移行が完了してもアプリにはUIWebViewが潜んでいるかもしれない

はじめ

先日、アプリで使用しているWebViewを非推奨APIとなっているUIWebViewからWKWebViewへ移行しました。
これでアプリ内からUIWebViewはいなくなりました。
と思っていたのですが、それでもAppleから「UIWebViewを使わないでね」という連絡がやってきてしまいました。

Dear Developer,
We identified one or more issues with a recent delivery for your app, <アプリ名>. Your delivery was successful, but you may wish to correct the following issues in your next delivery:
ITMS-90809: Deprecated API Usage - Apple will stop accepting submissions of apps that use UIWebView APIs starting from December 2020 . See https://developer.apple.com/documentation/uikit/uiwebview for more information.
After you've corrected the issues, you can upload a new binary to App Store Connect.
Best regards,
The App Store Team

メールを持っている人のイラスト(男性)

プロジェクト内を「UIWebView」で検索したところPods内のAFNetworking/UIKitがヒットしました。
原因はAFNetworking/UIKitに含まれているUIWebViewの拡張機能でした。
これからプロジェクトを修正してUIWebViewを根絶やしにしていきたいと思います。
粗探しのイラスト

修正方針

AFNetworkingのissueを参考に修正します。
issueでは以下の修正方法などが紹介されています。

  • AFNetworking/UIKit内のUIWebViewに関するファイルを削除する方法
  • 不要な依存関係(AFNetworking/UIKit)を削除する方法

前者はチーム開発の場合に各々がファイルを削除することになり、ミスに繋がると思いました。
また、Pods内を編集すること自体にも抵抗があります。
今回は後者の方法で修正することにしました。

修学旅行のイラスト「地図を見て道を探す子供達」

修正

まずは、Podfileの該当行を修正します。
'UIKit'以外の必要なsubspecsを指定します。

Podfile
# pod 'AFNetworking'
# ↓
pod 'AFNetworking', :subspecs => ['Reachability', 'Security', 'Serialization', 'NSURLSession']

そして、インポートするファイルが変わるためアプリ側のソースも合わせて修正します。

// #import "AFNetworking.h"
// ↓
#import <AFNetworking/AFNetworking-umbrella.h>

最後に、pod installすれば問題なく動くはずです。

機械のメンテナンスをする人のイラスト

まとめ

アプリでUIWebViewを使用していないのにもかかわらず、Appleから連絡が来てしまう場合はライブラリが原因かもしれません。
ライブラリのissueを確認するなどして対応するといいと思います。
今後、UIWebViewが含まれているアプリはリジェクトされるようになります(新規アプリは2020年4月、アップデートは2020年12月から)。
本記事がアプリ開発の助けになれば幸いです。

スマートフォンのアプリのイラスト

参考

審査に出したiOSアプリが ITMS-90809 としてAppleから問題を指摘される話

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

【Swift】Sing in with AppleのボタンをStoryboardで設定する

Sing in with Appleでは
ASAuthorizationAppleIDButtonを使用する必要がありますが
このクラスはStoryboardでサポートされておらず
コード上で追加する必要があります。

そこで
今回はCustom Classを作成することで
StoryboardでもSign in with Appleのボタンを設定する方法について
整理してみました。

コードでの追加方法は以前↓に書かせていただきました。
https://qiita.com/shiz/items/5e094910f742c2ad72a4

実装方法

UIButtonクラスのサブクラスを定義する

UIButtonクラスのサブクラスを定義します
(名前は任意です)

class SignInWithAppleIDButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
}

ASAuthorizationAppleIDButtonをプロパティに保持する

実際にSign in with Appleを実行するための
ASAuthorizationAppleIDButtonをプロパティに保持します。

class SignInWithAppleIDButton: UIButton {

    private var appleIDButton: ASAuthorizationAppleIDButton!

    ...
}

暗黙的にOptionalをUnwrapしていますが
次に定義するdraw(_:)メソッドで必ず初期化をするため
問題はありません。

draw(_:)メソッドをoverrideする

draw(_:)メソッドの中で
ボタンを描画します。

class SignInWithAppleIDButton: UIButton {
    super.draw(rect)

    appleIDButton = ASAuthorizationAppleIDButton(authorizationButtonType: .default, authorizationButtonStyle: .black)

    addSubview(appleIDButton)

    appleIDButton.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        appleIDButton.topAnchor.constraint(equalTo: self.topAnchor),
        appleIDButton.leadingAnchor.constraint(equalTo: self.leadingAnchor),
        appleIDButton.bottomAnchor.constraint(equalTo: self.bottomAnchor),
        appleIDButton.trailingAnchor.constraint(equalTo: self.trailingAnchor),
    ])
}

ここでは
authorizationButtonTypeと
authorizationButtonStyleに
固定値を設定していますが
後ほどカスタム可能にします。

Storyboardで設定可能にする

次にStoryboardで表示できるように
@IBDesignableをクラスに設定します。

@IBDesignable
class SignInWithAppleIDButton: UIButton {
    super.draw(rect)

    appleIDButton = ASAuthorizationAppleIDButton(authorizationButtonType: .default, authorizationButtonStyle: .black)

    addSubview(appleIDButton)

    appleIDButton.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        appleIDButton.topAnchor.constraint(equalTo: self.topAnchor),
        appleIDButton.leadingAnchor.constraint(equalTo: self.leadingAnchor),
        appleIDButton.bottomAnchor.constraint(equalTo: self.bottomAnchor),
        appleIDButton.trailingAnchor.constraint(equalTo: self.trailingAnchor),
    ])
}

ここまででStoryboardにUIButtonを設置してCustom Classに
SignInWithAppleIDButtonを指定すると
下記のようにStoryboard上で表示させることができます。

スクリーンショット 2020-03-17 5.05.11.png

※ 制約などについてはAppleのHuman Interface Guidelinesに沿っています。
こちらについては後ほど記載します。

ボタンをカスタマイズする

ここからはさらに
@IBInspectableを使って
Storyboard上でボタンをカスタマイズ可能にしたいと思います。

ASAuthorizationAppleIDButtonの定義を見てみると

ASAuthorizationAppleIDButton.ButtonType
ASAuthorizationAppleIDButton.Style
cornerRadius

の設定が可能です。

extension ASAuthorizationAppleIDButton {


    @available(iOS 13.0, *)
    public enum ButtonType : Int {


        case signIn = 0

        case `continue` = 1


        @available(iOS 13.2, *)
        case signUp = 2


        public static var `default`: ASAuthorizationAppleIDButton.ButtonType { get }
    }


    @available(iOS 13.0, *)
    public enum Style : Int {


        case white = 0

        case whiteOutline = 1

        case black = 2
    }
}

@available(iOS 13.0, *)
open class ASAuthorizationAppleIDButton : UIControl {


    public convenience init(type: ASAuthorizationAppleIDButton.ButtonType, style: ASAuthorizationAppleIDButton.Style)


    public init(authorizationButtonType type: ASAuthorizationAppleIDButton.ButtonType, authorizationButtonStyle style: ASAuthorizationAppleIDButton.Style)


    /** @abstract Set a custom corner radius to be used by this button.
     */
    open var cornerRadius: CGFloat
}

そこでこの3つのプロパティをカスタマイズできるようにします。

@IBDesignable
class SignInWithAppleIDButton: UIButton {

    ...

    @IBInspectable
    var cornerRadius: CGFloat = 6.0

    @IBInspectable
    var type: Int = ASAuthorizationAppleIDButton.ButtonType.default.rawValue

    @IBInspectable
    var style: Int = ASAuthorizationAppleIDButton.Style.black.rawValue

    ...

    override func draw(_ rect: CGRect) {
        super.draw(rect)

        let type = ASAuthorizationAppleIDButton.ButtonType.init(rawValue: self.type) ?? .default
        let style = ASAuthorizationAppleIDButton.Style.init(rawValue: self.style) ?? .black
        appleIDButton = ASAuthorizationAppleIDButton(authorizationButtonType: type, authorizationButtonStyle: style)
        appleIDButton.cornerRadius = cornerRadius

        addSubview(appleIDButton)

        appleIDButton.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            appleIDButton.topAnchor.constraint(equalTo: self.topAnchor),
            appleIDButton.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            appleIDButton.bottomAnchor.constraint(equalTo: self.bottomAnchor),
            appleIDButton.trailingAnchor.constraint(equalTo: self.trailingAnchor),
        ])
    }
}

こうするとStoryboard上で設定が変更可能になります。

スクリーンショット 2020-03-17 5.20.44.png

IBInspectableではenumが指定できないため
RawValueIntを設定し
enumのcaseに存在しない値に関してはデフォルトの値を設定しています。

cornerRadiusの値はAppleがデフォルトで提供している値のようです。

ボタンをタップ可能にする

ここまででデザインはできましたが
ボタンをタップしても動作しません。

そこでボタンがタップされたことを
プロパティで保持しているASAuthorizationAppleIDButton
伝えるようにします。

@IBDesignable
class SignInWithAppleIDButton: UIButton {

    private var appleIDButton: ASAuthorizationAppleIDButton!

    ...

    override func draw(_ rect: CGRect) {
        super.draw(rect)

        ...

        appleIDButton.addTarget(self, action: #selector(appleIDButtonTapped), for: .touchUpInside)

        ...
    }

    @objc
    func appleIDButtonTapped(_ sender: Any) {
        sendActions(for: .touchUpInside)
    }
}

sendActions(for:)はUIControlのメソッドで
関連しているUIControl(今回はUIButtonのsubViewのASAuthorizationAppleIDButton)へ
イベントを伝播させることができます。

https://developer.apple.com/documentation/uikit/uicontrol/1618211-sendactions

Human Interface Guidelines

上記のようにStoryboardで設定はできるようになるものの
ASAuthorizationAppleIDButtonはデザインに関して
多くのガイドラインが存在します。

そこでそれらが記載されている
Human Interface Guidelines
を見ていきたいと思います。
https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

全てに共通のルール

  • 目立つように表示すること
  • 他のサインインボタン(Facebook, Googleなど)よりも小さくしないこと
  • スクロールしなくてもボタンは見えるようにすること

スクロールしなくてもボタンは見えるようにすることに関しては
iPhoneSEなどの小さい端末での表示に注意する必要がありそうですね。

システムが提供するボタンを使用する場合

3つのスタイルの注意点

White

全プラットフォーム※で利用可能
※iOS, macOS, iPadOS, watchOS

  • 暗い背景や十分にコントラストが与えられる色のついた背景で使用すること

image.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

White with Outline

iOS, macOS, webで利用可能

  • 白い背景やWhiteで使うと十分にコントラストが与えられない背景色で使用すること
  • 黒のアウトラインが見辛いので暗かったり飽和した背景とは一緒に使わないこと

image.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/


Black

全プラットフォームで利用可能

  • 白や明るい背景と一緒に使用すること

image.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

※ WatchOSの場合、純粋な黒の背景に対応するために
グレーのボタンが用意されている

image.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

Button Size and Corner Radius

アプリの他のボタンに合わせて調整が可能ですが
その中でも下記のようなガイドラインがあります。

image.png
https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

  • iOS, macOS, webではボタンの最小サイズと最小マージンを維持すること

ロケールでボタンタイトルの長さが変わるので注意が必要

スクリーンショット 2020-03-18 5.04.33.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

カスタムボタンを作成する場合

iOS, macOS, Webで利用可能

Apple Design Resourcesからロゴのダウンロードが可能
https://developer.apple.com/design/resources/

ロゴを使用する場合のガイドライン

  • ロゴ自体をボタンとして使用しないこと
  • ロゴファイルの高さをボタンの高さに合わせること
  • 切り取らないこと
  • 垂直方向(vertical)のpaddingを追加しないこと
  • 独自のカラーを使用しないこと

左寄せロゴボタンを作成する場合

ボタンの高さを基にロゴファイルのフォーマットを選択する

SVGとPDFはベクター形式のなのでどの高さでも利用できるが、PNGは44ptの高さのみで使用すること
44ptはiOSでのデフォルトの高さで推奨される高さ

タイトル※にはシステムのフォントを利用すること

※ Sign in with Apple, Sign up with Apple, or Continue with Apple

タイトルのフォントサイズはボタンの高さの43%

逆を言えばボタンの高さはフォントサイズの233%

サイズの例
image.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

タイトルの大文字、小文字を変えない

  • 最初の一文字を大文字にすること 

具体的にはSign, Continue, Appleは最初が大文字
それ以外は全て小文字にする

タイトルとボタンの右端の間のマージンをボタンの幅を最低8%以上にする

ボタンのサイズと周りのマージンを最小以上にする

システムで提供されているものと同様のガイドラインです。

スクリーンショット 2020-03-18 5.04.33.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

ロゴのみのボタンを作成する場合

ボタンのサイズを基にロゴファイルのフォーマットを選択する

  • ボタンの高さを基にロゴファイルのフォーマットを選択する
  • SVGとPDFはベクター形式のなのでどの高さでも利用できるが、PNGは44ptx44ptでのサイズでのみ使用すること

右側に平行方向のpaddingを入れない

  • ロゴのみの画像はすでに適切なpaddingが全てのサイドに入っているので変更はしないこと
  • アスペクト比は1:1を維持すること

デフォルトの四角い形を変更したい場合はMaskを利用すること

  • 元の画像を切り取ったりPaddingを追加してはいけない

ボタンの周りのマージンは最低ボタンの高さの1/10以上にする

まとめ

Sign in with Appleで使用するボタンについて
Storyboardでの設定方法について見てみました。

画面に関してStoryboardを使用している場合には
他のパーツと一緒にデザインできるので
役に立つのかと思っています。

一方でしっかりと使用する際のガイドラインも存在し
そこを守らないとアプリの審査でリジェクトされる可能性もあるため
しっかりとチェックする必要がありますね。

もし何か間違いなどございましたらご指摘ください??‍♂️

参照記事

https://developer.apple.com/documentation/authenticationservices/implementing_user_authentication_with_sign_in_with_apple
https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/
https://developer.apple.com/library/archive/referencelibrary/GettingStarted/DevelopiOSAppsSwift/ImplementingACustomControl.html
https://medium.com/swlh/how-to-use-asauthorizationappleidbutton-in-storyboard-653f9cd94817

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

【Swift】Sign in with AppleのボタンをStoryboardで設定する

Sign in with Appleでは
ASAuthorizationAppleIDButtonを使用する必要がありますが
このクラスはStoryboardでサポートされておらず
コード上で追加する必要があります。

そこで
今回はCustom Classを作成することで
StoryboardでもSign in with Appleのボタンを設定する方法について
整理してみました。

コードでの追加方法は以前↓に書かせていただきました。
https://qiita.com/shiz/items/5e094910f742c2ad72a4

実装方法

UIButtonクラスのサブクラスを定義する

UIButtonクラスのサブクラスを定義します
(名前は任意です)

class SignInWithAppleIDButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
}

ASAuthorizationAppleIDButtonをプロパティに保持する

実際にSign in with Appleを実行するための
ASAuthorizationAppleIDButtonをプロパティに保持します。

class SignInWithAppleIDButton: UIButton {

    private var appleIDButton: ASAuthorizationAppleIDButton!

    ...
}

暗黙的にOptionalをUnwrapしていますが
次に定義するdraw(_:)メソッドで必ず初期化をするため
問題はありません。

draw(_:)メソッドをoverrideする

draw(_:)メソッドの中で
ボタンを描画します。

class SignInWithAppleIDButton: UIButton {
    super.draw(rect)

    appleIDButton = ASAuthorizationAppleIDButton(authorizationButtonType: .default, authorizationButtonStyle: .black)

    addSubview(appleIDButton)

    appleIDButton.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        appleIDButton.topAnchor.constraint(equalTo: self.topAnchor),
        appleIDButton.leadingAnchor.constraint(equalTo: self.leadingAnchor),
        appleIDButton.bottomAnchor.constraint(equalTo: self.bottomAnchor),
        appleIDButton.trailingAnchor.constraint(equalTo: self.trailingAnchor),
    ])
}

ここでは
authorizationButtonTypeと
authorizationButtonStyleに
固定値を設定していますが
後ほどカスタム可能にします。

Storyboardで設定可能にする

次にStoryboardで表示できるように
@IBDesignableをクラスに設定します。

@IBDesignable
class SignInWithAppleIDButton: UIButton {
    super.draw(rect)

    appleIDButton = ASAuthorizationAppleIDButton(authorizationButtonType: .default, authorizationButtonStyle: .black)

    addSubview(appleIDButton)

    appleIDButton.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        appleIDButton.topAnchor.constraint(equalTo: self.topAnchor),
        appleIDButton.leadingAnchor.constraint(equalTo: self.leadingAnchor),
        appleIDButton.bottomAnchor.constraint(equalTo: self.bottomAnchor),
        appleIDButton.trailingAnchor.constraint(equalTo: self.trailingAnchor),
    ])
}

ここまででStoryboardにUIButtonを設置してCustom Classに
SignInWithAppleIDButtonを指定すると
下記のようにStoryboard上で表示させることができます。

スクリーンショット 2020-03-17 5.05.11.png

※ 制約などについてはAppleのHuman Interface Guidelinesに沿っています。
こちらについては後ほど記載します。

ボタンをカスタマイズする

ここからはさらに
@IBInspectableを使って
Storyboard上でボタンをカスタマイズ可能にしたいと思います。

ASAuthorizationAppleIDButtonの定義を見てみると

ASAuthorizationAppleIDButton.ButtonType
ASAuthorizationAppleIDButton.Style
cornerRadius

の設定が可能です。

extension ASAuthorizationAppleIDButton {


    @available(iOS 13.0, *)
    public enum ButtonType : Int {


        case signIn = 0

        case `continue` = 1


        @available(iOS 13.2, *)
        case signUp = 2


        public static var `default`: ASAuthorizationAppleIDButton.ButtonType { get }
    }


    @available(iOS 13.0, *)
    public enum Style : Int {


        case white = 0

        case whiteOutline = 1

        case black = 2
    }
}

@available(iOS 13.0, *)
open class ASAuthorizationAppleIDButton : UIControl {


    public convenience init(type: ASAuthorizationAppleIDButton.ButtonType, style: ASAuthorizationAppleIDButton.Style)


    public init(authorizationButtonType type: ASAuthorizationAppleIDButton.ButtonType, authorizationButtonStyle style: ASAuthorizationAppleIDButton.Style)


    /** @abstract Set a custom corner radius to be used by this button.
     */
    open var cornerRadius: CGFloat
}

そこでこの3つのプロパティをカスタマイズできるようにします。

@IBDesignable
class SignInWithAppleIDButton: UIButton {

    ...

    @IBInspectable
    var cornerRadius: CGFloat = 6.0

    @IBInspectable
    var type: Int = ASAuthorizationAppleIDButton.ButtonType.default.rawValue

    @IBInspectable
    var style: Int = ASAuthorizationAppleIDButton.Style.black.rawValue

    ...

    override func draw(_ rect: CGRect) {
        super.draw(rect)

        let type = ASAuthorizationAppleIDButton.ButtonType.init(rawValue: self.type) ?? .default
        let style = ASAuthorizationAppleIDButton.Style.init(rawValue: self.style) ?? .black
        appleIDButton = ASAuthorizationAppleIDButton(authorizationButtonType: type, authorizationButtonStyle: style)
        appleIDButton.cornerRadius = cornerRadius

        addSubview(appleIDButton)

        appleIDButton.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            appleIDButton.topAnchor.constraint(equalTo: self.topAnchor),
            appleIDButton.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            appleIDButton.bottomAnchor.constraint(equalTo: self.bottomAnchor),
            appleIDButton.trailingAnchor.constraint(equalTo: self.trailingAnchor),
        ])
    }
}

こうするとStoryboard上で設定が変更可能になります。

スクリーンショット 2020-03-17 5.20.44.png

IBInspectableではenumが指定できないため
RawValueIntを設定し
enumのcaseに存在しない値に関してはデフォルトの値を設定しています。

cornerRadiusの値はAppleがデフォルトで提供している値のようです。

ボタンをタップ可能にする

ここまででデザインはできましたが
ボタンをタップしても動作しません。

そこでボタンがタップされたことを
プロパティで保持しているASAuthorizationAppleIDButton
伝えるようにします。

@IBDesignable
class SignInWithAppleIDButton: UIButton {

    private var appleIDButton: ASAuthorizationAppleIDButton!

    ...

    override func draw(_ rect: CGRect) {
        super.draw(rect)

        ...

        appleIDButton.addTarget(self, action: #selector(appleIDButtonTapped), for: .touchUpInside)

        ...
    }

    @objc
    func appleIDButtonTapped(_ sender: Any) {
        sendActions(for: .touchUpInside)
    }
}

sendActions(for:)はUIControlのメソッドで
関連しているUIControl(今回はUIButtonのsubViewのASAuthorizationAppleIDButton)へ
イベントを伝播させることができます。

https://developer.apple.com/documentation/uikit/uicontrol/1618211-sendactions

Human Interface Guidelines

上記のようにStoryboardで設定はできるようになるものの
ASAuthorizationAppleIDButtonはデザインに関して
多くのガイドラインが存在します。

そこでそれらが記載されている
Human Interface Guidelines
を見ていきたいと思います。
https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

全てに共通のルール

  • 目立つように表示すること
  • 他のサインインボタン(Facebook, Googleなど)よりも小さくしないこと
  • スクロールしなくてもボタンは見えるようにすること

スクロールしなくてもボタンは見えるようにすることに関しては
iPhoneSEなどの小さい端末での表示に注意する必要がありそうですね。

システムが提供するボタンを使用する場合

3つのスタイルの注意点

White

全プラットフォーム※で利用可能
※iOS, macOS, iPadOS, watchOS

  • 暗い背景や十分にコントラストが与えられる色のついた背景で使用すること

image.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

White with Outline

iOS, macOS, webで利用可能

  • 白い背景やWhiteで使うと十分にコントラストが与えられない背景色で使用すること
  • 黒のアウトラインが見辛いので暗かったり飽和した背景とは一緒に使わないこと

image.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/


Black

全プラットフォームで利用可能

  • 白や明るい背景と一緒に使用すること

image.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

※ WatchOSの場合、純粋な黒の背景に対応するために
グレーのボタンが用意されている

image.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

Button Size and Corner Radius

アプリの他のボタンに合わせて調整が可能ですが
その中でも下記のようなガイドラインがあります。

image.png
https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

  • iOS, macOS, webではボタンの最小サイズと最小マージンを維持すること

ロケールでボタンタイトルの長さが変わるので注意が必要

スクリーンショット 2020-03-18 5.04.33.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

カスタムボタンを作成する場合

iOS, macOS, Webで利用可能

Apple Design Resourcesからロゴのダウンロードが可能
https://developer.apple.com/design/resources/

ロゴを使用する場合のガイドライン

  • ロゴ自体をボタンとして使用しないこと
  • ロゴファイルの高さをボタンの高さに合わせること
  • 切り取らないこと
  • 垂直方向(vertical)のpaddingを追加しないこと
  • 独自のカラーを使用しないこと

左寄せロゴボタンを作成する場合

ボタンの高さを基にロゴファイルのフォーマットを選択する

SVGとPDFはベクター形式のなのでどの高さでも利用できるが、PNGは44ptの高さのみで使用すること
44ptはiOSでのデフォルトの高さで推奨される高さ

タイトル※にはシステムのフォントを利用すること

※ Sign in with Apple, Sign up with Apple, or Continue with Apple

タイトルのフォントサイズはボタンの高さの43%

逆を言えばボタンの高さはフォントサイズの233%

サイズの例
image.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

タイトルの大文字、小文字を変えない

  • 最初の一文字を大文字にすること 

具体的にはSign, Continue, Appleは最初が大文字
それ以外は全て小文字にする

タイトルとボタンの右端の間のマージンをボタンの幅を最低8%以上にする

ボタンのサイズと周りのマージンを最小以上にする

システムで提供されているものと同様のガイドラインです。

スクリーンショット 2020-03-18 5.04.33.png
参照元:https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

ロゴのみのボタンを作成する場合

ボタンのサイズを基にロゴファイルのフォーマットを選択する

  • ボタンの高さを基にロゴファイルのフォーマットを選択する
  • SVGとPDFはベクター形式のなのでどの高さでも利用できるが、PNGは44ptx44ptでのサイズでのみ使用すること

右側に平行方向のpaddingを入れない

  • ロゴのみの画像はすでに適切なpaddingが全てのサイドに入っているので変更はしないこと
  • アスペクト比は1:1を維持すること

デフォルトの四角い形を変更したい場合はMaskを利用すること

  • 元の画像を切り取ったりPaddingを追加してはいけない

ボタンの周りのマージンは最低ボタンの高さの1/10以上にする

まとめ

Sign in with Appleで使用するボタンについて
Storyboardでの設定方法について見てみました。

画面に関してStoryboardを使用している場合には
他のパーツと一緒にデザインできるので
役に立つのかと思っています。

一方でしっかりと使用する際のガイドラインも存在し
そこを守らないとアプリの審査でリジェクトされる可能性もあるため
しっかりとチェックする必要がありますね。

もし何か間違いなどございましたらご指摘ください??‍♂️

参照記事

https://developer.apple.com/documentation/authenticationservices/implementing_user_authentication_with_sign_in_with_apple
https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/
https://developer.apple.com/library/archive/referencelibrary/GettingStarted/DevelopiOSAppsSwift/ImplementingACustomControl.html
https://medium.com/swlh/how-to-use-asauthorizationappleidbutton-in-storyboard-653f9cd94817

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

Swift で map (closure) のこんな書き方

Swift の map などで使用する closure には書き方が色々ありますね。
下記のような書き方は知っていました。

let numbers = [0, 1, 2, 3]

// 下記すべて[String]型 ["0", "1", "2", "3"]
numbers.map({ (number: Int) -> String in return String(number) })
numbers.map({ (number: Int) -> String in String(number) })  // 暗黙的な戻り値
numbers.map({ number -> String in String(number) })         // 引数の型省略
numbers.map({ number in String(number) })                   // 戻り値の型推論
numbers.map({ String($0) })                                 // 引数名の省略
numbers.map { String($0) }                                  // 括弧の省略

こんな書き方を発見しました。勉強不足でした。

numbers.map(String.init)

結果は上記の例と同じですが、
単純に numbers.map{ String($0) } と全く一緒というわけではなさそうです。
http://swiftlife.hatenablog.jp/entry/2015/12/27/213058 こちらで解説されていましたのでご参照ください。

現に、Playgrounds で試すと結果の表示のされ方が異なります。右半分の表示です。

実験

イニシャライザ以外にも使えるのか

いけた。

// いけた
func f(_ number: Int) -> String {
    return String(number)
}
numbers.map(f)

// 引数ラベルをアンダースコアで省略できるようにしとかなくてもいい
func ff(number: Int) -> String {
    return String(number)
}
numbers.map(ff)

// クラス関数とかでも問題なく使用可
class C {
    class func f(_ number: Int) -> String {
        return String(number)
    }
}
numbers.map(C.f)

関数の引数の数

closure の引数の数と、関数の引数の数があってないと エラー です。
引数2つ目以降無視されるとかはないです。

// エラー!!
func f(a: Int, b: Int) -> String {
    return String(a)
}
numbers.map(f)

// これは OK (sorted のクロージャの引数は2つのため)
func ff(a: Int, b: Int) -> Bool {
    return true
}
numbers.sorted(by: ff)

参考

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