- 投稿日:2019-05-29T23:52:12+09:00
[Swift]Alamofire+SwiftyJSONでQiitaの記事の検索結果をTableViewに表示する[API]
はじめに
・Qiitaの記事を検索し、結果を表示するシンプルなアプリです。
・APIを叩いて結果を表示するものの中でも、簡単にできる方法の紹介です。
・一つの機能だけなのでViewControllerにベタ書きしてます。
・比較的初心者向けの記事になります。完成品
こんな感じでタグで検索をし、それに紐づくQiitaの記事のタイトルを表示します。
ライブラリのインストール
今回インストールするのは
Alamofire
とSwiftyJSON
になります。
ライブラリの導入方法がわからない方はこちらがわかりやすいかと思います。StoryBoard
デフォルトで置いてあるViewControllerを消して、TableViewControllerとその上にSearchBarを置きます。
cellのIdentifierの設定を忘れないようにして置きましょう。(今回は"Cell"にしました)ViewController
import UIKit import SwiftyJSON import Alamofire class TableViewController:UITableViewController,UISearchBarDelegate { @IBOutlet weak var searchBar: UISearchBar! var articles = [[String: AnyObject]]() let baseURL = "https://qiita.com/api/v2/items" // Get JSON func getArticleData(url: String) { Alamofire.request(url, method: .get) .responseJSON { response in if response.result.isSuccess { print("Success! Got the data") let articles : JSON = JSON(response.result.value!) if let article = articles.arrayObject { self.articles = article as! [[String: AnyObject]] print(articles) } } else { print("Error: \(String(describing: response.result.error))") } if self.articles.count > 0 { self.tableView?.reloadData() } } }まず初めに今回使うライブラリのimportを忘れないように行います。
// Get JSON
のところで、Alamofireを使ってQiitaのAPIからデータを取得しています。// Search function func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { getArticleData(url: baseURL + "?page=1&query=tag%3A" + searchBar.text!) tableView.reloadData() } override func viewDidLoad() { super.viewDidLoad() getArticleData(url: baseURL) } }今回はQiitaのタグで検索をかけるようにするため、
getArticleData(url: baseURL + "?page=1&query=tag%3A" + searchBar.text!)
という風にURLに付け加え、新たにJSONを取得するといった風にしています。// Table cell settings extension TableViewController { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return articles.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) let articlePath = articles[indexPath.row] cell.textLabel?.text = articlePath["title"] as? String return cell } }こちらおなじみのTable Cellの設定です。
以上です。
これだけでQiitaの検索が実装できます!最後に
最後まで読んでくださりありがとうございました。
もしどなたかの為になれれば、幸いです!!
- 投稿日:2019-05-29T20:32:10+09:00
Swiftの全角文字の活用
全角の変数や関数はほとんど使わないけど、どこかで活用できないかと考えてみた
開発環境
Xcode 10.2.1
Swift 5
Date型をString型にするとき
import Foundation let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.locale = Locale(identifier: "ja-JP") formatter.dateFormat = "yyyy/MM/dd" let date = Date() print (formatter.string(from: date)) // 2019/05/28dateFormatをenumで定義する
日時を表すアルファベットをよく忘れるのでenumを使用する
DateFormatenum DateFormat { case yyyyMMdd case HHmmss var string: String { switch self { case .yyyyMMdd: return "yyyy/MM/dd" case .HHmmss: return "HH:mm:ss" } } }let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = DateFormat.yyyyMMdd.string let date = Date() print (formatter.string(from: date)) // 2019/05/28問題点
- 区切り文字が定義できない
- switch文で返す文字列を分岐させなきゃいけない
全角文字の使用
区切り文字を全角にすることで定義できる
文字列は"\(self)"
で受け取り、applyingTransform(transform: , reverse: )
を使用し半角にすればOKenum DateFormat { case yyyy/MM/dd case HH:mm:ss var string: String { return "\(self)".applyingTransform(.fullwidthToHalfwidth, reverse: false)! } }Extension
Date + toStringextension Date { func toString(_ dateFormat: DateFormat, calendar: Calendar = Calendar(identifier: .gregorian), locale: Locale = Locale(identifier: "en_US_POSIX"), timeZone: TimeZone = TimeZone.current) -> String { let formatter = DateFormatter() formatter.calendar = calendar formatter.locale = locale formatter.timeZone = timeZone formatter.dateFormat = dateFormat.string return formatter.string(from: self) } }print (Date().toString(.yyyy/MM/dd)) // 2019/05/28まとめ
- 全角記号は意外と使える場面があると思う
- 全角スペースは使用できないので注意
他に使用できそうなところがあったら教えてください
- 投稿日:2019-05-29T18:19:39+09:00
WordPressとCloudFunctionsを連携してアプリ内にブログ機能を実装する
WordPressとCloudFunctionsを連携してアプリ内にブログ機能を実装する
PORT Firebase × PostCoffee #2 で登壇したときの内容です。
コーヒーのサブスクアプリ「PostCofee」の開発をしている中川です。
このPostCofeeのアプリはバックエンドがFirebaseのみで作られています。
コーヒーが好きな方はぜひダウンロードしてみてください!さて、先日のアップデートでストーリーというタブが追加されました。
ストーリーはアプリ内でブログを閲覧できる機能です。
アプリ内でWebサイトを閲覧できるというのはごく普通の機能ですが、これをFirebaseで実現する場合どのようにするのかを紹介したいと思います。WordPressとの連携の流れ
ストーリーで表示しているブログはWordPressで書かれていて、構成はこのようになっています。
- WordPressで記事を公開、更新する
- 上記のタイミングでWordPress側からCloudFunctionsで用意したfunctionを必要なparamを入れて叩いてもらう
- CloudFunctionsが叩かれたらparamを受け取ってFireStoreへ保存し、アプリ側へPush通知を送る
- アプリ側はCloudFireStoreへデータを取りに行く
CloudFunctionsでやること
まずはCloudFunctionsを実装します。
トリガーはfunctions.https
を使用します。
詳細はこちら HTTP リクエスト経由で関数を呼び出すまた、Push通知もこのCloudFunctionsの中で送るようにします。
今回は全ユーザーを対象に送るためトピック配信を使いました。
トピックにメッセージを送信するexports.wordPressCreated = functions.region('asia-northeast1').https.onRequest(async (req, res) => { // ここでWordPressからのデータを受け取ってFireStoreへ保存します。 // WordPressからのデータはreq.bodyの中に入っくるので // 例) const title = req.body.title // このように取得します。 // FireStoreへの保存が完了したらPush通知を送る })Push通知について補足
記事によってPush通知を送る、送らないを分けたい場合、WordPress側から送るデータにBool値を入れると良いです。
Bool値といっても受け取る際にStringの '0', '1' に変換されてしまうようなので判定に注意が必要です。例) WordPressからのparam { "title" : "ほげほげ", "thumbnail" : "https://○○○.png", "url" : "https://qiita.com/", "isSnedPush" : "1" // Push通知を送るかどうか }// isSendPushが '1' のときにPushを送る if (req.body.isSendPush === '1') { // Push通知を送る }CloudFunctionsでの実装は以上です。
実装が完了したらデプロイしましょう。WordPressでやること
WordPress側では記事の公開、更新をフックして上記で実装したCloudFunctionsを叩くようにします。
FireStoreに保存したいデータもここで送るようにします。叩くURL(CloudFunctions)はFirebaseコンソールの
Functions → ダッシュボード → トリガー
に記載されています。
デバッグについて
最終的には実際にWordPressで記事を公開して確認するのが良いのですが、実装途中で確認したい場合や、WordPressを自分でいじれない場合、直接CloudFunctionsを叩いて確認することができます。
デプロイ済みのCloudFunctionsを叩く方法はいくつかあると思うのですが、今回はPostmanを使用しました。
使い方はURLを入力してPOST
でリクエストするだけです。
今回はWordPress側からparamを受け取るので実際に送るデータをjson形式で指定します。CloudFunctionsが叩かれたかどうかは
Firebaseコンソールの Functions → ログ
で確認できます。
まとめ
今回はWordPressとCloud Functionsの連携について紹介しました。
WordPressに限らず、他のサービスとの連携もほぼ同じような構成でいけると思いますので、参考になれば幸いです。ここもっと詳しく知りたいなどありましたら書ける範囲で追記しますのでお気軽にコメントください!
- 投稿日:2019-05-29T17:37:56+09:00
Swift クロージャのパターン
この記事は何?
前回、クロージャを定数オブジェクトに代入して、呼び出しました。
今回は、パラメータ・返り値の有無で、記述がどう変わるかを確認します。実行環境
以下の環境で、動作を確認しました。
- Xcode10.2.1
- Swift5.xハンズオン
コードを書いてみました。
こんな関数があったとして
ランダムな整数を出力する関数
randomNumber
です。func randomNumber() -> Void { print( Int.random(in: 1...Int.max) ) } randomNumber() // 1~最大整数のランダムな整数
Void
は 存在しない と言う意味です。
つまり、-> Void
は 返り値がない ってことになります。
これは省略可能な記述なので、普段のコーディングでは記述されません。クロージャのパターン
引数の有無で、全部で4つのパターンがあります。
いずれにしても{関数の型 in 実装}
となっている点を意識してみると、ふーんって感じになれます。1. 引数なし・返り値なし
パラメータなし・返り値なしlet printRandomNumber = { () -> Void in print(Int.random(in: 1...Int.max)) }2. 引数あり・返り値なし
「出力するランダム整数の最大値」をパラメータとして指定できるようにしています。
パラメータあり・返り値なしlet printRandomNumberWithUpperBounds = { (upperBounds: Int) -> Void in print(Int.random(in: 1...upperBounds)) }3. 引数なし・返り値あり
パラメータなし・返り値ありlet getRandomNumber = { () -> Int in return Int.random(in: 1...Int.max) }4. 引数あり・返り値あり
「取得するランダム整数の最小値と最大値」をパラメータとして指定できるようにしています。
パラメータあり・返り値ありlet getRandomNumberWithLowerAndUpperBounds = { (lowerBounds: Int, upperBounds: Int) -> Int in return Int.random(in: lowerBounds...upperBounds) }ファーストクラス関数
オブジェクトに代入される関数のパターンを整理してみました。
が、「そりゃそうだ」みないな内容になってしまいました。
ちなみに...
関数をオブジェクトに代入できる ことは、第一級(ファーストクラス)関数 と言う要件を果たすための1つだそうです。
ファーストクラス関数の要件には、他に...
関数をパラメータとして、関数に渡せる ことなどがあるそうです。
- 投稿日:2019-05-29T16:59:34+09:00
Swift Closureのキホン
この記事は何?
Swiftプログラミングを理解する上で欠かせない Closure(クロージャ) について、キホンから勉強できます。
実行環境
Xcode10.2.1
Swift5.xハンズオン
実際にコードを書いて、確認します。
カンタンに言うと
クロージャを使うと、関数をオブジェクトとして扱えるようになります。
関数を 何かしらの目的を果たしてくれる機能 として捉えてみましょう。
そんな表現で、クロージャを使うと...
- 機能をオブジェクトに代入する
- 機能をパラメータとして関数に渡す
みたいなことができます。
普通の関数
こんな関数
sum(numbers:)
があったとします。
この関数は、整数の配列になっている各要素の合計値を計算してくれます。func sum(numbers: [Int]) -> Int { var total = 0 for number in numbers { total += number } return total }パラメータとして
[Int]
を受けて、Int
を返していることに注目しておきましょう。
これは、この関数の型[Int]->Int
です。実行すると...
sum(numbers: [1, 2, 3]) // 6
1 + 2 + 3 = 6
ですね。クロージャ
同じ機能をクロージャで表現します。
let sumNumbers = { (numbers: [Int]) -> Int in var total = 0 for number in numbers { total += number } return total }クロージャを定数オブジェクト
sumNumbers
に代入してる感じです。
{ 関数の型 in 実装 }
の部分がクロージャです。
関数の型だけに注目すると...
(numbers: [Int]) -> Int
となっています。
最初の関数sum(numbers:)
と同じ型になっていますね。
機能をオブジェクトに代入する ことができました。
この定数オブジェクトの機能を実行するには...sumNumbers([1, 2, 3]) // 6と記述します。
結果は1 + 2 + 3 = 6
ですね。クロージャのパターン
この記事で扱ったクロージャは...
引数あり・返り値あり な機能でした。と言うことは、他にも...
- 引数なし・返り値ありなクロージャ
- 引数あり・返り値なしなクロージャ
- 引数なし・返り値なしなクロージャ
があるんでしょうね。
- 投稿日:2019-05-29T15:22:00+09:00
iOS シミュレータ を オレオレ証明書なサーバに接続する手順
iOS シミュレータで localhostに接続する
(今更ATSかよっと思いますがご愛嬌) info.plistにATS対象除外リストに入れるのが普通のやり方だと思いますが、証明書をシミュレータにインストールして、信頼できるサーバとして登録したほうが早そう。ATSの設定をしてもうまく行かなかったので。ただのリンク集です。
手順
- オレオレ証明書をこんな感じにシミュレータに入れる
- シミュレータの設定appを起動 > General > Profile > localhost を install
- こんな感じでlocalhostを信頼する
そのほか
ATS調査の時に使ったリンク集がこちら。
https://www.gettoby.com/p/rk8t8x16qrmsTOBYというgoogle chrome 拡張。便利ですねコレ。
- 投稿日:2019-05-29T12:56:53+09:00
UITableViewのデリゲートメソッドの呼び出しを別のオブジェクトに転送する方法
UITableViewのデリゲートメソッドの呼び出しを
tableView.delegate
で指定したオブジェクト以外に転送したかったので、その方法を調べました。 1要件として、既存の
class Delegate: NSObject, UITableViewDelegate
が存在する状態で、Delegate
で宣言しているデリゲートメソッドはそのメソッドが呼び出され、宣言していないメソッドはAnotherDelegate
に転送してほしいということがありました。一つの方法として、
Delegate
上で全てのデリゲートメソッドを定義しておき、その各メソッド内部で転送先のオブジェクトの同名メソッドを呼び出すというのを考えたのですが、面倒な上にUITableView
の一部のデフォルト動作がデリゲートメソッド自体の有無で切り替わるようになっているらしく、この方法だと定義しなくてもいいメソッドのデフォルト動作がうまく作動しないことがわかりました。そこで、セレクタの呼び出しをそのまま別のオブジェクトに転送しようと考えその方法を検索しました。すると、
NSProxy
を利用した方法などが見つかるのですが、残念ながらNSProxy
中で使われているNSInvocation
というクラスがSwiftではunavailableとされており、利用できません。そこで、以下の方法で実装しました。
前提
- Xcode 10.2.1
- Swift 5.0.1
- iOS 12.2
セレクタについての説明は省略します。
TL;DR
コード
final class Delegate: NSObject, UITableViewDelegate { /// 転送先のオブジェクト private let proxy: NSObject init(with proxy: NSObject) { self.proxy = proxy } override func responds(to aSelector: Selector!) -> Bool { // `Delegate` のインスタンスか `proxy` がセレクタを処理できるなら `true` を返す return Delegate.instancesRespond(to: aSelector) || proxy.responds(to: aSelector) } override func forwardingTarget(for aSelector: Selector!) -> Any? { // `Delegate` のインスタンスが処理できないセレクタは `proxy` に処理させる return proxy } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { print("Selected: \(indexPath)") } } final class AnotherDelegate: NSObject, UITableViewDelegate { func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { print("Deselected: \(indexPath)") } } tableView.delegate = Delegate(with: AnotherDelegate())適当にセルをタップした時の出力
Selected: [1, 3] Deselected: [1, 3] Selected: [1, 1] Deselected: [1, 1] Selected: [2, 0]1. 既存の
Delegate
に転送先を保存するためのプロパティを作成するfinal class Delegate: NSObject, UITableViewDelegate { /// 転送先のオブジェクト private let proxy: NSObject init(with proxy: NSObject) { self.proxy = proxy } /* ... */ }まず、転送先のオブジェクトを保存するプロパティ
proxy
を既存のDelegate
に追加しておきます。2.
Delegate
にresponds(to aSelector: Selector!)
を実装するfinal class Delegate: NSObject, UITableViewDelegate { /* ... */ override func responds(to aSelector: Selector!) -> Bool { // `Delegate` のインスタンスか `proxy` がセレクタを処理できるなら `true` を返す return Delegate.instancesRespond(to: aSelector) || proxy.responds(to: aSelector) } /* ... */ }
responds(to aSelector: Selector!)
は、NSObjectProtocol
が定義しているメソッドで、あるオブジェクトが指定したセレクタを処理できるかを返します。今回の要件の場合、
Delegate
のインスタンスがaSelector
を処理できるproxy
がaSelector
を処理できるのいずれか一方が真なら全体としてセレクタを処理できる事になります。
そこでそれぞれの処理の可能性を、
で調べています。
3.
Delegate
にforwardingTarget(for aSelector: Selector!)
を実装するfinal class Delegate: NSObject, UITableViewDelegate { /* ... */ override func forwardingTarget(for aSelector: Selector!) -> Any? { // `Delegate` のインスタンスが処理できないセレクタは `proxy` に処理させる return proxy } /* ... */ }次に、
Delegate
が処理できなかったデリゲートメソッドの呼び出しをproxy
に転送する必要があります。Objective-Cのランタイムでは、あるオブジェクトが実装していないセレクタがそのオブジェクトに通知されると、
forwardingTarget(for aSelector: Selector!)
というメソッドが呼び出されます。 (本当はもっと色々呼び出されるのですが、今回関係するのはこのメソッドだけです。)このメソッドは引数(
aSelector
)に実装していないセレクタが渡され、戻り値でそのセレクタを処理できる別のオブジェクトを返す必要があります。今回の要件の場合、
Delegate
のインスタンスで処理できないセレクタはproxy
が処理できるので、無条件にproxy
を返しています。「処理できないセレクタが呼び出された時にこのメソッドが呼ばれるのであれば、
responds(to aSelector: Selector!)
を実装する必要はないのではないか」と思われるかもしれませんが、両方実装しないと正常に動作しません(forwardingTarget(for aSelector: Selector!)
が呼ばれない)。おそらくUITableView
の内部でデリゲートメソッドを呼び出す際にdelegate.responds(to aSelector: Selector!)
を呼び出し、true
でない場合にはそのデリゲートメソッドの呼び出し自体を省略しているからだと考えられます。4. 通常通りデリゲートメソッドを実装する
final class Delegate: NSObject, UITableViewDelegate { /* ... */ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { print("Selected: \(indexPath)") } } final class AnotherDelegate: NSObject, UITableViewDelegate { func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { print("Deselected: \(indexPath)") } } tableView.delegate = Delegate(with: AnotherDelegate())ここまで実装しておけば、あとは通常通りデリゲートメソッドを実装することで呼び出されます。
TL;DRに書いたコードでは、
Delegate
にtableView(_:didSelectRowAt:)
を実装し、AnotherDelegate
にtableView(_:didDeselectRowAt:)
を実装していますが、どちらも正常に呼び出されています。デリゲートメソッドの呼び出しの優先度は、
Delegate
→AnotherDelegate
の順です。もし両方のクラスに同じデリゲートメソッドを定義した場合、Delegate
のものが優先的に呼ばれます。自分はObjective-Cの理解が全然甘いので、もし何か間違ったことを言っていれば教えてください。
参考文献
- instancesRespond(to:) - NSObject | Apple Developer Documentation
- responds(to:) - NSObjectProtocol | Apple Developer Documentation
- forwardingTarget(for:) - NSObject | Apple Developer Documentation
- forwardInvocation: - NSObject | Apple Developer Documentation
- SwiftでNSProxyを使った開発ができなくなってた - しめ鯖日記
- メッセージの転送を理解する - Qiita
タイトルにはUITableViewとありますが、UIKitの他のデリゲートでも同じことができます ↩
当たり前ですが、ここで
Delegate
のインスタンスがaSelector
を処理できるかを調べるのにself.responds(to: aSelector)
やsuper.responds(to: aSelector)
を使うことはできません。前者は無限ループになるし、後者はそもそもスーパークラスに定義されているわけではないので意味が通りません。(多分)この記事のような目的のためにNSObject.instancesRespond(to:)
というクラスメソッドがあるんだと思います。 ↩
- 投稿日:2019-05-29T12:51:17+09:00
僕のプロフィール
簡単な経歴
1998年生まれ
Geeksalon(プログラミングスクール)Unity編卒業生。
プログラミングSPARTCAMP Swift編、Python編卒業生。
現在はIT企業にて受託案件に携わる。主な作品
Swiftを用いた開発
タイトル NumberNumber
https://itunes.apple.com/us/app/numbernumber/id1444835578?l=ja&ls=1&mt=8
様々なアニメーションをを使い、ランダムで出てくる数字に迫力をだし、他の数字ゲームとの差別化を図りました。またUIにもこだわりうユーザーが飽きないようなイタリックでおしゃれな大人でも使えるようなアプリになったと思います。Hanamaru
https://github.com/matushinn/Hanamaru
insta
https://github.com/matushinn/InstaApp
Weather
https://github.com/matushinn/WeatherApp
Unityを用いた開発
MazeMazeMaze
https://github.com/matushinn/OriginalMaze
ただの迷路ゲームには収まらず、ユーザーが迷路の中にいる相手に見つかったら、かなりのスピードをあげ追いかけてくる機能をつけました。その機能はダイクストラ方という方法を用いました。また実際に森の中で冒険しているような画面に意識して作りました。大富豪
https://github.com/matushinn/Daifugo
旗揚げ
https://github.com/matushinn/IntroClass
クラスの理解にこれを真似して作ってみるのは勉強になっていいと思います。パズルゲーム
https://github.com/matushinn/2DPiece
これ以外にも様々なiOS,Androidアプリを開発しました。
仕事の依頼は
shoooe97@gmail.com
までお願いします。
- 投稿日:2019-05-29T12:25:12+09:00
[swift]フリック入力ボタン
フリック入力ボタン
フリック入力ボタンを作ったので共有します。
動作イメージ
以下から動作イメージを確認できます。
https://youtu.be/8_mnbM1f-_o解説コメントなしのコード
以下に解説コメントなしのコードが置いてあります。
https://github.com/sk409/FlickButton-swift/blob/master/FlickButton.swift解説コメント付きのコード
FlickButton.swiftimport UIKit protocol FlickButtonDelegate { // コンポーネントビューが表示される直前に呼び出されます。 func flickButton(_ flickButton: FlickButton, componentsWillAppear: [FlickButton.Location: UIView]) // コンポーネントビューが表示された直後に呼び出されます。 func flickButton(_ flickButton: FlickButton, componentsDidAppear: [FlickButton.Location: UIView]) // フリック入力後に呼び出されます。 func flickButton(_ flickButton: FlickButton, didFlick activatedView: UIView) // コンポーネントビューが非表示になる直前に呼び出されます。 func flickButton(_ flickButton: FlickButton, componentsWillDisappear: [FlickButton.Location: UIView]) // コンポーネントビューが非表示になった直後に呼び出されます。 func flickButton(_ flickButton: FlickButton, componentsDidDisappear: [FlickButton.Location: UIView]) } // デフォルト実装では何も処理を行いません。 extension FlickButtonDelegate { func flickButton(_ flickButton: FlickButton, componentsWillAppear: [FlickButton.Location: UIView]) {} func flickButton(_ flickButton: FlickButton, componentsDidAppear: [FlickButton.Location: UIView]) {} func flickButton(_ flickButton: FlickButton, didFlick activatedView: UIView) {} func flickButton(_ flickButton: FlickButton, componentsWillDisappear: [FlickButton.Location: UIView]) {} func flickButton(_ flickButton: FlickButton, componentsDidDisappear: [FlickButton.Location: UIView]) {} } class FlickButton: UIButton { // 各コンポーネントビューの位置を区別する enum Location { case top case bottom case left case right } //----------------------------------------------------- // 各コンポーネントビューです。 // 設定時にすでにコンポーネントビューが設定されていたら、スーパービューから取り除きます。 // 設定後にコンポーネントビューを保持する配列にセットします。 var topView: UIView? { willSet { guard let topView = topView else { return } topView.removeFromSuperview() } didSet { guard let topView = topView else { return } componentViews[.top] = topView } } var bottomView: UIView? { willSet { guard let bottomView = bottomView else { return } bottomView.removeFromSuperview() } didSet { guard let bottomView = bottomView else { return } componentViews[.bottom] = bottomView } } var leftView: UIView? { willSet { guard let leftView = leftView else { return } leftView.removeFromSuperview() } didSet { guard let leftView = leftView else { return } componentViews[.left] = leftView } } var rightView: UIView? { willSet { guard let rightView = rightView else { return } rightView.removeFromSuperview() } didSet { guard let rightView = rightView else { return } componentViews[.right] = rightView } } //----------------------------------------------------- // 各コンポーネントビューと自身のデフォルトカラーです。 // フリックによってアクティブ状態になっていない場合に背景色に設定されます。 var defaultColor: UIColor? { didSet { backgroundColor = defaultColor } } // フリックによってアクティブになった場合に背景色に設定されます。 var activeColor: UIColor? var delegate: FlickButtonDelegate? //------------------------------------------------------------------------------ // 各コンポーネントビューと自身とのマージンを設定できます。 // ここに設定した数値分離れた場所にコンポーネントビューが配置されます。 // 例) componentMargins = UIEdgeInsets(top: 2, left: 4, bottom: 6, right: 8) // // [topView] // | // 2 // | // [leftView]-4-[FlickButton]-8-[rightView] // | // 6 // | // [bottomView] var componentMargins = UIEdgeInsets.zero //------------------------------------------------------------------------------ // 設定されているコンポーネントビューを保持します。 private(set) var componentViews = [Location: UIView]() // フリック入力によってアクティブになったビューをユーザに分かりやすいように // 少しの間表示状態にするために使用します。 private var timer: Timer? override init(frame: CGRect) { super.init(frame: frame) setupGestureRecognizers() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupGestureRecognizers() } // 各GestureRecognizerを設定します。 // UILongPressGestureRecognizer: ロングプレスによって各コンポーネントビューを表示します。 // UIPanGestureRecognizer: 上下左右のフリック入力の判定に使用します。 private func setupGestureRecognizers() { let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture(_:))) longPressGestureRecognizer.delegate = self longPressGestureRecognizer.minimumPressDuration = 0.25 addGestureRecognizer(longPressGestureRecognizer) let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) panGestureRecognizer.delegate = self addGestureRecognizer(panGestureRecognizer) } // 各コンポーネントビューを表示します。 // componentMarginsに設定された値を考慮して表示位置を決定します。 private func addComponentViews() { delegate?.flickButton(self, componentsWillAppear: componentViews) for (location, componentView) in componentViews { componentView.backgroundColor = defaultColor addSubview(componentView) let safeAreaFrame = safeAreaLayoutGuide.layoutFrame switch location { case .top: componentView.frame = CGRect( x: safeAreaFrame.origin.x, y: safeAreaFrame.origin.y - safeAreaFrame.height - componentMargins.top, width: safeAreaFrame.width, height: safeAreaFrame.height ) case .bottom: componentView.frame = CGRect( x: safeAreaFrame.origin.x, y: safeAreaFrame.origin.y + safeAreaFrame.height + componentMargins.bottom, width: safeAreaFrame.width, height: safeAreaFrame.height ) case .left: componentView.frame = CGRect( x: safeAreaFrame.origin.x - safeAreaFrame.width - componentMargins.left, y: safeAreaFrame.origin.y, width: safeAreaFrame.width, height: safeAreaFrame.height ) case .right: componentView.frame = CGRect( x: safeAreaFrame.origin.x + safeAreaFrame.width + componentMargins.right, y: safeAreaFrame.origin.y, width: safeAreaFrame.width, height: safeAreaFrame.height ) } } delegate?.flickButton(self, componentsDidAppear: componentViews) } // 各コンポーネントビューを非表示にします。 private func removeComponentViews() { delegate?.flickButton(self, componentsWillDisappear: componentViews) for componentView in componentViews.values { componentView.backgroundColor = defaultColor componentView.removeFromSuperview() } delegate?.flickButton(self, componentsDidDisappear: componentViews) } // フリック入力時に呼び出されます。 // ユーザの指の位置からどのビューが選択されているかを判定し、背景色を変更します。 private func handlePanGestureChanged(_ sender: UIPanGestureRecognizer) { let location = sender.location(in: self) let activatedView = activeView(at: location) ([self] + componentViews.values).forEach { componentView in componentView.backgroundColor = (activatedView == componentView) ? activeColor : defaultColor } } // フリック入力終了時に呼び出されます。 // デリゲートにフリック入力が終了したことを通知します。 // フリック入力によってアクティブになったビューはすぐに非表示にするのではなく、ユーザに分かりやすいように0.2秒間表示した状態を保っています。 private func handlePanGestureEnded(_ sender: UIPanGestureRecognizer) { let location = sender.location(in: self) let activatedView = activeView(at: location) delegate?.flickButton(self, didFlick: activatedView) backgroundColor = defaultColor removeComponentViews() if activatedView != self { addSubview(activatedView) activatedView.backgroundColor = activeColor timer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(handleRemovingActivatedViewEvent(_:)), userInfo: activatedView, repeats: false) } } // フリック入力時のユーザの指の座標から、選択されているビューを判定し返します。 // どのビューも選択されていなければFlickButton自身が返されます。 private func activeView(at location: CGPoint) -> UIView { let width = safeAreaLayoutGuide.layoutFrame.width let height = safeAreaLayoutGuide.layoutFrame.height if let topView = topView, location.y < 0 && ((0 <= location.x && location.x <= width) || abs(location.x) <= abs(location.y)) { return topView } else if let bottomView = bottomView, height < location.y && ((0 <= location.x && location.x <= width) || abs(location.x) <= abs(location.y)) { return bottomView } else if let leftView = leftView, location.x < 0 && ((0 <= location.y && location.y <= height) || abs(location.y) <= abs(location.x)) { return leftView } else if let rightView = rightView, width < location.x && ((0 <= location.y && location.y <= height) || abs(location.y) <= abs(location.x)) { return rightView } return self } // フリック入力によってアクティブになり少しの間表示状態を保っていたビューを非表示にします。 @objc private func handleRemovingActivatedViewEvent(_ timer: Timer) { guard let activatedView = timer.userInfo as? UIView else { return } activatedView.removeFromSuperview() } // ロングプレスジェスチャーの開始によって各コンポーネントビューを表示します。 // ロングプレスジェスチャーの終了によって各コンポーネントビューを非表示にします。 @objc private func handleLongPressGesture(_ sender: UILongPressGestureRecognizer) { if sender.state == .began { addComponentViews() } else if sender.state == .ended { removeComponentViews() } } // パンジェスチャーのコールバックです。 @objc private func handlePanGesture(_ sender: UIPanGestureRecognizer) { if sender.state == .began { backgroundColor = activeColor addComponentViews() } else if sender.state == .changed { handlePanGestureChanged(sender) } else if sender.state == .ended { handlePanGestureEnded(sender) } } } extension FlickButton: UIGestureRecognizerDelegate { // UILongPressGestureRecognizerとUIPanGestureRecognizerが同時に認識されるようにしています。 // 以下の記事を参考にしました。 // https://qiita.com/ruwatana/items/16997b1b416512c20fb6 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } }使用方法
上記の動作イメージ動画で使用してるコードを掲載します。
ViewController.swiftimport UIKit class ViewController: UIViewController { let label = UILabel() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .lightGray let flickButton = FlickButton() view.addSubview(flickButton) flickButton.delegate = self flickButton.defaultColor = .darkGray flickButton.activeColor = .cyan flickButton.setTitle("CENTER", for: .normal) flickButton.setTitleColor(.white, for: .normal) flickButton.titleLabel?.font = .boldSystemFont(ofSize: 18) flickButton.addTarget(self, action: #selector(handleFlickButtonTouchUpInsideEvent(_:)), for: .touchUpInside) flickButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ flickButton.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), flickButton.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor), flickButton.widthAnchor.constraint(equalToConstant: 128), flickButton.heightAnchor.constraint(equalTo: flickButton.widthAnchor), ]) let topView = UILabel() flickButton.topView = topView topView.text = "TOP" topView.textColor = .white topView.textAlignment = .center topView.font = .boldSystemFont(ofSize: 18) let bottomView = UILabel() flickButton.bottomView = bottomView bottomView.text = "BOTTOM" bottomView.textColor = .white bottomView.textAlignment = .center bottomView.font = .boldSystemFont(ofSize: 18) let leftView = UILabel() flickButton.leftView = leftView leftView.text = "LEFT" leftView.textColor = .white leftView.textAlignment = .center leftView.font = .boldSystemFont(ofSize: 18) let rightView = UILabel() flickButton.rightView = rightView rightView.text = "RIGHT" rightView.textColor = .white rightView.textAlignment = .center rightView.font = .boldSystemFont(ofSize: 18) view.addSubview(label) label.backgroundColor = .darkGray label.textColor = .white label.font = .boldSystemFont(ofSize: 24) label.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), label.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), label.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), label.heightAnchor.constraint(equalToConstant: 64), ]) } @objc private func handleFlickButtonTouchUpInsideEvent(_ sender: FlickButton) { label.text = sender.title(for: .normal) } } extension ViewController: FlickButtonDelegate { func flickButton(_ flickButton: FlickButton, didFlick activatedView: UIView) { if let flickButton = activatedView as? FlickButton { label.text = flickButton.title(for: .normal) } else { label.text = (activatedView as? UILabel)?.text } } }
- 投稿日:2019-05-29T11:34:41+09:00
「」内の文字が省略されるUILabelを作る
ときどき、文字列の中央を省略するのではなく、「」内の文字を省略して表示してほしいという要件が来ることがあります。正攻法でやるとなかなか難しそうなので、こんな方法を考えました。
文頭「
中の文字列
」文末
のように、3つのUILabel
に分割し、Stack View等でまとめる- 省略したい中央の
UILabel
のContent Compression Resistance Priority
を、ほかの2つのUILabel
より小さいな値にするコード上では特別することはありませんが、文全体が動的に変わるような場合は適宜分割し、それぞれのラベルに振り分ける必要があります。
ちなみにもし、一行ではなく複数行でこれをやりたいというケースだった場合...どうしたらいいんでしょうね(笑)
- 投稿日:2019-05-29T11:09:51+09:00