- 投稿日:2021-01-15T22:49:50+09:00
【Swift】タップ、ロングプレス処理を実装する!
はじめに
アプリ開発していく中で、タップ・ロングプレスといったジェスチャーを使用しました。そこで今回はタップ・ロングプレスジェスチャー時にどうやって処理を実行させるかについてまとめます。
開発環境
- macOS Catalina version10.15.7
- Xcode version12.2
- Swift5
スマホのジェスチャーを使うには?
Swiftでは、
UIGestureRecognizer
を用いることで様々なジェスチャーを用いた処理を実装することができる。
ジェスチャーの種類は下記のように色々あります。
- Tap(DoubleTap)
- Swipe
- Pinch
- Rotate
- LongPress
- Pan(Drag)
Apple公式ドキュメント:UIGestureRecognizer
Appleのヒューマンインターフェースガイドラインより、様々なジェスチャーの動画を確認できます。
Human Interface Guidelinesタップを用いた処理
タップを認識するためには、
UITapGestureRecognizer
を使用します。全体のコードはこちら
import UIKit //UIGestureRecognizerDelegateを追加する class ViewController: UIViewController, UIGestureRecognizerDelegate { override func viewDidLoad() { super.viewDidLoad() //シングルタップ用のインスタンスを生成する let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer( target: self, action: #selector(ViewController.singleTap(_:)) ) //デリゲートをセット tapGesture.delegate = self //viewにタップジェスチャーを追加 self.view.addGestureRecognizer(tapGesture) } //シングルタップ時に実行されるメソッド @objc func singleTap(_ sender: UITapGestureRecognizer) { if sender.state == .ended { //ここに、タップ終了時に実行したい処理を記載する } } }解説
//シングルタップ用のインスタンスを生成する let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer( target: self, action: #selector(ViewController.singleTap(_:)) )シングルタップ用のUITapGestureRecognizerインスタンスを生成しています。
生成時に、ターゲットとタップ時に実行するメソッドを指定しています。Apple公式ドキュメント init(target:action:)
//viewにタップジェスチャーを追加 self.view.addGestureRecognizer(tapGesture)コメントの通り、viewにタップジェスチャーを追加しています。これによって、viewがタップされた時に指定の処理が実行される。
//シングルタップ時に実行されるメソッド @objc func singleTap(_ sender: UITapGestureRecognizer) { if sender.state == .ended { //ここに、タップして離した際に実行したい処理を記載する } }UITapGestureには、様々なプロパティが実装されていてその中に
state
プロパティがあります。ここではどんなstateプロパティがあるかだけ紹介します。詳細は公式ドキュメントでご確認お願いします。
- possible
- began
- changed
- ended
- cancelled
- failed
Apple公式ドキュメント UIGestureRecognizer.State
今回は、
sender.state == .ended
とすることでタップジェスチャー終了時に指定の処理を実行するようにしています。
@objc
や#selector
ってなんぞや?
@objc
はメソッドを定義する際につけることで、Objective-Cのメソッドとして認識されるとのこと。
ではなぜObjective-Cのメソッドとして認識する必要があるのか?target-actionはObjective-Cで実装された仕組みで、target-actionで指定するのはObjective-Cのメソッドである必要がある。そのため、メソッド定義時は
@objc
をつけている。また、Swiftでは
#selector
によってObjective-Cのメソッドを指定できるとのこと。参考サイト:#selector、@objc とターゲット-アクション
ロングプレスを用いた処理
先ほどのタップを用いた処理を基本的には同じです。
override func viewDidLoad() { super.viewDidLoad() //ロングプレス用のインスタンスを生成する let longPressGesture = UILongPressGestureRecognizer( target: self, action: #selector(ViewController.longPress(_:)) ) //デリゲートをセット longPressGesture.delegate = self //viewにロングプレスジェスチャーを追加 self.view.addGestureRecognizer(longPressGesture) } //ロングプレス時に実行されるメソッド @objc func longPress(_ sender: UILongPressGestureRecognizer) { if sender.state == .ended { //ロングプレス終了時に実行したい処理を記載する } }解説もシングルタップと同じなので割愛します。
※ロングプレスのインスタンスを生成する部分で、UILongPressGestureRecognizer
である点だけ違います。まとめ
今回はタップ、ロングプレス処理を実装するにあたり学習したことをまとめました。
想像以上に簡単に実装できて驚きました。まだ理解は浅いため、ドキュメントなどを再度読み込み理解を深めていきたいと思います。以上。
- 投稿日:2021-01-15T18:50:40+09:00
CGAffineTransformのパラメータが簡単に理解できるPlayground Code
アフィン変換を行うCGAffineTransformのパラメータをa, b, c, d, tx, tyと全部指定して変換したい場合、やり方がよくわからなくなることがあるので、使い方が簡単に理解できるPlayground Codeを作りました。
CGAffineTransformのパラメータについての説明
CGAffineTransformのパラメータは、アフィン変換の式にあてはめると次のようになります。
拡大の場合は、aにxの拡大量、dにyの拡大量を指定します。
移動の場合は、txにxの移動量、tyにyの移動量を指定します。
回転の場合はパラメータの部分を次のようにします。
拡大と回転を組み合わせる場合はそれらをかけ合わせる必要があります。
Playground Codeでしていること
UILabelを5つ作り、何もしない、拡大する、移動する、回転するとそれら全部を組み合わせた組み合わせるといったアフィン変換を使っています。
Playground//: A UIKit based Playground for presenting user interface import UIKit import PlaygroundSupport class MyViewController : UIViewController { override func loadView() { func buildLabel(_ num: Int) -> UILabel { let label = UILabel() label.frame = CGRect(x: 150, y: num * 100 + 50, width: 100, height: 20) label.backgroundColor = .red label.textColor = .black label.text = "変換なし" return label } func buildView() -> [UILabel]{ var labels = [UILabel]() let view = UIView() view.backgroundColor = .white for i in 0...4 { let label = buildLabel(i) view.addSubview(label) labels.append(label) } self.view = view return labels } let labels = buildView() _ = { // 拡大 let a: CGFloat = 2 // xの拡大量 let b: CGFloat = 0 let c: CGFloat = 0 let d: CGFloat = 2 // yの拡大量 let tx: CGFloat = 0 let ty: CGFloat = 0 let transform = CGAffineTransform( a: a, b: b, c: c, d: d, tx: tx, ty: ty ) labels[1].transform = transform labels[1].text = "拡大" }() _ = { // 移動 let a: CGFloat = 1 let b: CGFloat = 0 let c: CGFloat = 0 let d: CGFloat = 1 let tx: CGFloat = 30 // xの移動量 let ty: CGFloat = 30 // yの移動量 let transform = CGAffineTransform( a: a, b: b, c: c, d: d, tx: tx, ty: ty ) labels[2].transform = transform labels[2].text = "移動" }() _ = { // 回転 let r = -(CGFloat.pi / 4) let a: CGFloat = cos(r) let b: CGFloat = sin(r) let c: CGFloat = -sin(r) let d: CGFloat = cos(r) let tx: CGFloat = 0 let ty: CGFloat = 0 let transform = CGAffineTransform( a: a, b: b, c: c, d: d, tx: tx, ty: ty ) labels[3].transform = transform labels[3].text = "回転" }() _ = { // 組み合わせ let r = -(CGFloat.pi / 4) let a: CGFloat = 2*cos(r) let b: CGFloat = 2*sin(r) let c: CGFloat = 2*cos(r) let d: CGFloat = 2*cos(r) let tx: CGFloat = 30 // xの移動量 let ty: CGFloat = 30 // yの移動量 let transform = CGAffineTransform( a: a, b: b, c: c, d: d, tx: tx, ty: ty ) labels[4].transform = transform labels[4].text = "組み合わせ" }() } } // Present the view controller in the Live View window PlaygroundPage.current.liveView = MyViewController()最後に
NoteではiOS開発、とくにCoreML、ARKit、Metalなどについて定期的に発信しています。
https://note.com/tokyoyoshidaTwitterでも発信しています。
https://twitter.com/jugemjugemjugem
- 投稿日:2021-01-15T18:40:48+09:00
分からないからこれから書くリスト(読まなくていい)
随時更新
目次
どうでもいいことを書いているから飛ばせ
Qiitaに登録してから2か月が経った。
あの頃よりも身に染みる寒さになり、縮こまっていたせいで首と背中が痛む。IT業界に飛び込んで、もうすぐ1年になる。
そんな中でまだ使ったことがないメソッドや勉強できていない言語に触れざるを得なくなり、
ネットで調べてQiitaに行きつくといつも思う。「早く記事書かないとな……。」
半ば強迫観念にも似た思いだった。
Qiitaの登録は無料。なんて優しいんだろう。
有料でアカウントを作ってもやる気になるタチではない僕には優しい仕様だ。今初めて記事を書いている。
最初に「Markdown記法で書いて」なんて書いてあるのを見て、Markdown記法とは何ぞやと
僕はそっと右のはてなボタンを押した。
軽く目を通して、とりあえず思い出せるもので書いてみよう。うん。楽しい。 チルダ3つでコードエリアを表示するのはスペース4つを使うよりずっと分かりやすい。それに右のエリアにすぐ反映されるところが素晴らしく楽しい。
最初は「何だこれ、分からない。くそったれ。」と思ったが、
書いてみれば楽しいじゃあないか。やめられない。
グダグダと関係のないことばかりを書いていて申し訳ないが、何せ文章を書くのが楽しい。とはいえ、僕の最初の記事はこれから書きたいことを列挙するだけのものになる。
つまり、この後を読んだところで全く得るものはない。
「こいつ、こういうことが分からないんだ。フーン。」となるだけだ。
何かの答えやヒントを探しているのならさっさと検索結果に戻ったほうがいい。僕が主に勉強しているのはSwiftだ。
最近はObjective-Cも読まないといけない状況になって、てんてこまいだったが
何とか読み書きはできるようになった。
ただ、今度はObjective-Cを見すぎてSwiftが分からなくなってきた。これはいけない。
ついにメモ帳の分からないことリストを開放するときが来た。分からないことリスト
これから書こうと思っている自分への忘備録やチートシートのようなものだ。
なるべくObjective-CとSwift両方のコードを揃えて書きたいと思っている。
ざっくりとだけ書いておく。これはほとんどメモと言っていい。
- 文字列の編集(切り出し、検索等)
- Objective-Cで使ったときに少し困った
- Switch文でプロパティ(インスタンスかもしれない)を書くときは波状カッコがいる
- プロパティとインスタンス
- Objective-CとSwiftでのインスタンス生成の違い(*を付けるタイミングを知りたい)
- Objective-CとSwiftのファイルが混合しているプロジェクトでの参照の仕方
- UserDefaultsのこと
- For文の回し方(ついでにWhile文も)
- SQLの書き方(多分チートシートみたいな感じになる)
- 画面遷移の書き方(Push、Modalなど。開発でよく見かけるのを中心に。あとSegue)
- 同じstoryboard、別storyboard、別storyboard先をsegueで繋ぐ。
- APIの使い方(天気確認アプリを作ったのだが、気象庁のAPIが変わってしまったので修正したい)
- ターミナルの動かし方(いまだに使えない)
- Strong、Weak、Unownedの考え方(escapeも)
- クロージャー(よく見かけたが本当に使い方も書き方も分からない)
- enumの使い方
- extensionの使い方(同じファイル内と別ファイルでの使い方)
今のところ、これだけ分からないことがある。
リストにすると中々の量だ。頑張るかという気にさせてくれる。
書けはするがそれが一体何なのかわかっていないことが多いようなので、そこを埋めていきたい。それから、これは全くの余談なのだがQiitaのイメージキャラクターが描きたくなってきた。
完全に非公式でお遊びのネタなので、すぐに記事ができると思う。
名前はすでに決まっている。きーたん
これで決まりだ。
- 投稿日:2021-01-15T17:20:31+09:00
Swift TableViewで無限スクロール(ページネート)を実装する方法
Swift TableViewで無限スクロール(ページネート)を実装する方法
普段の実装で、なんとなくTableViewの無限スクロール(ページネート)を実装してしまっていたので、改めてここに備忘録として記載したいと思います
そもそもページネート(ページネーション)とは
ページネイト(ページネーション)とは、ページ付けと言う意味の英単語になります。
webページなどで、大量のコンテンツやリンクなどを掲載する際、一枚のページでは表示仕切れなかったり処理に時間がかかってしまったりします。
そこで、同じ様なデザインのページを複数分割し、ページ下部などに各ページへのリンクを並べた構成などにすることがあります。
この様なページ構成や、ソフトウェアにより自動的にページ分割を行う処理や機能のことをページネイト(ページネーション)と言います。Youtubeの検索でも、検索結果が表示されてその下に「1,2,3,4,5」とあり、それぞれのページでより下層の検索結果が見れますよね?(最新順ならどんどん古いの、関連度順ならどんどん関連が古いの・・・)
その構成・機能をページネーションと言います。無限スクロールとは
ユーザーが多量のコンテンツをスクロールし続けるテクニックです。
インスタグラムなども、スクロールすればするだけどんどん下層のコンテンツが見れますよね?
これは下にスクロールするたびに新たな情報(Modelとか)を取得し表示していくことで実現しています。
イメージとしては、ページネートと一緒で一度に大量の情報を提示してはあらゆる処理に時間がかかってしまうので、一定数ずつ表示して、最後まで行けば2ページ目分の情報を追加、また最後まで行けば3ページ目分の情報を追加・・・・としているのです。TableViewでの実装方法
STEP1: TableViewを表示されるまで
割愛します
STEP2: 表示する情報を取得するAPIを準備(作成or流用)
詳細は割愛します。
私は自作のAPIから取得しますので、その方法に合わせて記載します。
今回はページ数を送るとその分のデータを取得できる仕様となっています。情報獲得部分は各自が利用するAPIや方法に読み替えてください。
STEP3: 変数を準備する
無限スクロールに利用する変数を準備しましょう。
・表示するデータを入れる配列
・現在の表示ページ数
・現在の表示ステータスViewControlle.swiftimport UIKit import Alamofire import SwiftyJSON class testViewControlle:UIViewController,UITableViewDelegate,UITableViewDataSource{ //==========準備する変数 ここから============ //表示するデータの配列 var datas:[Data] = [] //ページネーション関連変数 var pageCount:Int = 1 //表示ステータス var displayStatus:String = "standby" //ページの総数 var total_pages:Int = 1 //==========準備する変数 ここまで============ override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self tableView.delegate = self getDatas() } func numberOfSections(in tableView: UITableView) -> Int { // #warning Incomplete implementation, return the number of sections return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // #warning Incomplete implementation, return the number of rows return datas.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { //各自好きな様にcellを構築 let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) return cell } //dataを取得するためのメソッド func getDatas(){ //ここにAPIを用いて該当ページの情報を取得する処理を記載 //例 let getUelText = "https://test.com/testdata_get/" + String(pageCount) let getUrl = getUelText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! AF.request(getUrl,method: .get,parameters: nil,encoding: JSONEncoding.default).responseJSON{(responce) in switch responce.result{ case .success: let mJson:JSON = JSON(responce.data as Any) var dateCount:Int = mJson["count"].int! var mData:Data = Data() //例)ここでmDataに取得した情報をセットしておく self.datas.append(mData) self.tableView.reloadData() case .failure(let error): } } } }STEP4: スクロール位置を検知し、終わりに近づいたらdataを取得する様にする
・スクロールの位置を検知するには「scrollViewDidScroll」メソッドを用います。
・メソッドないで、現在のスクロール位置と一番下を取得し、最後に近づいただdataを取得しに行きます。
・取得しにいく時は次のページの情報を取得しにいく様、PageCountを1増やしますViewControlle.swiftimport UIKit import Alamofire import SwiftyJSON class testViewControlle:UIViewController,UITableViewDelegate,UITableViewDataSource{ //表示するデータの配列 var datas:[Data] = [] //ページネーション関連変数 var pageCount:Int = 1 //表示ステータス var displayStatus:String = "standby" //ページの総数 var total_pages:Int = 1 override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self tableView.delegate = self getDatas() } func numberOfSections(in tableView: UITableView) -> Int { // #warning Incomplete implementation, return the number of sections return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // #warning Incomplete implementation, return the number of rows return datas.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { //各自好きな様にcellを構築 let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) return cell } //dataを取得するためのメソッド func getDatas(){ //ここにAPIを用いて該当ページの情報を取得する処理を記載 //例 let getUelText = "https://test.com/testdata_get/" + String(pageCount) let getUrl = getUelText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! AF.request(getUrl,method: .get,parameters: nil,encoding: JSONEncoding.default).responseJSON{(responce) in switch responce.result{ case .success: let mJson:JSON = JSON(responce.data as Any) var dateCount:Int = mJson["count"].int! var mData:Data = Data() //例)ここでmDataに取得した情報をセットしておく self.datas.append(mData) self.tableView.reloadData() case .failure(let error): } } } //==========STEP4 scrollViewDidScroll ここから============ func scrollViewDidScroll(_ scrollView: UIScrollView) { let currentOffsetY = scrollView.contentOffset.y let maximumOffset = scrollView.contentSize.height - scrollView.frame.height let distanceToBottom = maximumOffset - currentOffsetY if(distanceToBottom) { pageCount += 1 getDatas() } } //========== ここまで============ }STEP5: 条件を整え、「重複して取得しようとする」「もうデータがないのに追加取得しようとする」が内容にします
・現状の「scrollViewDidScroll」内では、botttomまでの距離が500以下だと何回でもgetDatasをしてしまいます
→ displayStatus変数を用いて、重複して取得しようとしない様調整します
・現状の「scrollViewDidScroll」内では、もう取得できるデータがなくても、getDatasをしてしまいます
→ total_pages変数を用いて、重複して取得しようとしない様調整しますViewControlle.swiftimport UIKit import Alamofire import SwiftyJSON class testViewControlle:UIViewController,UITableViewDelegate,UITableViewDataSource{ //表示するデータの配列 var datas:[Data] = [] //ページネーション関連変数 var pageCount:Int = 1 //表示ステータス var displayStatus:String = "standby" //ページの総数 var total_pages:Int = 1 override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self tableView.delegate = self getDatas() } func numberOfSections(in tableView: UITableView) -> Int { // #warning Incomplete implementation, return the number of sections return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // #warning Incomplete implementation, return the number of rows return datas.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { //各自好きな様にcellを構築 let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) return cell } //dataを取得するためのメソッド func getDatas(){ //==========STEP5 ここから============ self.displayStatus = "loading" //==========ここまで============ //ここにAPIを用いて該当ページの情報を取得する処理を記載 //例 let getUelText = "https://test.com/testdata_get/" + String(pageCount) let getUrl = getUelText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! AF.request(getUrl,method: .get,parameters: nil,encoding: JSONEncoding.default).responseJSON{(responce) in //==========STEP5 ここから============ self.displayStatus = "standby" //==========ここまで============ switch responce.result{ case .success: let mJson:JSON = JSON(responce.data as Any) var dateCount:Int = mJson["count"].int! total_pages = //トータルページ数が取得できるなら取得する var mData:Data = Data() //例)ここでmDataに取得した情報をセットしておく self.datas.append(mData) self.tableView.reloadData() case .failure(let error): print(error) } } } func scrollViewDidScroll(_ scrollView: UIScrollView) { let currentOffsetY = scrollView.contentOffset.y let maximumOffset = scrollView.contentSize.height - scrollView.frame.height let distanceToBottom = maximumOffset - currentOffsetY //==========STEP5 ここから============ if(distanceToBottom < 500 && displayStatus == "standby" && total_pages != pageCount){ pageCount += 1 getDatas() } //========== ここまで============ } }完成!!
※もしトータルページ数が取得できない場合は、データ取得時にレスポンスが0であればもう最後である旨のフラグを立てたりすると良いかもしれません。まとめ
・変数を準備します。
・「scrollViewDidScroll」でスクロールの位置を取得します。
・スクロールが下に近づいたら、次のデータを取得しにいく様にします。
・取得しに行っている間に重複して作動したり、もうデータ(ページ)がないのに取得しに行ったりしない様、各種条件で制御しましょう。
- 投稿日:2021-01-15T15:43:55+09:00
[iOS][SwiftUI]ネットワークのURLから画像表示(同期・非同期・キャッシュ・リンク・ウィジェット)
ウィジェット(widgets)内に画像を表示し、画像タップでURLスキームによるアプリ連携のために使用したViewとViewModelになります。
ViewModel(画像データの同期・非同期・キャッシュ)
Does try? Data(contentsOf: URL) でキャッシュしてます
https://stackoverflow.com/a/57826757URLImageViewModel.swiftimport SwiftUI final class URLImageViewModel: ObservableObject { @Published var downloadData: Data? = nil let url: String init(url: String, isSync: Bool = false) { self.url = url if isSync { self.downloadImageSync(url: self.url) } else { self.downloadImageAsync(url: self.url) } } func downloadImageAsync(url: String) { guard let imageURL = URL(string: url) else { return } let cache = URLCache.shared let request = URLRequest(url: URL(string: url)!, cachePolicy: URLRequest.CachePolicy.returnCacheDataElseLoad) if let data = cache.cachedResponse(for: request)?.data { self.downloadData = data }else { DispatchQueue.global().async { let data = try? Data(contentsOf: imageURL) DispatchQueue.main.async { self.downloadData = data } } } } func downloadImageSync(url: String) { guard let imageURL = URL(string: url) else { return } let cache = URLCache.shared let request = URLRequest(url: URL(string: url)!, cachePolicy: URLRequest.CachePolicy.returnCacheDataElseLoad) if let data = cache.cachedResponse(for: request)?.data { self.downloadData = data }else { let data = try? Data(contentsOf: imageURL) self.downloadData = data } } }View(画像表示)
URLImageView.swiftimport SwiftUI struct URLImageView: View { @ObservedObject var viewModel: URLImageViewModel var body: some View { if let imageData = self.viewModel.downloadData { if let image = UIImage(data: imageData) { return Image(uiImage: image).resizable().scaledToFit() } else { return Image(uiImage: UIImage()).resizable().scaledToFit() } } else { return Image(uiImage: UIImage()).resizable().scaledToFit() } } }View(リンク付き画像表示)
LinkURLImageView.swiftimport SwiftUI struct LinkURLImageView: View { let url: URL let imageUrlString: String let isSyncURLImage: Bool init(url: URL, imageUrlString: String, isSyncURLImage: Bool = false) { self.url = url self.imageUrlString = imageUrlString self.isSyncURLImage = isSyncURLImage } var body: some View { Link(destination: url) { let imageViewModel = URLImageViewModel(url: imageUrlString, isSync: isSyncURLImage) URLImageView(viewModel: imageViewModel) } } }使用例
ウィジェット(widgets)では非同期が使用できないため、同期通信を使用します。
(非同期はisSyncURLImageにfalseを指定)WidgetEntryView.swiftimport SwiftUI struct WidgetEntryView: View { var body: some View { LinkURLImageView(url: URL(string: "タップ先のリンクのURL(widgets://)")!, imageUrlString: "画像のURL", isSyncURLImage: true) } }