- 投稿日:2020-09-25T20:25:04+09:00
iOS 14 Safariではホスト名にアンダーバー(_)が含まれるWebページへのHTTPSアクセスが失敗する【一部未検証箇所あり】
- 投稿日:2020-09-25T20:25:04+09:00
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を守ろう。
それが自分の身と、自分の周りの大切な人を守ることに繋がる。
- 投稿日:2020-09-25T20:02:51+09:00
ARKitのトラッキング状態を保存、ロードして再開する
1、ワールドマップを保存
sceneView.session.getCurrentWorldMap { [self] worldMap, error in self.sceneView.session.pause() self.map = worldMap }2、保存したワールドマップ構成でセッション再開
let configuration = ARWorldTrackingConfiguration() configuration.initialWorldMap = map sceneView.session.run(configuration, options: [])
Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。お仕事のご依頼は
rockyshikoku@gmail.com
まで
- 投稿日:2020-09-25T19:52:49+09:00
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ガチャ」には遭遇しませんでした。
- 投稿日:2020-09-25T18:42:57+09:00
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度傾ける想定する変更を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度傾けるという順番でアフィン変換をかけるように記述してみました。
期待した状態と結果が異なるのは、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 }
concatenating(_:)
では期待した結果を得ることができました。
concatenating(_:)については、ドキュメントにてReturn Value
A new affine transformation matrix. That is, t’ = t1*t2.と記載されています。2
なのでこの場合はrotateの変換
→scaleの演算
の順番でアフィン変換が行われる為、期待した状態になります。
- 投稿日:2020-09-25T18:16:14+09:00
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の初期化
ViewControllerimport 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() } }
- 投稿日:2020-09-25T18:12:27+09:00
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オブジェクトを入手してみます。
今回入手した3Dモデルの形式は、.FBXでした。
2. Blenderにインポートする
Blenderを起動して、xを押してデフォルトの立方体を削除しておきます。
ファイル > インポート > インポートしたいモデルの形式(今回の例では.FBX)を選択します。
ファイルの選択ダイアログがでます。
ここで、不要な情報はインポートしないようにします。
今回はアニメーションの情報はインポートしないようにしました。
ここで、エクスポートといきたいところですが、今回のモデルはそのままエクスポートして、Xcodeで開いてもモデルが表示されませんでした。
これはアーマチュアを解除することで対処できました。
その方法を示します。オブジェクトモードでモデルを選択し、Option+pでメニューを出して、トランスフォームを維持してクリアを選択します。
アウトライナーにあるArmatureからメニューを開きます。
これでアーマチュアを解除することができました。
この時、オブジェクトのオフセット位置が原点からずれていたので、gボタンを押して原点(x,y,zがゼロの位置)に移動しておきました。
エクスポートします。
オブジェクトモードの状態でモデルを選択し、
ファイル > エクスポート > Collada(デフォルト)(.dae)を選択します
ダイアログが出るので、オプションを選択します。
「選択物のみ」「子を含む」を選択しないとXcodeでうまく読み込めませんでした。
これで、dae形式へのエクスポートができました。
3. Xcodeで.daeファイルを開く
Xcodeでエクスポートした.daeファイルを開きます。
うまく表示されていれば成功です。
usdz形式にエクスポートします。
File > Export
でエクスポートダイアログを出し、File FormatにUniversal Scene Description(Mobile)を選択します。これで、usdz形式で出力されました。
4.Reality Composerで読み込む
Reality Composerを起動してシーンを作成したら、エクスポートした.usdzファイルをドラッグ & ドロップするだけです。
任意の3Dモデルを読み込めるようになるば、ARKitで実現できることの幅が広がりそうですね。
- 投稿日:2020-09-25T16:05:08+09:00
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私は先日まで知りませんでした。
ときどき
— ウホーイ (@the_uhooi) September 24, 2020DEVELOPER_DIR
で/Applications/Xcode_11.6.app/Contents/Developer
でなく/Applications/Xcode_11.6.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
コマンド以外にも、xcrun
やxcodebuild
などのコマンドはWeb上に情報が多くありません。
man
コマンドによる説明が一番詳しい公式ドキュメントといっても過言ではないので、覚えておくと有用です。xcodebuildとかもそうなんですけど、Xcode関係はだいたいmanが一番詳しい(それがいいことなのかどうかは分からないけど)。
— kishikawa katsumi (@k_katsumi) September 24, 2020おわりに
「DEVELOPER_DIR」に指定するパスは、「Developer」フォルダと「Xcode.app」のパスのどちらでもいいことがわかりました。
つまりどちらを指定するかは好みです。私は1で指定していましたが、短いほうがスッキリするので最近は2で指定することが多いです。
- 投稿日:2020-09-25T12:35:19+09:00
TestFlightで内部テスターにアプリをテストしてもらう
リリース前のアプリを内部ユーザーにインストールしてもらってテストしてもらう手順です。
手順
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を使ったアプリを作っています。
機械学習関連の情報を発信しています。
- 投稿日:2020-09-25T12:05:21+09:00
【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を選択し内容注意!
2.WidgetExtensionを追加
b.右上のテキストボックスに、Widgetと入力しWidgetExtensionを検索・選択し、Next!
以下のようにWidgetExtensionを追加することができました!
動作
備考
userDefaultsを使用してデータの受け渡しとうする際に、
AppGroupを使用したりする必要がありますが、それらの方法は
後ほど追記するか、別途記事にしたいと思っております。アプリ公開しました!よろしければインストールお願いします。
とらんぽTwitter始めました!よろしければフォローお願いします。
@yajima_tohshu
- 投稿日:2020-09-25T09:08:48+09:00
CollectionViewに複数のジェスチャーを認識させてマップアプリのような動きのUIを作った
マップアプリのようなUIで使用したい機能がありまして、今回自前で実装をしてみました。
実装の内容や使用した機能について、今後のためにこの場を借りてまとめることにしました。動作イメージ
今回作ったUIの動作イメージです。
デモ用に、地図で選択した位置で撮影した写真を表示するアプリを作成しました。・場所を未選択の状態:全ての写真を表示
・場所を選択した状態:選択した位置で撮影した写真を表示といった感じです。
モーダルのような見た目のViewを作り、その中にコレクションビューを配置して
拡大表示の場合のみスクロールできるようにしています。— 小岩井 (@WfODXAd0jmop1Ev) September 18, 2020レイアウトイメージ
今回のデモのレイアウトは、次のように組んでいます。
実装で使用した機能
- 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の機能が使えるようになりました。