検証環境:
Xcode 11.1
Swift 5.1
前回の続き。
後ほど。
検証環境:
Xcode 11.1
Swift 5.1
前回の続き。
今回は画像をダブルタップしたときに拡大・縮小するように実装を追加する。
ScrollView のタップ箇所をズームさせる方法は Apple のプログラミングガイドを参考に実装する。
https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/UIScrollView_pg/ZoomZoom/ZoomZoom.html#//apple_ref/doc/uid/TP40008179-CH102-SW7developer.apple.com
まず、画像が乗っている ScrollView に対してダブルタップのジェスチャを追加する。
// ScrollView にダブルタップのジェスチャを追加 let gesture = UITapGestureRecognizer(target: self, action: #selector(scrollViewDoubleTapped(_:))) gesture.numberOfTapsRequired = 2 scrollView.addGestureRecognizer(gesture)
次にタブルタップ時に呼ばれるアクション部分を実装する。
ScrollView の zoomScale
が最小値(1倍)だった場合は最大値(4倍)に拡大させ、最小値(1倍)より大きい場合には最小値(1倍)に戻るように縮小する。
private let minZoomScale: CGFloat = 1.0 private let maxZoomScale: CGFloat = 4.0 // ScrollView がダブルタップされた時 @objc private func scrollViewDoubleTapped(_ gesture: UITapGestureRecognizer) { guard let scrollView = gesture.view as? UIScrollView else { return } if scrollView.zoomScale == minZoomScale { // タップされた場所を中心に最大に拡大する let location = gesture.location(in: scrollView) let rect = zoomRect(for: scrollView, scale: maxZoomScale, center: location) scrollView.zoom(to: rect, animated: true) } else { // 最小に戻す scrollView.setZoomScale(minZoomScale, animated: true) } } private func zoomRect(for scrollView: UIScrollView, scale: CGFloat, center: CGPoint) -> CGRect { let size = CGSize( width: scrollView.frame.width / scale, height: scrollView.frame.height / scale ) let rect = CGRect( origin: CGPoint( x: center.x - size.width / 2.0, y: center.y - size.height / 2.0 ), size: size ) return rect }
拡大時には UIScrollView の zoom(to:animated:)
を使い、縮小時には setZoomScale(_:animated:)
を使う。
全コード
class ImageViewerViewController: UIViewController, UIScrollViewDelegate { @IBOutlet private weak var scrollView: UIScrollView! private let minZoomScale: CGFloat = 1.0 private let maxZoomScale: CGFloat = 4.0 private var imageView: UIImageView = { let image = UIImage(named: "dog")! let imageView = UIImageView(image: image) // アスペクト比固定でフィットさせる imageView.contentMode = .scaleAspectFit return imageView }() override func viewDidLoad() { super.viewDidLoad() setupScrollView() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() adjustImageViewSize() updateContentInset() } private func setupScrollView() { scrollView.delegate = self scrollView.backgroundColor = .black scrollView.minimumZoomScale = minZoomScale scrollView.maximumZoomScale = maxZoomScale scrollView.showsVerticalScrollIndicator = false scrollView.showsHorizontalScrollIndicator = false scrollView.addSubview(imageView) // ダブルタップのジェスチャを追加 let gesture = UITapGestureRecognizer(target: self, action: #selector(scrollViewDoubleTapped(_:))) gesture.numberOfTapsRequired = 2 scrollView.addGestureRecognizer(gesture) } // 画像のアスペクト比を維持したまま ScrollView の表示範囲にぴったり収まるように ImageView のサイズを調整する private func adjustImageViewSize() { guard let size = imageView.image?.size, imageView.frame.isEmpty else { return } let wRate = scrollView.bounds.width / size.width let hRate = scrollView.bounds.height / size.height let rate = min(wRate, hRate, 1) imageView.frame.size = CGSize(width: size.width * rate, height: size.height * rate) // contentSize を画像サイズと同じにする scrollView.contentSize = imageView.frame.size } // ImageView のサイズが ScrollView の表示範囲より小さい場合に画面中央に配置されるように contentInset を設定する private func updateContentInset() { scrollView.contentInset = UIEdgeInsets( top: max((scrollView.frame.height - imageView.frame.height) / 2, 0), left: max((scrollView.frame.width - imageView.frame.width) / 2, 0), bottom: 0, right: 0 ) } // ズーム対象のビューを返す func viewForZooming(in scrollView: UIScrollView) -> UIView? { return imageView } // ズームのタイミングで contentInset を更新する func scrollViewDidZoom(_ scrollView: UIScrollView) { updateContentInset() } // ScrollView がダブルタップされた時 @objc private func scrollViewDoubleTapped(_ gesture: UITapGestureRecognizer) { guard let scrollView = gesture.view as? UIScrollView else { return } if scrollView.zoomScale == minZoomScale { // タップされた場所を中心に最大に拡大する let location = gesture.location(in: scrollView) let rect = zoomRect(for: scrollView, scale: maxZoomScale, center: location) scrollView.zoom(to: rect, animated: true) } else { // 最小に戻す scrollView.setZoomScale(minZoomScale, animated: true) } } private func zoomRect(for scrollView: UIScrollView, scale: CGFloat, center: CGPoint) -> CGRect { let size = CGSize( width: scrollView.frame.width / scale, height: scrollView.frame.height / scale ) let rect = CGRect( origin: CGPoint( x: center.x - size.width / 2.0, y: center.y - size.height / 2.0 ), size: size ) return rect } }
参考:
検証環境:
Xcode 11.1
Swift 5.1
画像1枚を表示するシンプルな画像ビューワを実装してみる。
画像はピンチで拡大縮小ができるようにする。
実装方法としては UIScrollView 上に UIImageView を配置して実現する。
ただ乗せるだけだと画像の起点が左上になるが UIScrollView の contentInset を設定することで画面の中央に配置できる。
画像のズームは
minimumZoomScale
、maximumZoomScale
を設定の2つが必要。
Storyboard 側で UIScrollView を画面いっぱいに貼り付けてある。
class ImageViewerViewController: UIViewController, UIScrollViewDelegate { @IBOutlet private weak var scrollView: UIScrollView! private let minZoomScale: CGFloat = 1.0 private let maxZoomScale: CGFloat = 4.0 private var imageView: UIImageView = { let image = UIImage(named: "dog")! let imageView = UIImageView(image: image) // アスペクト比固定でフィットさせる imageView.contentMode = .scaleAspectFit return imageView }() override func viewDidLoad() { super.viewDidLoad() setupScrollView() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() adjustImageViewSize() updateContentInset() } private func setupScrollView() { scrollView.delegate = self scrollView.backgroundColor = .black scrollView.minimumZoomScale = minZoomScale scrollView.maximumZoomScale = maxZoomScale scrollView.showsVerticalScrollIndicator = false scrollView.showsHorizontalScrollIndicator = false scrollView.addSubview(imageView) } // 画像のアスペクト比を維持したまま ScrollView の表示範囲にぴったり収まるように ImageView のサイズを調整する private func adjustImageViewSize() { guard let size = imageView.image?.size, imageView.frame.isEmpty else { return } let wRate = scrollView.bounds.width / size.width let hRate = scrollView.bounds.height / size.height let rate = min(wRate, hRate, 1) imageView.frame.size = CGSize(width: size.width * rate, height: size.height * rate) // contentSize を画像サイズと同じにする scrollView.contentSize = imageView.frame.size } // ImageView のサイズが ScrollView の表示範囲より小さい場合に画面中央に配置されるように contentInset を設定する private func updateContentInset() { scrollView.contentInset = UIEdgeInsets( top: max((scrollView.frame.height - imageView.frame.height) / 2, 0), left: max((scrollView.frame.width - imageView.frame.width) / 2, 0), bottom: 0, right: 0 ) } // ズーム対象のビューを返す func viewForZooming(in scrollView: UIScrollView) -> UIView? { return imageView } // ズームのタイミングで contentInset を更新する func scrollViewDidZoom(_ scrollView: UIScrollView) { updateContentInset() } }
参考:
UIScrollViewの中央にUIImageViewを配置しつつズーム可能にする - Qiita
https://qiita.com/wmoai/items/52b1901e62d28dae9f91
検証環境:
Xcode 11.1
Swift 5.1
例えば Universal Links などの機能を使ってディープリンクで深い階層に一気に遷移させたい場合などに使える。
UINavigationController の setViewControllers(_:animated:)
というメソッドがあるので、この引数に複数の ViewController を渡せばよい。
以下実装例。
まず、RootViewController には ViewControllerA
クラスが設定されている。
ここに ViewControllerB、ViewControllerC、ViewControllerD の順に push したのと同じ状態を setViewControllers を使って作る。
pop で最終的に ViewControllerA にまで戻りたいのであれば、表示中の ViewControllerA についても配列の最初に追加する必要がある。
// ViewControllerA @IBAction func handleButton(_ sender: UIButton) { let sb = UIStoryboard(name: "Main", bundle: nil) let vcB = sb.instantiateViewController(identifier: "ViewControllerB") let vcC = sb.instantiateViewController(identifier: "ViewControllerC") let vcD = sb.instantiateViewController(identifier: "ViewControllerD") navigationController?.setViewControllers([self, vcB, vcC, vcD], animated: true) }
用意した遷移ボタン押下すると上記 handleButton
メソッドが呼ばれる。
挙動としては ViewControllerB
、ViewControllerC
はスキップし、ViewControllerD
が表示される。
この時の ViewController のライフサイクルは、遷移元のViewControllerA
とViewControllerD
のみ呼ばれる。
ViewControllerD:viewDidLoad() ViewControllerA:viewWillDisappear(_:) ViewControllerD:viewWillAppear(_:) ViewControllerA:viewDidDisappear(_:) ViewControllerD:viewDidAppear(_:)
この状態から戻るボタンで pop させるとViewControllerC
の読み込みが開始される。
ViewControllerC:viewDidLoad() ViewControllerD:viewWillDisappear(_:) ViewControllerC:viewWillAppear(_:) ViewControllerD:viewDidDisappear(_:) ViewControllerC:viewDidAppear(_:)
さらに戻るとViewControllerB
が読み込まれる。
ViewControllerB:viewDidLoad() ViewControllerC:viewWillDisappear(_:) ViewControllerB:viewWillAppear(_:) ViewControllerC:viewDidDisappear(_:) ViewControllerB:viewDidAppear(_:)
さらに戻ると最初のViewControllerA
が表示される。
ViewControllerB:viewWillDisappear(_:) ViewControllerA:viewWillAppear(_:) ViewControllerB:viewDidDisappear(_:) ViewControllerA:viewDidAppear(_:)
環境: Swift5.1
例えば、あるビューに追加されている複数のサブビューの中から UIButton にキャストできるものだけ処理する場合
for case let button as UIButton in view.subviews { // ボタンに対する処理。ボタン以外はスキップされる。 // 例えばボタンの押下 button.sendActions(for: .touchUpInside) }
キャストまでは不要だが、あるクラス(またはサブクラス)時のみループ内を処理を実行する場合
for view in view.subviews where view is UIButton { // ビューに対する処理。ボタン以外はスキップされる。 // 例えばボタンの削除 view.removeFromSuperview() }
環境: macOS Mojave 10.14.6
Finder の右クリックで表示されるコンテキストメニューからVisual Studio Codeでファイルをすぐに開きたい。
Automator を使用することで Finder のコンテキストメニューに新たなメニューが追加できる。
Automator を起動し、ファイル -> 新規 -> クイックアクションを選択する。
ユーティリティ -> シェルスクリプトを実行 を選択し、右側のスペースにドラッグアンドドロップする。
- ワークフローが受け取る現在の項目: ファイルまたはフォルダ
- 検索対象: Finder.app
- シェル: /bin/bash
- 入力の引き渡し方法: 引数として
- スクリプト
for f in "$@"; do open -a 'Visual Studio Code' "$f" done
Command+S で保存ダイアログを表示して、クイックアクション名として「Visual Studio Code で開く」と入力し、保存する。
これでコンテキストメニューに以下のように追加される。
作成したワークフローは~/Library/Services
ディレクトリ内にある。
こちらを参考にした。
qiita.com
iOS アプリの場合の話。
現在の設定状態は AdMob 管理画面のアプリ->すべてのアプリを表示->APP-ADS.TXT タブから確認できる。
以下は設定方法についての記述部分。
これを読んで最初わからなかったのが、「デベロッパー ウェブサイト」の設定はどこにするんだろうということ。
これは App Store Connect のアプリ申請画面にある「マーケティングURL」に入力すればOKだった。
ここを入力すると App Store のアプリページに以下のように「デベロッパWebサイト」というリンクとして表示される。
で、設定したドメインのルートに自分用のコードスニペットをコピペしたapp-ads.txt
を置く。
これで設定完了したはずなのだが、なかなか管理画面上に反映されず。
ドキュメントには
AdMob によるお客様の app-ads.txt ファイルのクロールおよび確認が完了するまで、少なくとも 24 時間お待ちください。
と書いてあるのだが、結局反映されたのはストア公開してから4日後であった。。
認識されると以下のように、緑の丸がついた状態となる。
環境: Xcode10.3、Swift 5.0.1
UITableView で行の一番下までスクロールして、データの追加読み込み行い、行追加後の reloadData 時にカクっと画面位置がズレる場合がある。
セルの高さはすべて固定値の場合の話。
UITableView の rowHeight
と estimatedRowHeight
はデフォルトで UITableView.automaticDimension
になっているのでこれを固定すればよい。
コードでやる場合は viewDidLoad あたりで
tableView.rowHeight = 60 tableView.estimatedRowHeight = 0
のように設定する。
StoryBoard 上でやる場合は Automatic のチェックを外す。
環境: Xcode10.3、Swift 5.0.1
iPhone のマイクから拾ったオーディオ情報から音程を判定する方法について調べた。
ちゃんとやるには、離散フーリエ変換 (discrete Fourier transform) を使って周波数を算出するらしいのだけれども、今回はAudioKit
というOSSライブラリを使うことで簡単に実現できたのでメモ。
やりたいことがそのまま公式Example(GitHubコードはこちら)として実装されていたのでこの通りに進めた。
画面にはマイクから拾った音の周波数(Frequency)と音階(Note)、また波形がリアルタイムで表示される。
プリコンパイル済みのFrameworkはこちらからダウンロードできる。
プロジェクトにダウンロードしたAudioKit.framework
を追加、
そして TARGET の Build Settings > Linking > Other Linker Flags に -lc++
の追加する。
Podfile
pod 'AudioKit', '~> 4.0'
インストール
$ pod install
Cartfile
github "AudioKit/AudioKit"
ビルド
# iOS $ carthage update --platform iOS --no-use-binaries --cache-builds --new-resolver # Mac $ carthage update --platform Mac --no-use-binaries --cache-builds --new-resolver
さらに TARGET の Build Settings > Linking > Other Linker Flags に -lc++
の追加が必要。
Exampleのコードそのまま。今回は波形表示は不要だったので省いた。
AKFrequencyTracker
クラスを利用することでピッチ検出することができる。
ただし、現状は検出できるのは単音(モノフォニック)のみで、和音のような複数音(ポリフォニック)には対応していないようだ。
import UIKit import AudioKit class ViewController: UIViewController { @IBOutlet weak var frequencyLabel: UILabel! @IBOutlet weak var amplitudeLabel: UILabel! @IBOutlet weak var noteNameWithSharpsLabel: UILabel! @IBOutlet weak var noteNameWithFlatsLabel: UILabel! var mic: AKMicrophone! var tracker: AKFrequencyTracker! var silence: AKBooster! let noteFrequencies = [16.35, 17.32, 18.35, 19.45, 20.6, 21.83, 23.12, 24.5, 25.96, 27.5, 29.14, 30.87] let noteNamesWithSharps = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"] let noteNamesWithFlats = ["C", "D♭", "D", "E♭", "E", "F", "G♭", "G", "A♭", "A", "B♭", "B"] override func viewDidLoad() { super.viewDidLoad() AKSettings.audioInputEnabled = true mic = AKMicrophone() tracker = AKFrequencyTracker(mic) silence = AKBooster(tracker, gain: 0) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) AudioKit.output = silence do { try AudioKit.start() } catch { AKLog("AudioKit did not start!") } Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(ViewController.updateUI), userInfo: nil, repeats: true) } @objc func updateUI() { if tracker.amplitude > 0.1 { frequencyLabel.text = String(format: "%0.1f", tracker.frequency) var frequency = Float(tracker.frequency) while frequency > Float(noteFrequencies[noteFrequencies.count - 1]) { frequency /= 2.0 } while frequency < Float(noteFrequencies[0]) { frequency *= 2.0 } var minDistance: Float = 10_000.0 var index = 0 for i in 0..<noteFrequencies.count { let distance = fabsf(Float(noteFrequencies[i]) - frequency) if distance < minDistance { index = i minDistance = distance } } let octave = Int(log2f(Float(tracker.frequency) / frequency)) noteNameWithSharpsLabel.text = "\(noteNamesWithSharps[index])\(octave)" noteNameWithFlatsLabel.text = "\(noteNamesWithFlats[index])\(octave)" } amplitudeLabel.text = String(format: "%0.2f", tracker.amplitude) } }
コード内にあるnoteFrequencies
配列の数値の意味がわからなかったが、調べたところ音高(Pitch)の周波数らしい。
オクターブ 4 のラの音A4 = 440Hz
を基準音とすると以下の表のようになる。
1オクターブ高いと周波数は2倍になる。
Frequency (Hz) | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
Octave | Note | |||||||||||
C | C# | D | Eb | E | F | F# | G | G# | A | Bb | B | |
0 | 16.35 | 17.32 | 18.35 | 19.45 | 20.60 | 21.83 | 23.12 | 24.50 | 25.96 | 27.50 | 29.14 | 30.87 |
1 | 32.70 | 34.65 | 36.71 | 38.89 | 41.20 | 43.65 | 46.25 | 49.00 | 51.91 | 55.00 | 58.27 | 61.74 |
2 | 65.41 | 69.30 | 73.42 | 77.78 | 82.41 | 87.31 | 92.50 | 98.00 | 103.8 | 110.0 | 116.5 | 123.5 |
3 | 130.8 | 138.6 | 146.8 | 155.6 | 164.8 | 174.6 | 185.0 | 196.0 | 207.7 | 220.0 | 233.1 | 246.9 |
4 | 261.6 | 277.2 | 293.7 | 311.1 | 329.6 | 349.2 | 370.0 | 392.0 | 415.3 | 440.0 | 466.2 | 493.9 |
5 | 523.3 | 554.4 | 587.3 | 622.3 | 659.3 | 698.5 | 740.0 | 784.0 | 830.6 | 880.0 | 932.3 | 987.8 |
6 | 1047 | 1109 | 1175 | 1245 | 1319 | 1397 | 1480 | 1568 | 1661 | 1760 | 1865 | 1976 |
7 | 2093 | 2217 | 2349 | 2489 | 2637 | 2794 | 2960 | 3136 | 3322 | 3520 | 3729 | 3951 |
8 | 4186 | 4435 | 4699 | 4978 | 5274 | 5588 | 5920 | 6272 | 6645 | 7040 | 7459 | 7902 |
ピアノの鍵盤数は88で7オクターブ、この表での範囲はA0 = 27.50Hz
からC8 = 4186Hz
になる。
ちなみに日本のピアノはA4 = 442Hz
で調律されることが多いとのこと。
水色が真ん中のド (C4)、英語では middle C と言う、黄色がラ (A4)
参考: