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

iOS 14 Safariではホスト名にアンダーバー(_)が含まれるWebページへのHTTPSアクセスが失敗する【一部未検証箇所あり】

TL;DR

ホスト名にアンダーバー _ は使わないこと。
下記の条件下で問題が起きる。

  • HTTPSページにSafariでアクセスする
  • iOS 14
  • ホスト名にアンダーバーが含まれる

下記は未検証

  • 証明書がワイルドカード証明書のときに起こったが、それ以外の証明書でも起こるのか?

発見の経緯

起こった不具合

(以下、後で書く)

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

iOS 14 Safariではホスト名にアンダースコア(_)が含まれるWebページへのHTTPSアクセスが失敗する【一部未検証箇所あり】

TL;DR

ホスト名にアンダースコア _ は使わないこと。
下記の条件下で問題が起きる。

  • HTTPSページにSafariでアクセスする
  • iOS 14
  • ホスト名にアンダースコアが含まれる

下記は未検証

  • 証明書がワイルドカード証明書のときに起こったが、それ以外の証明書でも起こるのか?

DO NOT use any underscore _ in HTTPS hostname, or you will encounter a SSL certificate error while accessing HTTPS web page in Safari on iOS 14 or later version.

発見の経緯

起こった不具合

iOS 14にアップデートしたところ弊社(onecareer.jp)サイト内の某LPのコンテンツが表示されなくなった。

対象ページについて

Headless CMSを使ってコンテンツ入稿し、ajaxでCMSからコンテンツ取得してjQueryでページにコンテンツを埋め込んでいた。

トラブルシュート

初動

iOS 14のSafariでしか起こっていないので、iPhoneをMacにつないでSafariの開発者モードを起動。
失敗しているHTTPリクエストを調査。
SSLエラーによりコンテンツ取得していないことが判明。

調査

iOS 14に関する証明書絡みのネタを洗った。
2020年9月1日以降に発行した証明書は1年ちょっと以内の有効期限じゃないといけなくなる変更が発表されているが、今回の証明書はそのNGパターンに該当しない。
けれども、証明書検証ロジックに変更があったのは確かっぽい。

Headless CMSのサイト側も怪しいので、ajaxから叩くエンドポイント以外でも再現するページがないか、iOS Safariでアクセスしてみる。
一部の管理側ページも同様のエラーが起こることを確認。
エラーが発生する条件を帰納的に調べたところ、アンダースコア _ がホスト名に含まれるサイトで同様の不具合が発生することが分かった。

考察

そもそも、RFC的にはホスト名にアンダースコアは含めることができない。
だが、今回利用していたHeadless CMSは、CMSのワークスペースを生成するときにアンダースコアを含むワークスペース名を指定することができた。
また、そのCMSはワークスペース名をそのままサブドメイン部に用いたサイトを生成し、その中でコンテンツ管理をする仕様になっていた。

これまでのバージョンのiOS Safariは、RFCの定義に反し暗黙のうちにアンダースコアを含むホスト名の証明書検証をパスしてきた模様。
一方、直近のiOS Safariでの証明書検証ロジックが変更され、アンダースコア周りの処理条件も暗黙のうちに厳格化されたと考えられる。

まとめ

Webの世界はたくさんRFC非準拠な実装が溢れているが、自分だけはRFCを守ろう。
それが自分の身と、自分の周りの大切な人を守ることに繋がる。

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

ARKitのトラッキング状態を保存、ロードして再開する

1、ワールドマップを保存

sceneView.session.getCurrentWorldMap { [self] worldMap, error in
    self.sceneView.session.pause()
    self.map = worldMap
}

75ae3206-121a-47ed-9464-af0f322443cc.png

2、保存したワールドマップ構成でセッション再開

let configuration = ARWorldTrackingConfiguration()
configuration.initialWorldMap = map
sceneView.session.run(configuration, options: [])

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

お仕事のご依頼は
rockyshikoku@gmail.com
まで

Twitter
MLBoysチャンネル
Medium

相棒
note

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

FlutterのiOS14対応で個人的にやったことまとめ

2020年9月17日にiOS14が正式リリースされました。

仕事でFlutterを使っているのですが、その時に対応したことのまとめです。

Flutter対応

iOS14正式版にアップデートして既存の開発アプリの動作確認をしたところ(ビルドが通り)正常に動きました。
(元々ベータ版iOS14を使っていたので当然っちゃ当然ですが)

ただし、本当に大丈夫かなと思ってドキュメントを読んだところ、

Upgrading to Flutter 1.22 beta allows you to build, test, and deploy to iOS without issue. Upgrading to 1.20.4 stable allows you to build and deploy to iOS 14, but not debug.

https://flutter.dev/docs/development/ios-14

Flutetrのバージョンが 1.20.4 stable でビルドとデプロイはできるがデバッグはできない、 1.22 beta でビルドとデプロイ、デバッグができるとのことでした。

自分は諸事情でベータ版を使っており、Flutterのバージョン 1.22 になっていたので問題なく使えのでした。

% flutter --version                                                                                                                                               [19:47:38]
Flutter 1.22.0-1.0.pre • channel dev • https://github.com/flutter/flutter.git
Framework • revision ce40de69b7 (5 weeks ago) • 2020-08-20 07:31:50 -0700
Engine • revision 81027ab0cc
Tools • Dart 2.10.0 (build 2.10.0-45.0.dev)

※ Flutterのベータ版は stable と比べると不安定なので基本的に非推奨です。

どうしてもベータ版を使う必要がある場合は、 以前の記事 にも書いたようにchannelの切り替えが必要です。

Xcode対応

iOS14で動くようにビルドするにはXcodeのアップデートは不要でした。ただし、iOS14特有の機能を使うにはXcodeのアップデートが必要です。幸いにも、自分は「Xcodeガチャ」には遭遇しませんでした。

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

CGAffineTransformを使ったアフィン変換で気をつけること

iOS DC2020のトークにあった
CGAffineTransformはどう働いてるのか?〜Swiftエンジニアのための線形代数〜1
について、自分なりの理解も込めてこの場を借りてまとめてみました。

内容は、CGAffineTransformを使用する際に、
アフィン変換の説明に用いられる行列の形式とAppleのDocumentationの形式の違いによって、
CGAffineTransformの**ed APIを使用した際に、期待した動きにならない事象について説明されていました。

アフィン変換の説明に用いられる形式

元々の座標(x,y)に対して、前から行列を掛け合わせて変換後の座標を取得する

\begin{eqnarray}
\left(
\begin{array}{cccc}
x' \\
y' \\
1 \\
\end{array}
\right) = \left(
\begin{array}{cccc}
a & b & t_{ x }\\
c & d & t_{ y } \\
0 & 0 & 1 \\
\end{array}
\right)
\left(
\begin{array}{cccc}
x\\
y\\
1\\
\end{array}
\right)
\end{eqnarray}

x' = ax + by + t_{ x } \\
y' = cx + dy + t_{ y }

AppleのCGAffineTransformのDocumentationの形式

元々の座標(x,y)に対して、後から行列を掛け合わせて変換後の座標を取得する

\begin{eqnarray}
\left(
\begin{array}{cccc}
x' & y' & 1\\
\end{array}
\right) = \left(
\begin{array}{cccc}
x & y & 1\\
\end{array}
\right)
\left(
\begin{array}{cccc}
a & b & 0\\
c & d & 0\\
t_{ x } & t_{ y } & 1 \\
\end{array}
\right)

\end{eqnarray}

x' = ax + cy + t_{ x } \\
y' = bx + dy + t_{ y }

この違いによって、生まれた誤解について説明されていました。

今回アフィン変換を使って想定する変更

CGAffineTransformを使用して、次の順番でアフィン変換を行った場合を考えてみました。

1.xを0.5倍、yを1.5倍のスケールに変換する
2.軸を90度傾ける

期待.gif

想定する変更をCGAffineTransformの**ed APIを使用して実際に動かしてみる

UIView.animate(withDuration: 0.2) {
      self.view.transform = CGAffineTransform.identity.scaledBy(x: 0.5, y: 1.5)
                                                      .rotated(by: 90 * .pi / 180)
}
  • scaledBy(x:y:) を使用してxを0.5倍、yを1.5倍のスケールに変換
  • rotated(by:) を使用して軸を90度傾ける

という順番でアフィン変換をかけるように記述してみました。

実行結果
実際.gif

期待した状態と結果が異なるのは、CGAffineTransfoormでは、後ろから元の座標に対して演算するので、
rotateの変換、scaleの変換と処理を並べた場合は、
scaleの演算→rotateの演算の順とコードで記述した順番と逆で演算が行われる為、期待した状態と異なってしまいます。

紹介された concatenating(_:) を使用して動かしてみる

let scale = CGAffineTransform(scaleX: 0.5, y: 1.5)
let rotate = CGAffineTransform(rotationAngle: 90 * .pi / 180)
let transform = scale.concatenating(rotate)

UIView.animate(withDuration: 0.2) {
      self.transView.transform = transform
}

実行結果
別.gif

concatenating(_:)では期待した結果を得ることができました。
concatenating(_:)については、ドキュメントにて

Return Value
A new affine transformation matrix. That is, t’ = t1*t2.

と記載されています。2
なのでこの場合は rotateの変換scaleの演算の順番でアフィン変換が行われる為、期待した状態になります。

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

iOS: Youtubeをアプリで再生する

自作のアプリでYoutubeを再生させます。

ライブラリのインストール

  • アプリのプロジェクトフォルダで、CocoaPodの初期化
  • → Podfileが作成される
pod init
  • Podfileに、youtube-ios-player-helperを追加
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'YoutubePlayer' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for YoutubePlayer
  pod 'youtube-ios-player-helper', '~> 1.0.2' # 追加
end
  • CocoaPodインストールコマンド実行
pod install
  • Xcodeを「プロジェクト名.xcworkspace」ファイルをダブルクリックして起動する

ViewにYoutube Playerを追加

  • StoryBoardから、画面にView(UIView)を画面に設置
  • ViewのクラスをYTPlayerViewに変更
  • ViewControllerに、このYTPlayerViewを紐づける
ViewController
@IBOutlet weak var youtubeView: YTPlayerView!

Youtubeを再生させる

  • youtube_ios_player_helperをインポートする
  • YTPlayerViewDelegateを実装する
  • viewDidLoadでyoutubeViewの初期化
ViewController
import UIKit
import youtube_ios_player_helper

class ViewController: UIViewController, YTPlayerViewDelegate {

    @IBOutlet weak var youtubeView: YTPlayerView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        self.youtubeView.delegate = self
        self.youtubeView.load(withVideoId: "-Em3LZjMzZs")
    }

    func playerViewDidBecomeReady(_ playerView: YTPlayerView) {
        self.youtubeView.playVideo()
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reality Composerに任意の3Dオブジェクトをインポートする方法

Reality Composerに自分で作成した3Dモデルや、サイトからダウンロードした3Dモデルをインポートする作業が、結構大変だったので、やりかたを共有します。

なお、Reality Composerに3Dオブジェクトをインポートする一番簡単な方法は、AppleのReality Converter(β版)を使うか、Apple DevelopperサイトのDownload から、USDPythonをダウンロードして、そこに含まれているusdzconvertを使うことです。

ですが、今回変換したかったモデルは、Reality Converterやusdzconvertで変換しても何も表示されなかったため、別の方法を使いました。

環境
Mac (Catalina 10.15.5)
Blender 2.83.3
Xcode 11.7
Reality Composer 1.4

※各ソフトウェアのインストール方法は割愛します

TL:DR

結論から言うと、このやり方がうまくいきました。

任意の3Dオブジェクト => Blenderで加工をする & .daeに変換 => Xcodeで .usdzに変換 => Reality Composerにインポートする

やりかた

1. 3Dオブジェクトを入手する or 作成する

例えば、Unity Asset Storeでなにか3Dオブジェクトを入手してみます。

image.png

今回入手した3Dモデルの形式は、.FBXでした。

2. Blenderにインポートする

Blenderを起動して、xを押してデフォルトの立方体を削除しておきます。

image.png

ファイル > インポート > インポートしたいモデルの形式(今回の例では.FBX)を選択します。
image.png

ファイルの選択ダイアログがでます。
ここで、不要な情報はインポートしないようにします。
今回はアニメーションの情報はインポートしないようにしました。
image.png

モデルがインポートされました。
image.png

ここで、エクスポートといきたいところですが、今回のモデルはそのままエクスポートして、Xcodeで開いてもモデルが表示されませんでした。

これはアーマチュアを解除することで対処できました。
その方法を示します。

オブジェクトモードでモデルを選択し、Option+pでメニューを出して、トランスフォームを維持してクリアを選択します。
image.png

アウトライナーにあるArmatureからメニューを開きます。
image.png

メニューからリンク切断を選択します。
image.png

これでアーマチュアを解除することができました。

この時、オブジェクトのオフセット位置が原点からずれていたので、gボタンを押して原点(x,y,zがゼロの位置)に移動しておきました。

エクスポートします。
オブジェクトモードの状態でモデルを選択し、
ファイル > エクスポート > Collada(デフォルト)(.dae)を選択します
image.png

ダイアログが出るので、オプションを選択します。
「選択物のみ」「子を含む」を選択しないとXcodeでうまく読み込めませんでした。
image.png

これで、dae形式へのエクスポートができました。

3. Xcodeで.daeファイルを開く

Xcodeでエクスポートした.daeファイルを開きます。
うまく表示されていれば成功です。
image.png

usdz形式にエクスポートします。
File > Export
でエクスポートダイアログを出し、File FormatにUniversal Scene Description(Mobile)を選択します。

image.png

これで、usdz形式で出力されました。

4.Reality Composerで読み込む

Reality Composerを起動してシーンを作成したら、エクスポートした.usdzファイルをドラッグ & ドロップするだけです。

image.png

任意の3Dモデルを読み込めるようになるば、ARKitで実現できることの幅が広がりそうですね。

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

Xcodeで「DEVELOPER_DIR」に指定するパス

はじめに

ターミナルでどのXcodeを使うか指定するとき、以下の2パターンを見かけますが、みなさんはどちらで指定しているでしょうか。
また、同じ挙動になるかご存知でしょうか。

DEVELOPER_DIRの指定パス
# 1
$ export DEVELOPER_DIR=/Applications/Xcode_12.app/Contents/Developer

# 2
$ export DEVELOPER_DIR=/Applications/Xcode_12.app

私は先日まで知りませんでした。

結論

結論から言うと、1も2も同様の挙動になるので、どちらで指定しても問題ありません。

man xcode-select で解説されています。

xcode-selectのmanualの抜粋
EXAMPLES
       xcode-select --switch /Applications/Xcode.app/Contents/Developer
          Select /Applications/Xcode.app/Contents/Developer as the active developer directory.

       xcode-select --switch /Applications/Xcode.app
          As above, selects /Applications/Xcode.app/Contents/Developer as the active developer directory. The Developer content directory is automatically inferred by xcode-select.

DEVELOPER_DIR の名前通り「Developer」フォルダを指定すべきですが、「Xcode.app」のパスを指定した場合は自動で「Developer」フォルダが推測されます。

おまけ:「DEVELOPER_DIR」と「xcode-select」について

コマンドラインで使うXcodeを指定するには、 DEVELOPER_DIR 環境変数をエクスポートする方法と、 xcode-select コマンドで指定する方法の2通りあります。

以下の記事で取り上げた通り、私は前者の方法を使っています。
https://qiita.com/uhooi/items/29664ecf0254eb637951#xcodeの選択不要

おまけ:XcodeのCLIツールの説明はmanが詳しい

xcode-select コマンド以外にも、 xcrunxcodebuild などのコマンドはWeb上に情報が多くありません。
man コマンドによる説明が一番詳しい公式ドキュメントといっても過言ではないので、覚えておくと有用です。

おわりに

「DEVELOPER_DIR」に指定するパスは、「Developer」フォルダと「Xcode.app」のパスのどちらでもいいことがわかりました。
つまりどちらを指定するかは好みです。

私は1で指定していましたが、短いほうがスッキリするので最近は2で指定することが多いです。

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

TestFlightで内部テスターにアプリをテストしてもらう

image.png

リリース前のアプリを内部ユーザーにインストールしてもらってテストしてもらう手順です。

手順

1、テストするアプリをApp Store ConnectにUploadする。

XcodeからArchiveしていつも通りアップ。

2、テストユーザーを登録。

App Store Connectの「ユーザーとアクセス」の+ボタンでテストユーザーのメールアドレスを指定して追加。
テストユーザーに招待メールリンクから承認してもらう必要があります。

3、TestFlightでテスター登録

App Store ConnectのAppからテストするアプリページを開き、TestFlightタブを開く。
アプリがアップロードされていれば、ビルドに表示されているので、クリックして「輸出コンプライアンス」の設定をしておく。これしておかないとテストできません。

左のメニューから内部グループのApp Store Connectユーザーを選び、+ボタンから2、で登録したテストユーザーをテスターに追加する。
2、でユーザーに承認してもらっていないと、テスター候補に表示されません。

4、TestFlightアプリをダウンロードしてもらう。

テスターにはTestFlight招待メールが送信されます。App StoreからTestFlightアプリをダウンロードすると、招待されたアプリをインストールしてテストできます。

内部テスターは100人まで登録できます。

1万人までテストできる外部テストには、アップルのアプリ審査を受ける必要があります。


Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

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

【Swift】iOS14.0から追加されたWidgetKitを簡単に実装する方法

私の理解力が足りないのか
iOS14で追加された、WidgetKitを実装する際に遠回りをしてしまったので、
シンプルにWidgetKitをとりあえず実装してみたい!!という方々向けに、
最低の最低限(ノーアウト満塁からゲッツー崩れの1打点)だけまとめました。

詳しく知りたい方は公式のサンプルを..
https://developer.apple.com/documentation/widgetkit/building_widgets_using_widgetkit_and_swiftui

環境

  • Xcode: 12.0(GM)
  • Swift5

・手順

1. SwiftUIでプロジェクトを作成

WidgetはSwiftUIで作成しますので、storyboardを選択し内容注意!
スクリーンショット 2020-09-25 11.20.12のコピー.png

2.WidgetExtensionを追加

a.File > New > Targetを選択
スクリーンショット 2020-09-25 11.23.41.png

b.右上のテキストボックスに、Widgetと入力しWidgetExtensionを検索・選択し、Next!
スクリーンショット 2020-09-25 11.24.03のコピー.png

c.ProductNameを入力し、Finish!!
スクリーンショット 2020-09-25 11.24.34のコピー.png

以下のようにWidgetExtensionを追加することができました!
スクリーンショット 2020-09-25 11.25.12.png

動作

widget2.gif

備考

userDefaultsを使用してデータの受け渡しとうする際に、
AppGroupを使用したりする必要がありますが、それらの方法は
後ほど追記するか、別途記事にしたいと思っております。

アプリ公開しました!よろしければインストールお願いします。
とらんぽ

Twitter始めました!よろしければフォローお願いします。
@yajima_tohshu

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

CollectionViewに複数のジェスチャーを認識させてマップアプリのような動きのUIを作った

マップアプリのようなUIで使用したい機能がありまして、今回自前で実装をしてみました。
実装の内容や使用した機能について、今後のためにこの場を借りてまとめることにしました。

動作イメージ

今回作ったUIの動作イメージです。
デモ用に、地図で選択した位置で撮影した写真を表示するアプリを作成しました。

・場所を未選択の状態:全ての写真を表示
・場所を選択した状態:選択した位置で撮影した写真を表示

といった感じです。

モーダルのような見た目のViewを作り、その中にコレクションビューを配置して
拡大表示の場合のみスクロールできるようにしています。

レイアウトイメージ

今回のデモのレイアウトは、次のように組んでいます。

スクリーンショット 2020-09-18 17.09.42.png

実装で使用した機能

  • UIPanGestureRecognizer
  • UIView.transform
  • UIView.animate
  • UIGestureRecognizerDelegate

UIPanGestureRecognizer

目的:pictureCollectionViewへのスワイプのアクションを認識したい

パンジェスチャーを認識する機能で、座標の情報の変化や速度を取得することが可能です。
今回は、 translation(in: UIView?)を使って座標情報の変化を監視しました。

override func viewDidLoad() {
    super.viewDidLoad()

    let collectionViewPanGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(sender:)))
    collectionViewPanGesture.delegate = self
    pictureCollectionView.addGestureRecognizer(handlePanGesture)
}

@objc private func handlePanGesture(sender: UIPanGestureRecognizer) {
    let translation = sender.translation(in: contentView)
    print("translationY : ", translation.y)

    # 省略
}

これでパンジェスチャーにより座標が、どのくらい移動したいかの情報が取得できるようになりました。

UIView.transform

目的:pictureCollectionViewへのスワイプのアクションの情報をViewへ反映させたい

次に移動した座標を、実際にViewへ反映させます。
Viewへの反映には UIView.transform を使用して反映させています。

@objc private func handlePanGesture(sender: UIPanGestureRecognizer) {
    let translation = sender.translation(in: contentView)
    switch sender.state {
    case .changed:
       contentView.transform = CGAffineTransform(translationX: 0, y: translation.y)
    default:
       break
    }
}

UIGestureRecognizer.Stateが、changedのタイミングで更新します。
これにより、ユーザーの動きに追従させるように見せる準備ができました。

実際には、縮小表示から拡大表示にする際の調整に次のような対応を入れています。

override func viewDidLoad() {
    super.viewDidLoad()

    let collectionViewPanGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(sender:)))
    collectionViewPanGesture.delegate = self
    pictureCollectionView.addGestureRecognizer(handlePanGesture)

    // 最初に画面を表示した際の位置を調整(デフォルトの位置)
    contentViewTop.constant = 400
}

@objc private func handlePanGesture(sender: UIPanGestureRecognizer) {
    let translation = sender.translation(in: contentView)
    let maxVertical = -contentViewTop.constant

    switch sender.state {
    case .began:
       adjustValue = 0

       // 拡大表示の際に調整する値を設定
       if !contentView.transform.isIdentity {
          adjustValue = maxVertical
       }
    case .changed:
       let y = max(maxVertical, trans.y + adjustValue)
       contentView.transform = CGAffineTransform(translationX: 0, y: y)
    default:
       break
    }
}

UIView.animate

目的:スワイプアクションが止まった時にViewを拡大・縮小させたい

UIView.animateを使用して、ジェスチャーが終了したタイミングで
Viewを拡大・縮小させてモーダルのように見せます。

@objc private func handlePanGesture(sender: UIPanGestureRecognizer) {
    let translation = sender.translation(in: contentView)
    let maxVertical = -contentViewTop.constant

    switch sender.state {
    case .changed:
       let y = max(maxVertical, trans.y + adjustValue)
       contentView.transform = CGAffineTransform(translationX: 0, y: y)
    case .ended, .cancelled:
       if translation.y < 0  {
          UIView.animate(withDuration: 0.2, animations: {
             self.contentView.transform = CGAffineTransform(translationX: 0, 
                                                                       y: maxVertical)
          })
       } else {
          UIView.animate(withDuration: 0.2, animations: {
             self.contentView.transform = .identity
          })
       }
    default:
       break
    }
}

例では、パンジェスチャーの方向が上の場合には、拡大のアニメーションをさせています。
一方で下の場合では、縮小のアニメーションをさせます。
方向の判定には、translation.yの値を参照して判断しています。

これで、pictureCollectionViewのisScrollEnabledが無効の場合に
ジェスチャーに応じてViewを拡大・縮小できるようになります。

UIGestureRecognizerDelegate

目的:pictureCollectionViewのスクロールを有効にしつつ、他のジェスチャーも認識させたい

pictureCollectionViewのisScrollEnabledが有効の状態においても、
特定の条件の元でパンジェスチャーを有効にするためにUIGestureRecognizerDelegateを使用します。

パンジェスチャーを有効にする条件

今回ジェスチャーを有効にする条件を次の条件を満たす場合に設定しました。

以下の2つを満たす。

  • contentViewが拡大表示の状態
  • pictureCollectionViewが一番上にある状態

または

  • contentViewが縮小表示の状態

を対象としました。

gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)

ここでは、ジェスチャーの同時認識を許可するかどうかの制御を行います。
今回の場合ですと、pictureCollectionViewに対してのスクロールパンジェスチャーが対象になります。

戻り値にtrueを設定する事で、同時認識が許可されます。

extension ViewController: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        gestureRecognizer.view == pictureCollectionView
    }
}

ここでは、引数のgestureRecognizer.viewにて対象のView(今回はpictureCollectionView)の場合にのみ
trueを返すように設定しました。

これで、pictureCollectionViewに対して、複数のジェスチャーが認識できるようになりました。

gestureRecognizerShouldBegin(_:)

ここでは、ジェスチャーの認識を開始するかどうかの制御を行います。
今回は、pictureCollectionViewのパンジェスチャーに対して

①contentViewが拡大表示の状態 かつ pictureCollectionViewが一番上にある状態
②contentViewが縮小表示の状態

のどちらかに該当する場合に開始するように設定します。

extension ViewController: UIGestureRecognizerDelegate {
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if pictureCollectionView.contentOffset.y <= 0 {
            return true
        }

        return !pictureCollectionView.isScrollEnabled
    }
}

また、handlePanGesture内の拡大・縮小のアニメーションが完了したタイミングにisScrollEnabledを更新する処理を追加します。

@objc private func handlePanGesture(sender: UIPanGestureRecognizer) {
    let translation = sender.translation(in: contentView)
    let maxVertical = -contentViewTop.constant

    switch sender.state {
    case .changed:
       let y = max(maxVertical, trans.y + adjustValue)
       contentView.transform = CGAffineTransform(translationX: 0, y: y)
    case .ended, .cancelled:
       if translation.y < 0  {
          UIView.animate(withDuration: 0.2, animations: {
             self.contentView.transform = CGAffineTransform(translationX: 0, 
                                                                       y: maxVertical)
          }) { _ in
             self.pictureCollectionView.isScrollEnabled = true // 追加
          }
       } else {
          UIView.animate(withDuration: 0.2, animations: {
             self.contentView.transform = .identity
          }) { _ in
             self.pictureCollectionView.isScrollEnabled = false // 追加
          }
       }
    default:
       break
    }
}

これで、意図した条件下の場合にのみスワイプによるViewの拡大・縮小の体験ができます。
更にそれ以外の場合は、普通のpictureCollectionViewの機能が使えるようになりました。

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