- 投稿日:2020-01-15T22:53:58+09:00
【swift5】自作電卓アプリを作ってみた
まずは全体像から
GIFがこちら
全体のデザインがこちら
全体のコード
githubはこちらから
https://github.com/sventouz/calculatorimport UIKit class ViewController: UIViewController { var numberOnScreen:Int = 0 var previousNumber:Int = 0 var performingMath = false var operation = 0 @IBOutlet weak var label: UILabel! @IBAction func numbers(_ sender: UIButton) { if performingMath == true { label.text = String(sender.tag-1) numberOnScreen = Int(label.text!)! performingMath = false } else { label.text = label.text! + String(sender.tag-1) numberOnScreen = Int(label.text!)! } } @IBAction func buttons(_ sender: UIButton) { if label.text != "" && sender.tag != 11 && sender.tag != 16{ previousNumber = Int(label.text!)! if sender.tag == 12{ // ÷ label.text = "÷"; } else if sender.tag == 13{ // × label.text = "×"; } else if sender.tag == 14{ // - label.text = "-"; } else if sender.tag == 15{ // + label.text = "+"; } operation = sender.tag performingMath = true; } else if sender.tag == 16 // = が押された時の処理 { if operation == 12{ label.text = String(previousNumber / numberOnScreen) } else if operation == 13{ label.text = String(previousNumber * numberOnScreen) } else if operation == 14{ label.text = String(previousNumber - numberOnScreen) } else if operation == 15{ label.text = String(previousNumber + numberOnScreen) } } else if sender.tag == 11{ // C が押された時の処理 label.text = "" previousNumber = 0; numberOnScreen = 0; operation = 0; } } override func viewDidLoad() { super.viewDidLoad() } }コードを説明していきます
コードの接続
まずコードでは表示されていませんが、0〜9の数字を
@IBAction func numbers(_ sender: UIButton) { }のなかにドラッグアンドドロップします。
こちらも同様に+, -, ÷, × をドラッグアンドドロップ。
@IBAction func buttons(_ sender: UIButton) { }IBアクションではありません。connect actionです。
tag追加
tagを追加していきます。
0には1を
1には2をつけていき9に10がつけばOK
次は
Cに11
÷に12
と続き
=が16になれば完璧です。
続き
@IBAction func numbers(_ sender: UIButton) { // この中が数字をクリックしたときに動く場所 }@IBAction func buttons(_ sender: UIButton) { // この中が数字以外の四則関数をクリックしたときに動く場所 }あとは全体のコードを見ながら各自コードを解読していってください。
感想
電卓なんか簡単でしょ?って思っていたのですが難しかったです。笑
もっとシンプルなのを作っていき最終的には複雑なものを作っていければいいと思う。
その過程ではアウトプットを忘れずに!
参考
https://blog.codecamp.jp/iphone-app-develope-original-calculator
- 投稿日:2020-01-15T22:53:58+09:00
【swift5】自作電卓アプリを作ってみよう(完全版)
まずは全体像から
GIFがこちら
全体のデザインがこちら
全体のコード
githubはこちらから
https://github.com/sventouz/calculatorimport UIKit class ViewController: UIViewController { var numberOnScreen:Int = 0 var previousNumber:Int = 0 var performingMath = false var operation = 0 @IBOutlet weak var label: UILabel! @IBAction func numbers(_ sender: UIButton) { if performingMath == true { label.text = String(sender.tag-1) numberOnScreen = Int(label.text!)! performingMath = false } else { label.text = label.text! + String(sender.tag-1) numberOnScreen = Int(label.text!)! } } @IBAction func buttons(_ sender: UIButton) { if label.text != "" && sender.tag != 11 && sender.tag != 16{ previousNumber = Int(label.text!)! if sender.tag == 12{ // ÷ label.text = "÷"; } else if sender.tag == 13{ // × label.text = "×"; } else if sender.tag == 14{ // - label.text = "-"; } else if sender.tag == 15{ // + label.text = "+"; } operation = sender.tag performingMath = true; } else if sender.tag == 16 // = が押された時の処理 { if operation == 12{ label.text = String(previousNumber / numberOnScreen) } else if operation == 13{ label.text = String(previousNumber * numberOnScreen) } else if operation == 14{ label.text = String(previousNumber - numberOnScreen) } else if operation == 15{ label.text = String(previousNumber + numberOnScreen) } } else if sender.tag == 11{ // C が押された時の処理 label.text = "" previousNumber = 0; numberOnScreen = 0; operation = 0; } } override func viewDidLoad() { super.viewDidLoad() } }コードを説明していきます
コードの接続
まずコードでは表示されていませんが、0〜9の数字を
@IBAction func numbers(_ sender: UIButton) { }のなかにドラッグアンドドロップします。
こちらも同様に+, -, ÷, × をドラッグアンドドロップ。
@IBAction func buttons(_ sender: UIButton) { }IBアクションではありません。connect actionです。
tag追加
tagを追加していきます。
0には1を
1には2をつけていき9に10がつけばOK
次は
Cに11
÷に12
と続き
=が16になれば完璧です。
続き
@IBAction func numbers(_ sender: UIButton) { // この中が数字をクリックしたときに動く場所 }@IBAction func buttons(_ sender: UIButton) { // この中が数字以外の四則関数をクリックしたときに動く場所 }あとは全体のコードを見ながら各自コードを解読していってください。
感想
電卓なんか簡単でしょ?って思っていたのですが難しかったです。笑
もっとシンプルなのを作っていき最終的には複雑なものを作っていければいいと思う。
その過程ではアウトプットを忘れずに!
参考
https://blog.codecamp.jp/iphone-app-develope-original-calculator
- 投稿日:2020-01-15T21:55:47+09:00
Swift: ひらがな・カタカナ文字列の五十音順ソートで躓いた
はじめに
APIレスポンスから受け取ったデータを、五十音順にソートしてリスト表示するというものを行いました。
実際には不要だったのでプロダクトコードには残していませんが、ちょっとややこしくて時間食った&やった過程を無駄にしたくないので備忘録。やること
[{"name": "お店の名前", "kana": "フリガナ"}]
というリストを五十音順ソート- リスト表示するのは
name
のお店の名前のみ内容
五十音順と言えども、以下のような順序の指定がありました。
アイウエオ順→長音→清音→濁音→半濁音→小書きの仮名→ナミ字
- 長音: ー (直前の母音)
- 静音: ア とか ハ とか
- 濁音: ガ とか ダ とか
- 半濁音: パ とか ペ とか
- 小文字: ョ とか ッ とか
- ナミ字: 〜 (直前の母音)
Swift
には.sorted()
メソッドが用意されているので、配列や辞書などはこれを用いることができます。// array let array: [String] = ["パン", "バ", "ハ", "ハ〜シ", "〜バ", "ハーシ", "ハバ", "ハハ", "ハパーシ", "ハョーニ", "ーバ"] let sortedArray = array.sorted() print("sortedArray \(sortedArray)") // sortedArray ["〜バ", "ハ", "ハ〜シ", "ハハ", "ハバ", "ハパーシ", "ハョーニ", "ハーシ", "バ", "パン", "ーバ"] // dictionary let dict = ["1": "パン", "2": "バ", "3": "ハ", "4": "ハ〜シ", "5": "〜バ", "6": "ハーシ", "7": "ハバ", "8": "ハハ", "9": "ハパーシ", "10": "ハョーニ", "11": "ーバ"] let sortedDict = dict.sorted { $0.value < $1.value } print("sortedDict \(sortedDict)") // sortedDict [(key: "5", value: "〜バ"), (key: "3", value: "ハ"), (key: "4", value: "ハ〜シ"), (key: "8", value: "ハハ"), (key: "7", value: "ハバ"), (key: "9", value: "ハパーシ"), (key: "10", value: "ハョーニ"), (key: "6", value: "ハーシ"), (key: "2", value: "バ"), (key: "1", value: "パン"), (key: "11", value: "ーバ")]辞書型の戻り値は純粋な辞書型ではなく、
[(key: Data, value: Data)]
という、keyとvalueを持ったタプルの配列で返ってきます。ちなみに、ひら・カナ混合だと、降順だと
ひら→カナ
の順です。
しかし、既存のメソッドでソートされる順序は
アイウエオ順→ナミ字→清音→濁音→半濁音→小書きの仮名→長音
のようにナミ字の判定が最前列、長音が最後列といった風に上記の期待とは一部逆になります。
(厳密にいうとナミ字がア
よりも前に来ます)今回必要とされる条件は
アイウエオ順→長音→清音→濁音→半濁音→小書きの仮名→ナミ字
の順- 規定に沿ってソート、お店の名前だけディスプレイ
なので
1. 静音 <-> ナミ字 をスワップしてソートする
2.kana
だけソートするわけにいかないので、辞書型リストにしてセットでソートするで対応します。
コード
struct Shop { let name: String let kana: String } let shopList: [Shop] = [Shop(name: "1", kana: "パン"), Shop(name: "2", kana: "バ"), Shop(name: "3", kana: "ハ"), ...] // 全部書くの大変なので、以下のデータを元にしてAPIレスポンス時にshopListを作成していると思ってください // ["1": "パン", "2": "バ", "3": "ハ", "4": "ハ〜シ", "5": "〜バ", "6": "ハーシ", "7": "ハバ", "8": "ハハ", "9": "ハパーシ", "10": "ハョーニ", "11": "ーバ"] private var shopNames: [String] { // name と kana をセットにするため辞書型にする let baseDict = shopList.flatMap { [$0.name: $0.kana] } // ”〜” と ”ー” を入れ替えてソートする let replaced = replaceWaveAndLong(dict: baseDict).sorted { $0.value < $1.value } // ソートしたら元のデータに戻すために、再度”〜” と ”ー” を入れ替える let dict = replaceWaveAndLong(dict: replaced) print("dict \(dict)") // dict [(key: "11", value: "ーバ"), (key: "3", value: "ハ"), (key: "6", value: "ハーシ"), (key: "8", value: "ハハ"), (key: "7", value: "ハバ"), (key: "9", value: "ハパーシ"), (key: "10", value: "ハョーニ"), (key: "4", value: "ハ〜シ"), (key: "2", value: "バ"), (key: "1", value: "パン"), (key: "5", value: "〜バ")] // name の部分だけ取り出す return dict.map { $0.key } } // 指定のソート順に合わせるための ”〜” と ”ー” を入れ替えるメソッド func replaceWaveAndLong(dict: [(key: String, value: String)]) -> [(key: String, value: String)] { let replaced = dict.map { elem -> (key: String, value: String) in let wave = "〜", long = "ー" if elem.value.contains(wave) { return (key: elem.key, value: elem.value.replacingOccurrences(of: wave, with: long)) } if elem.value.contains(long) { return (key: elem.key, value: elem.value.replacingOccurrences(of: long, with: wave)) } return elem } return replaced }といった感じです。
まとめ
長音は大体の場合、前の文字の母音になるので、大体順序は先頭に来るように指定されることが多そう。でも既存だと後ろに行ってしまうので、このように手を加える必要がありそう。
ちなみにここに辿り着くまで、長音が来たら前の文字をローマ字化して、その母音と同じ音に変換してとか
unicodeで文字をスイッチしたりとか、enum
で全部作ってやろうかなど思いましたが、上記の条件ならこれで大丈夫そうでした。実装することはなかったけど少し勉強になりました、ちゃんちゃん。
- 投稿日:2020-01-15T21:48:05+09:00
【swift 5】アプリのバックグラウンドにユーザーの選んだ画像を設定する
環境
Xcode 11.3
swift 5.1.3
CocoaPods 1.8.4要件
機能要件
アプリのバックグラウンドに、ユーザーの選んだ画像を設定する機能
仕様
- 「背景に画像を設定するボタン」をタップ
- 背景画像設定画面の表示
- カメラロール内の画像一覧を表示
- 画像をタップすることで、拡大表示
- オッケーボタンを押すことで、画像がアプリ内部に保存される
- その段階で、アプリの背景に選択した画像が表示される
- 設定完了後、「背景に画像を設定するボタン」が表示されているもとのページに戻る
設計
写真選択
背景に画像を設定するボタンタップ後、背景画像設定用の画面を表示。
カメラロール内の画像一覧を表示し、画像を選択させる。
選択後、選択した画像が背景に拡大表示されるようにする。storyboard上の設定
TopViewControllerに対して
- UIButtonを追加
- storyboardIDを、
topView
に設定SetBackgroundImageViewControllerに対して
- 背景画像設定用の新規コントローラーの作成(SetBackgroundImageViewController)
- UIButton,imageViewを設置
- storyboardIDを
backgroundImageView
に設定コードの記述
TopViewController.swiftimport UIKit class TopViewController: UIViewController { // 背景image設定ページへ // Btnが押されたときのaction @IBAction func showImageSelection(_ sender: Any) { // コントローラーの指定 let backgroundImageController = self.storyboard?.instantiateViewController(withIdentifier: "backgroundImageView") as! SetBackgroundImageViewController // 全画面表示で画面遷移 backgroundImageController.modalPresentationStyle = UIModalPresentationStyle(rawValue: 0)! self.present(backgroundImageController, animated: false, completion: nil) } override func viewDidLoad() { super.viewDidLoad() print("user view did load") } }SetBackgroundImageViewControllerimport UIKit class SetBackgroundImageViewController: UIViewController { // 背景設定 @IBOutlet weak var backgroundImageView: UIImageView! var backgroundImage:UIImage! var themaColor:UIColor! // ボタンのoutlet接続およびaction接続 // 戻るボタン @IBOutlet weak var rerturnBtn: boundButton! @IBAction func returnBtnPushed(_ sender: Any) { } // 画像選択ボタン @IBOutlet weak var selectBtn: UIButton! @IBAction func selectBtnPushed(_ sender: Any) { } // 決定ボタン @IBOutlet weak var setBtn: UIButton! @IBAction func setBtnPushed(_ sender: Any) { } override func viewDidLoad() { super.viewDidLoad() // imagePickerを表示 addImagePickerView() } } // imagePickerViewの設定用 extension SetBackgroundImageViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate{ func addImagePickerView() { print("pushed image!") //imagePickerViewを表示する let pickerController = UIImagePickerController() pickerController.sourceType = .photoLibrary pickerController.delegate = self self.present(pickerController, animated: true, completion: nil) } // pickerの選択がキャンセルされた時の処理 func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { dismiss(animated: true, completion: nil) } }選択画像表示
画像が選択されたら、コントローラーのimageViewに表示されるようにする。
SetBackgroundImageViewController.swift// imagePickerViewの設定用 extension SetBackgroundImageViewController { // 以下の関数を追加 // 写真が選択された時の処理 func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { // 選択されたimageを取得 guard let selectedImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage? else {return} // imageをimageViewに設定 backgroundImage = selectedImage backgroundImageView.image = backgroundImage // imagePickerの削除 self.dismiss(animated: true, completion: nil) } }選択画像保存
決定ボタン押下時、選択されている画像をアプリ内に保存。
処理終了後に、topページに戻る。SetBackgroundViewController.swiftimport UIKit class SetBackgroundImageViewController: UIViewController { // 背景設定 @IBOutlet weak var backgroundImageView: UIImageView! var backgroundImage:UIImage! var themaColor:UIColor! // ボタン接続 // 戻るボタン @IBOutlet weak var rerturnBtn: boundButton! @IBAction func returnBtnPushed(_ sender: Any) { } // もう一度選ぶボタン @IBOutlet weak var selectBtn: UIButton! @IBAction func selectBtnPushed(_ sender: Any) { } // 確定ボタン @IBOutlet weak var setBtn: UIButton! @IBAction func setBtnPushed(_ sender: Any) { // ここから追記 // backgroundImage.pngという名前で保存 let imagePath = self.fileInDocumentsDirectory(filename: "backgroundImage.png") if saveImage(image: backgroundImage, path: imagePath) { // 画像を設定 let topVC = self.presentingViewController as! TopViewController topVC.backgroundImage.image = backgroundImage // もとの画面(TopViewController)に戻る self.dismiss(animated: false, completion: nil) } else { backgroundImage = nil } // ここまで追記 } override func viewDidLoad() { super.viewDidLoad() // imagePickerを表示 addImagePickerView() } } // ここから追記 // imageの保存関数 extension BackgroundImageViewController { // DocumentディレクトリのfileURLを取得 func getDocumentsURL() -> NSURL { let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as NSURL return documentsURL } // ディレクトリのパスにファイル名をつなげてファイルのフルパスを作る func fileInDocumentsDirectory(filename: String) -> String { let fileURL = getDocumentsURL().appendingPathComponent(filename) return fileURL!.path } // ファイルに書き込み func saveImage (image: UIImage, path: String ) -> Bool { let pngImageData = image.pngData() do { try pngImageData!.write(to: URL(fileURLWithPath: path), options: .atomic) } catch { print(error) return false } return true } } // ここまで追記 // imagePickerViewの設定用 extension SetBackgroundImageViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate{ func addImagePickerView() { print("pushed image!") //imagePickerViewを表示する let pickerController = UIImagePickerController() pickerController.sourceType = .photoLibrary pickerController.delegate = self self.present(pickerController, animated: true, completion: nil) } // pickerの選択がキャンセルされた時の処理 func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { dismiss(animated: true, completion: nil) } // 写真が選択された時の処理 func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { // 選択されたimageを取得 guard let selectedImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage? else {return} // imageをimageViewに設定 backgroundImage = selectedImage backgroundImageView.image = backgroundImage // imagePickerの削除 self.dismiss(animated: true, completion: nil) } }保存画像のロード・表示
以下の関数を用意しておき、背景画像を表示させたいところで呼び出す。
// imageをload func loadBackgroundImage() -> UIImage? { let image = UIImage(contentsOfFile: fileInDocumentsDirectory(filename: "backgroundImage.png")) if image == nil { print("backgroundImage is missing") } return image }参考ページ
teratail : エラー:which is already presenting (null)の対処法
qiita : 【Swift4】URL先の画像をアプリ内に保存&ロードする
HatenaBlog : iOSでアプリ内部(Document)に画像(UIImage)を保存する方法【iOSアプリ開発】
- 投稿日:2020-01-15T21:06:43+09:00
iPadのplaygroundsはプログラミングの学習ツールではなくガチの開発環境だった
パッと見で、子供でもプログラミングを学ぶことができる学習ツールに見える iPad の Playgrounds ですが、各種フレームワークも呼び出せるガチの開発環境だったので、SceneKitで3Dオブジェクトを表示するまでを紹介します。
新規プロジェクトの作成
Playgrounds を起動したら、左上のアイコンをタップして新しい空のプロジェクトを作成します。
コードの入力
ダブルタップでソフトウェアキーボードが出てくるので下記のコードを入力します。
内容は SceneKit を使って、ライトとカメラと立方体を作成して配置しているだけのシンプルなものです。
scenekit01.swiftimport PlaygroundSupport import UIKit import SceneKit var sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 1000, height: 200)) var scene = SCNScene() sceneView.scene = scene sceneView.backgroundColor = .black sceneView.allowsCameraControl = true PlaygroundPage.current.liveView = sceneView var lightNode = SCNNode() lightNode.light = SCNLight() lightNode.light?.type = .omni lightNode.light?.intensity = 1000 lightNode.light?.shadowMode = .deferred lightNode.position = SCNVector3(x: 2, y: 2, z: 2) scene.rootNode.addChildNode(lightNode) var cameraNode = SCNNode() cameraNode.camera = SCNCamera() cameraNode.position = SCNVector3(x: 0, y: 0, z: 5) scene.rootNode.addChildNode(cameraNode) var material = SCNMaterial() material.diffuse.contents = UIColor.red var box = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0.2) var boxNode = SCNNode(geometry: box) scene.rootNode.addChildNode(boxNode) box.materials = [material] boxNode.rotation = SCNVector4(x: 1.0, y: 1.0, z: 0.0, w: 0.0) boxNode.scale = SCNVector3(x: 1.0, y: 1.0, z: 1.0)実行してみる
allowsCameraControll を True にしているので、ドラッグで回転とかをする事ができます。
最後に
エディタの反応が少し悪くイラつく事もあるのですが、 iPad だけで、ガチの swift コードが書けることに感動しました。
これで自宅外でいつでもプログラミングができます!
- 投稿日:2020-01-15T19:59:34+09:00
Swift:OAuthSwiftを使ってQiita API v2をいじる
macOSでOAuthSwiftを使ってQiita APIにOAuth 2.0で認証する方法
1. Qiita側の準備
Qiitaの設定ページへ行き,新規にアプリケーションを登録して,Client IDとClient Secretを入手します.
リダイレクト先のURLは[任意の文字列] + ://oauth-callback
にして,メモしておきます.
2. プロジェクトの下準備
URL Typeを追加します.URL Schemesに先ほどのリダイレクト先の任意の文字列を入力します.
3. コーディング
OAuth認証をするためのWebViewController
OAuthWebVC.swiftimport Foundation import WebKit import OAuthSwift class OAuthWebVC: OAuthWebViewController, WKNavigationDelegate { var targetURL: URL? let webView = WKWebView() var cancelBtn: NSButton! override func viewDidLoad() { super.viewDidLoad() webView.navigationDelegate = self webView.translatesAutoresizingMaskIntoConstraints = false self.view.addSubview(webView) cancelBtn = NSButton(title: "Cancel", target: self, action: #selector(cancel)) cancelBtn!.translatesAutoresizingMaskIntoConstraints = false self.view.addSubview(cancelBtn) webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true webView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true webView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -40.0).isActive = true cancelBtn!.widthAnchor.constraint(equalToConstant: 82.0).isActive = true cancelBtn!.heightAnchor.constraint(equalToConstant: 32.0).isActive = true cancelBtn!.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20.0).isActive = true cancelBtn!.centerYAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -20.0).isActive = true } override func handle(_ url: URL) { targetURL = url super.handle(url) self.loadAddressURL() } func loadAddressURL() { guard let url = targetURL else { return } let req = URLRequest(url: url) webView.load(req) } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if let url = navigationAction.request.url { if url.host == "oauth-callback" { OAuthSwift.handle(url: url) decisionHandler(WKNavigationActionPolicy.cancel) self.dismissWebViewController() return } } decisionHandler(WKNavigationActionPolicy.allow) } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { Swift.print(error.localizedDescription) self.dismissWebViewController() } @objc func cancel() { self.dismissWebViewController() } }認証とユーザ情報の取得を促すベースのViewController
ViewController.swiftimport Cocoa import OAuthSwift class ViewController: NSViewController, OAuthWebViewControllerDelegate { var oauthswift: OAuth2Swift? var client: OAuthSwiftClient? lazy var webVC: OAuthWebVC = { let controller = OAuthWebVC() controller.view = NSView(frame: NSRect(x: 0, y: 0, width: 600, height: 400)) controller.delegate = self controller.viewDidLoad() return controller }() override func viewDidLoad() { super.viewDidLoad() } @IBAction func register(_ sender: Any) { let _ = webVC.webView oauthswift = OAuth2Swift(consumerKey: "Client ID", consumerSecret: "Client Secret", authorizeUrl: "https://qiita.com/api/v2/oauth/authorize", accessTokenUrl: "https://qiita.com/api/v2/access_tokens", responseType: "code") oauthswift?.authorizeURLHandler = getURLHandler() oauthswift?.allowMissingStateCheck = true let _ = oauthswift?.authorize( withCallbackURL: URL(string: "oauth-qiita-station://oauth-callback")!, scope: "read_qiita write_qiita", state: "", headers: ["Content-Type" : "application/json"], completionHandler: { (result) in switch result { case .success(let (credential, _, _)): Swift.print("token", credential.oauthToken) self.client = OAuthSwiftClient(credential: credential) case .failure(let error): Swift.print(error.description) } }) } func getURLHandler() -> OAuthSwiftURLHandlerType { if webVC.parent == nil { self.presentAsSheet(webVC) } return webVC } @IBAction func getUserInfo(_ sender: Any) { let userID: String = "Kyome" client?.get( URL(string: "https://qiita.com/api/v2/users/\(userID)")!, completionHandler: { (result) in switch result { case .success(let response): if let json = try? response.jsonObject(options: .allowFragments) as? [String : Any] { Swift.print(json) } case .failure(let error): Swift.print(error.description) } }) } func oauthWebViewControllerWillAppear() { } func oauthWebViewControllerDidAppear() { } func oauthWebViewControllerWillDisappear() { } func oauthWebViewControllerDidDisappear() { oauthswift?.cancel() } }参考
- 投稿日:2020-01-15T19:46:53+09:00
ストーリーボードをつかわず、再生↔一時停止をToolbarで書くとこうなるサンプル(Playgroundサンプル付き)
はじめに
オーディオプレーヤーの再生↔一時停止のようにツールバーをつくるとき、みなさんはどのように書いていますか。
- Storyboardをつかわない
- Swift4
- NavigationControllerをつかう
こんな条件のときのサンプルが見つけられなかったので、いろいろためしながら実装した方法を紹介したいとおもいます。
実行結果は、こんな感じになります。
この記事の最後に、Playgroundにコピペすると動作するコードを載せていますのでそちらも参考になれば前準備
ビューの作成時に、
self.toolbarItems
をセットしておく必要があります。
NavigationController
にツールバーを表示させる方法は、NavigationControllerのToolbarを特定のViewControllerでのみ有効にする - Qiitaを参考にしてみてください。ボタンをトグルするコード
ボタンのオブジェクトはひとつにして
barButtonSystemItem
を.play
と.pause
でトグルすればいいのでは?とおもったのですが、再生ボタンと一時停止ボタンの2つのオブジェクトを用意して差し替えるのがベターな方法みたいです。private var audioPlaying = false // MARK: ツールバーのアイテム private var uiPlayButtonItem : UIBarButtonItem { let button = UIBarButtonItem( barButtonSystemItem: .play, target: self, action: #selector(onButton(_:))) button.tag = 1 return button } private var uiPauseButtonItem : UIBarButtonItem { let button = UIBarButtonItem( barButtonSystemItem: .pause, target: self, action: #selector(onButton(_:))) button.tag = 1 // 置換するのでtagはおなじ return button } private lazy var uiButtonItems: [UIBarButtonItem] = { let flexibleItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil) flexibleItem.tag = 0 return [ flexibleItem, uiPauseButtonItem, // 最初は再生 flexibleItem, ] }() @objc func onButton(_ sender: UIBarButtonItem) { // 再生ボタンを差し替えるので、index位置を取得する guard let buttonIndex = self.uiButtonItems.firstIndex( where: {$0.tag == 1} ) else { print( "再生ボタンのインデックスが取得できてません") return } if self.audioPlaying { self.audioPlaying = false self.toolbarItems?[buttonIndex] = self.uiPlayButtonItem return } self.audioPlaying = true self.toolbarItems?[buttonIndex] = self.uiPauseButtonItem }Playgroundで試せるコード
以下を実行すると、冒頭のスクショのような動作になります。
import UIKit import XCPlayground import PlaygroundSupport class ViewController: UIViewController { private var audioPlaying = false let label = UILabel() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white label.textAlignment = .center view.addSubview(label) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() label.frame = view.bounds } // ツールバーを表示する override func viewWillAppear(_ animated: Bool) { self.toolbarItems = self.uiButtonItems self.updateButton() self.updateLabel() self.navigationController?.setToolbarHidden(false, animated: false) } override func viewWillDisappear(_ animated: Bool) { self.navigationController?.setToolbarHidden(true, animated: false) super.viewWillDisappear(animated) } // MARK: ツールバーのアイテム private var uiPlayButtonItem : UIBarButtonItem { let button = UIBarButtonItem( barButtonSystemItem: .play, target: self, action: #selector(onButton(_:))) button.tag = 1 return button } private var uiPauseButtonItem : UIBarButtonItem { let button = UIBarButtonItem( barButtonSystemItem: .pause, target: self, action: #selector(onButton(_:))) button.tag = 1 // 置換するのでtagはおなじ return button } private lazy var uiButtonItems: [UIBarButtonItem] = { let flexibleItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil) flexibleItem.tag = 0 return [ flexibleItem, uiPauseButtonItem, // 最初は再生 flexibleItem, ] }() func updateLabel() { label.text = self.audioPlaying ? "playing..." : "pause" } func togglePlayState() { self.audioPlaying = !self.audioPlaying } func updateButton() { // 再生ボタンを差し替えるので、index位置を取得する guard let buttonIndex = self.uiButtonItems.firstIndex( where: {$0.tag == 1} ) else { print( "再生ボタンのインデックスが取得できてません") return } if self.audioPlaying { self.toolbarItems?[buttonIndex] = self.uiPauseButtonItem return } self.toolbarItems?[buttonIndex] = self.uiPlayButtonItem } @objc func onButton(_ sender: UIBarButtonItem) { self.togglePlayState() self.updateButton() self.updateLabel() } } var ctrl = ViewController() let navigationController = UINavigationController(rootViewController: ctrl) navigationController.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480) PlaygroundPage.current.needsIndefiniteExecution = true PlaygroundPage.current.liveView = navigationController.view
- 投稿日:2020-01-15T18:29:41+09:00
NavigationControllerのToolbarを特定のViewControllerでのみ有効にする
UIViewControllerprivate lazy var uiButtonItems: [UIBarButtonItem] = { // 適当なUIBarButtonItemの配列 return [...] }() // ビュー表示のタイミングでツールバーを表示する override func viewWillAppear(_ animated: Bool) { self.toolbarItems = uiButtonItems self.navigationController?.setToolbarHidden(false, animated: false) } // ビュー非表示のタイミングで非表示に戻す override func viewWillDisappear(_ animated: Bool) { self.navigationController?.setToolbarHidden(true, animated: false) super.viewWillDisappear(animated) }
- 投稿日:2020-01-15T18:20:56+09:00
Swift 配列から条件にあった要素のインデックス値を取得する
String配列の一部を置換する例
var students = ["マリオ", "ピーチ", "ワルイージ", "ヨッシー"] if let i = students.firstIndex(of: "ワルイージ") { students[i] = "ルイージ" } print(students) // Prints "["マリオ", "ピーチ", "ルイージ", "ヨッシー"]"UIBarButtonItem配列で
tag==3
な要素のインデックスを取得するlet buttonA : UIBarButtonItem { let button = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(onPlayPauseButton(_:))) button.tag = 1 return button } let buttonB : UIBarButtonItem { let button = UIBarButtonItem(barButtonSystemItem: .pause, target: self, action: #selector(onPlayPauseButton(_:))) button.tag = 3 return button } // buttonB のインデックス位置を取得したい let buttons = [ buttonA, buttonB ] if let i = buttons.firstIndex(where: {$0.tag == 3}) else{ print(i) // 1 }
- 投稿日:2020-01-15T13:43:27+09:00
Xcodeでデバッグ中に変数の値が確認できなかったけど簡単に解決したメモ
そこにある変数が見えない
(lldb) po folders error: <EXPR>:3:1: error: use of unresolved identifier 'folders' folders ^~~~~~~ (lldb) po self.folders error: <EXPR>:3:1: error: use of unresolved identifier 'self' self.folders ^~~~ (lldb) p folders error: <EXPR>:3:1: error: use of unresolved identifier 'folders' folders ^~~~~~~値がどこからも確認できないんだ…
解決方法
ということで、SwiftとObjective-Cが混在しているプロジェクトで起きがちという噂の
「Xcodeでデバッグ中に変数の値が確認できない」
「lldbデバッグで変数の中身を見ようと、poしても見れない」
とお困りの方に朗報
こんなとき、コードにprint関数を埋め込んだりしてましたが、po print(対象の変数)
(lldb) po print(folders) ["新しいフォルダ", "新しいフォルダ1", "新しいフォルダ2"]lldbコマンドで、
print関数
の戻り値をprint object
するという方法で、コードを汚さず値を確認することができます。(できる場合があります。くらいにしておこうかな)
- 投稿日:2020-01-15T07:54:46+09:00
NeumorphismなUIをSwiftUIで作れるライブラリ、Neumorphismicを作ってみた
先日Neumorphism: 令和時代のスキューモーフィズムを読みました。
これが流行る頃にはSwiftUIも使えるようになってるだろう…ということでSwiftUIでNeumorphismのライブラリを作ってみました。
Switch
などは作っていません。今後時間があれば作っていきたいですが、まだ形がコレと定まってもいないので難しそうですね。そもそも流行るのかもわかりませんし。完成形
ModifierでShadowを実装する
Modifierについてはこの記事でひと通りわかると思います。簡単にいうと
.font
とか.frame
とかをまとめてView
に適合できるやつです。Button
などの場合はButtonStyle
などが使えればいいのですが、Appleさんが公開していないので諦めます。
SwiftUIでもshodowは1つしか追加できません。なのでZStack
でView
を2つ重ねてそれぞれに.shadow
をつける必要があります。struct ConvexModifier: ViewModifier { let lightShadowColor: Color let darkShadowColor: Color func body(content: Content) -> some View { ZStack { content .shadow(color: darkShadowColor, radius: 16, x: 9, y: 9) content .shadow(color: lightShadowColor, radius: 16, x: -9, y: -9) } } }しかし、これだけでは影の色をいちいち入力する必要があります。
色をEnvironmentで伝搬する
SwiftUIでは
@Environment
を使うことでその子View
全てに値を伝えることができます。詳しくはこの記事を読んでください。
そしてこれは自作することもできます。Neumorphismでは基本的にView
に1色しか使わないためこれが非常に有効です。さらに、ConvexModifier
もbaseColor
を基準に影の色を決めればよくなります。自作する方法はこちらをご覧ください。struct BaseColorKey: EnvironmentKey { static let defaultValue: Color = .gray } extension EnvironmentValues { var baseColor: Color { get { self[BaseColorKey.self] } set { self[BaseColorKey.self] = newValue } } }さて、色を変換したいわけだけど…
パッと見、SwiftUIの
Color
からはrgbやhueや取得できそうにありません。が、優しきAppleさんは.description
を用意してくれていました。#C1D2EBFF
のようにカラーコードを返してくれます。ということでカラーコードからColorを生成(このリンクではUIColor)できるようにし、RGBとHSL、RGBとHSBの変換コードを用意します。
Color
から色情報を取れるとわかったので、早速lighterColor
を実装します。neumorphismPrimary(value:)
は下に出てくるFloatingTabView
のラベルなどで使われています。func getHSLA() -> (h: Double, s: Double, l: Double, a: Double) { let string = String(self.description.dropFirst()) let v = Int(string, radix: 16) ?? 0 let r = Double(v / Int(powf(256, 3)) % 256) / 255 let g = Double(v / Int(powf(256, 2)) % 256) / 255 let b = Double(v / Int(powf(256, 1)) % 256) / 255 let a = Double(v / Int(powf(256, 0)) % 256) / 255 let (h, s, l) = ColorTransformer.rgbToHsl(r: r, g: g, b: b) return (h, s, l, a) } func lighter(value: Double) -> Color { let (h, s, l, a) = getHSLA() let hsb = ColorTransformer.hslToHsb(h: h, s: s, l: l + value) return Color(hue: hsb.h, saturation: hsb.s, brightness: hsb.b, opacity: a) } func darker(value: Double) -> Color { let (h, s, l, a) = getHSLA() let hsb = ColorTransformer.hslToHsb(h: h, s: s, l: l - value) return Color(hue: hsb.h, saturation: hsb.s, brightness: hsb.b, opacity: a) } func primary(value: Double) -> Color { let (_, _, l, _) = getHSLA() return (l > 0.5) ? darker(value: value) : lighter(value: value) }struct ColorExtension_Previews: PreviewProvider { static var previews: some View { let color = Color(hex: "C1D2EB") return Group { ColorPreview(color) ColorPreview(color.lighter(value: 0.12)) ColorPreview(color.darker(value: 0.18)) } .previewLayout(.fixed(width: 200, height: 100)) } }ConvexModifierを完成させる
材料は揃ったので合わせてみましょう。
struct ConvexModifier: ViewModifier { @Environment(\.baseColor) var baseColor: Color func body(content: Content) -> some View { ZStack { content .shadow(color: baseColor.darkerColor(value: 0.18), radius: 16, x: 9, y: 9) content .shadow(color: baseColor.lighterColor(value: 0.12), radius: 16, x: -9, y: -9) } } }struct ConvexModifier_Previews: PreviewProvider { static var previews: some View { ZStack { Color(hex: "C1D2EB") .edgesIgnoringSafeArea(.all) Circle() .fill(Color(hex: "C1D2EB")) .modifier(ConvexModifier()) .frame(width: 300, height: 300) } .environment(\.baseColor, Color(hex: "C1D2EB")) } }
.environment
でbaseColor
を伝えるの忘れないようにしましょう。
いい感じですね!
environment
を使ったので当然struct ConvexModifier_Previews: PreviewProvider { static var previews: some View { ZStack { Color(hex: "C1D2EB") .edgesIgnoringSafeArea(.all) Circle() .fill(Color(hex: "C1D2EB")) .frame(width: 300, height: 300) .modifier(ConvexModifier()) // `environment`で影の色を変える .environment(\.baseColor, Color.red) } .environment(\.baseColor, Color(hex: "C1D2EB")) } }とできるだろうと思っていたのですができませんでした。
.red
の時は黒い影が出てきて、その他の場合は影がなくなりました。Color(hex:)
を使ったら行けたのでよくわかりません。Highlight時に表示を変えられるButtonも作ってみた
ハイライト時にNeumorphismではどうするのが正解なんだろうと考えるためにとりあえず作ってみたのですが、標準の
Button
でもいい気がします。個人的に標準Button
は ハイライト時に色が薄すぎる気がするのでこれを使ってます。押下時にボタンを小さくしたいときなどに使ってみてください。struct HighlightableButton<Label>: View where Label: View { private let action: () -> Void private let label: (Bool) -> Label public init( action: @escaping () -> Void, label: @escaping (Bool) -> Label ) { self.action = action self.label = label } @State private var isHighlighted = false public var body: some View { label(isHighlighted) .animation(.easeOut(duration: 0.05)) .gesture(DragGesture(minimumDistance: 0) .onChanged { _ in withAnimation { self.isHighlighted = true } } .onEnded { _ in self.action() withAnimation { self.isHighlighted = false } } ) } }struct ConvexModifier_HighlightableButton_ForPreviews: View { @State var isSelected = false var body: some View { HighlightableButton(action: { self.isSelected.toggle() }) { isH in Image(systemName: self.isSelected ? "house.fill" : "house") .resizable() .aspectRatio(contentMode: .fit) .frame(width: isH ? 54 : 60) .foregroundColor(Color(hex: "C1D2EB").darker(value: 0.18)) .background( Circle() .fill(Color(hex: "C1D2EB")) .frame(width: isH ? 90 : 100, height: isH ? 90 : 100) .modifier(ConvexModifier()) ) .opacity(isH ? 0.6 : 1) } } }
.modifier(ConvexModifier())
はButton
の中に書いてください。外に書くときちんと適用されません。ForPreviews
みたいな名前になっているのは、PreviewProvider
の中には@State
が置けないからです。
FloatingTabViewも作ってみた
@touyouもタブを作ってることだし作るかーみたいな感じで作ってみました。
ViewBuilder
を読み解こうとまでは思わなかったので、標準のTabView
のような綺麗さはありませんが、まあ使えるのではないでしょうか。タブの数は4つまでにしてみました。その他を実装する気力はなかったので切り捨てています。あとで多分何とかします。
使い方はこんな感じです。struct FloatingTabView_ForPreviews: View { enum Season: String, CaseIterable { case spring, summer, fall, winter var color: Color { switch self { case .spring: return .pink case .summer: return .blue case .fall: return .orange case .winter: return .white } } } @State var season: Season = .spring var body: some View { FloatingTabView(selection: $season, labelText: { s in s.rawValue }, labelImage: { _ in Image(systemName: "camera") }) { s in s.color.edgesIgnoringSafeArea(.all) } } }例を作るのが面倒なのが目に見えますね…。
labelImage
なんてカメラだけですし。この例では色を変えていますが、Neumorphismでは色を変えるのはご法度なので注意。(もちろん局所的にアクセントとして使うのはOKです)
ああ、影の色が汚い…。まあ色変えるとこうなるよっていう悪い例と思ってください。
ちなみにGeometryReader
のせいかLive PreviewにしていないとTabが下に落ちてしまいました。SwiftUIは7不思議どころじゃありません。そこら辺に穴が一杯です。全く理由がわからず何となくLive Previewにしてみたところ正しいことがわかりました。恐ろしや。凹も作りたかったけど
凹凸どちらも作りたかったのですが、凹の方はいい案が思い浮かばず、凸だけになってしまいました。適合したい
aView
よりひとまわり大きいbView
を作って、そこからaView
の大きさを切り抜いて、aView
の上にbView
を2つ置いて影をつければ行けそうだなとは思ったんですが、くり抜く方法がわかりませんでした。Path
を使えば何とかなりそうですが、View
の形を取る方法もないですし…。
あと、SwiftUI製ですし、全プラットフォームに対応させたかったのですが、macOSにSF Symbolsがなかったり、tvOSにDragGesture
がなかったりと面倒になってやめました() まあそこまで大変そうでもないのでいつかします。使う上での諸注意
HighlightableButton
のところでも書きましたが、Button
で使う際は.modifier(ConvexModifier())
をButton
の中に書いてください。そうしないときれいに作れません。
またBinding
系のコンポーネントは2つ同時にViewに存在すると使えなくなるようです。TextField("C1D2EB", text: $model.userInput) .foregroundColor(baseColor.nmPrimary(value: 1)) .padding(5) .background( RoundedRectangle(cornerRadius: 5, style: .continuous) .fill(baseColor) .modifier(NMConvexModifier(radius: 9)) )のように
background
に設定したView
に.modifier(ConvexModifier())
してください。View
の量的にもこちらの方がパフォーマンスもいいです。初OSS & 初Qiita?
SwiftUIなのでマルチプラットフォームに対応のOSSにしたかったわけですが、iOSのみとは作り方が違うようで。WWDCを参考に作らせていただきました。
Qiitaも初投稿ですが、そろそろ開発から離れて受験勉強しないと浪人する未来しか見えないので当分記事を書くことはないでしょう。1年後に戻って来れるように頑張ります。いいねとスターつけてくださると嬉しいです!
- 投稿日:2020-01-15T04:36:39+09:00
[Swift]?1ヶ月に使うトイレットペーパーの数を計算したい?
僕は快便なのでうんち?をよくします。調子が良い時だと12ロール入りを3週間未満で使い切ります?
そこで今回はSwiftを使って1ヶ月に使うトイレットペーパーの数を動的に計算したいと思います?
(ションベンは無いものとする)開発環境
macOS Catalina v.10.15.1
Xcode v.11.3
Swift v.5.1.3?Playground
計算するだけなので、Playgroundを使います。
まず、1ロールの長さと1ヶ月を定義します?
// 1ロールの長さはJIS規格で決まっているらしいです。今回は一般的な32.5mで計算します let length: Float = 32.5 // 1ヶ月を31日とする let month: Int = 31そして1日にうんこをする数を代入する変数をget,setプロパティで実装します!?
var PoopCount: Int { get { // 日本人が1回のトイレで使うトイレットペーパーの平均の長さ146cm(ググりました) let aboutOnceUse: Float = 1.46 // 1ロールでうんこできる回数 let oneRollOfPoop = length / aboutOnceUse // 22回うんこできる return Int(oneRollOfPoop) } set { // 1日で使うロールの長さ let daysOfRollLength = Float(newValue) * 1.46 // 1ロールでうんこできる日数 let oneRollOfDays = length / daysOfRollLength // 1ヶ月に必要なトイレットペーパー let totalRoll = month / Int(oneRollOfDays) // 4ロール必要 print("\(totalRoll)") } } // 1日3回うんこすると... PoopCount = 3これで1ヶ月に使うトイレットペーパーの数を計算出来ました!
それでは、getとsetプロパティについて説明します???get
get { // 日本人が1回のトイレで使うトイレットペーパーの平均の長さ146cm(ググりました) let aboutOnceUse: Float = 1.46 // 1ロールでうんこできる回数 let oneRollOfPoop = length / aboutOnceUse // 22回うんこできる return Int(oneRollOfPoop) }これは変数が実行される度にgetの処理が実行され、実行結果が返ってきます。
今回の場合だと、PoopCountがInt型なので、Int型にキャストして返してます。getは読み取り専用なので、今回のような指定した値から計算するだけなら必要ないです、ただ使いたかったので1ロールでうんこできる処理を書きました。?
?set
set { // 1日で使うロールの長さ let daysOfRollLength = Float(newValue) * 1.46 // 1ロールでうんこできる日数 let oneRollOfDays = length / daysOfRollLength // 1ヶ月に必要なトイレットペーパー let totalRoll = month / Int(oneRollOfDays) // 4ロール必要 print("\(totalRoll)") } } // 1日3回うんこすると... PoopCount = 3今回のメイン処理となります!
これは変数に代入された値を元に計算を始める処理を書きます!
じゃあその代入した値はどこいくねん??ってなるかと思いますが、setプロパティを使ってる変数に代入した値はnewValueに代入されます!こういう意味です?
var PoopCount: Int { get { // 日本人が1回のトイレで使うトイレットペーパーの平均の長さ146cm(ググりました) let aboutOnceUse: Float = 1.46 // 1ロールでうんこできる回数 let oneRollOfPoop = length / aboutOnceUse // 22回うんこできる return Int(oneRollOfPoop) } set { // 1日で使うロールの長さ ?ここに来る let daysOfRollLength = Float(newValue) * 1.46 // 1ロールでうんこできる日数 let oneRollOfDays = length / daysOfRollLength // 1ヶ月に必要なトイレットペーパー let totalRoll = month / Int(oneRollOfDays) // 4ロール必要 print("\(totalRoll)") } } // 1日3回うんこすると... PoopCount = 3 ?この値が?おまけ
他にもwillSetやdidSetを使って値の監視を出来ます?
?willset
willsetは元ある値よりも代入された値に対して処理を書くことが出来ます。setとほぼ一緒ですね?♂️
こんな感じ?
var PoopCount: Int = 1 { willSet { if newValue < 1 { print("便秘です、食物繊維を摂取しましょう?") } } } // 1日にうんこする回数? PoopCount = 0?didSet
didSetは代入された値よりも元ある値で処理を書くことが出来ます。
例えば変数の値が動的に変わるとします。その値はDBに保存されていて、ユーザーが前日入力した値が反映される実装をしていると仮定しましょう。
元ある値はoldValueに代入されます。こんな感じ?
var poop = DBの値を読み込ませる var PoopCount: Int = poop { willSet { if newValue < 1 { print("便秘です、食物繊維を摂取しましょう?") } else if newValue > 10 { print("病院行きましょう?") } else { print("良い感じ!!?") } } didSet { ?ここに前日のDBの値が反映される if oldValue > 10 { print("病院行きましたか?") } else if oldValue < 1 { print("昨日はうんちしてません?") } else { print("快便や?") } } } PoopCount = 2こんな感じで前日と比較したうんち記録を作成することが出来ます?
?まとめ?
どうでしたでしょうか?文章を読むだけだと理解しずらいと思うので、実際にPlaygroundで試してみてください!
きっと理解できるはずです?それにしても、一人暮らしだとトイレットペーパーってあまり使わないんですね、俺はトイレットペーパー を使いすぎなのかな笑
- 投稿日:2020-01-15T03:56:53+09:00
SwiftUI 実践 tips 集
はじめに
MIHO というキャラボイスアプリを SwiftUI と Combine を用いて開発しました。互換性の欄が iOS13 以降となっていることが確認できるかと思います(証拠とは言えませんが)。
このアプリを開発するにあたって学んだことをここにまとめたいと思います。TapGesture & ContentShape
List
を用いた UI で、セルをタップすると別の View に遷移する処理は公式のチュートリアルでも確認できます。非常にシンプルです。
では、「セルをタップしたときに音声を再生させる」のように、セルをタップしたときに独自の処理を走らせる場合はどうしたら良いでしょうか。解決方法としてはButton
を使う方法とonTapGesture
を使う方法が考えられます。
今回はonTapGesture
を使う場合を紹介します。List(VoiceModel.allCases) { item in VoiceRowView(voice: item) .onTapGesture { viewModel.action() } }使い方は上記の通り、非常にシンプルです。
struct VoiceRowView: View { var body: some View { HStack(spacing: 8) { Image("image") Text("Apple") Spacer() } } }次に、そのセルのレイアウトが上記のように
Spacer
を用いた実装になっていたとします。
この場合、左から画像・文字列と並び、右側には空白ができることになります(もちろん文字数が多い場合は右端まで埋まる)。ここで勘がいい方は気がつくかもしれませんが、この右側の空白部分ではタップ反応を受け取ることができません。List(VoiceModel.allCases) { item in VoiceRowView(voice: item) .contentShape(Rectangle()) .onTapGesture { self.playView.viewModel.prepare(item: item) } }
onTapGesture
を使うという条件下で、この問題の解決方法は上記のようになります。.contentShape(Rectangle())
と指定することでセルの形を長方形であると定義します。こうすることによって空白があったとしてもセル全体でアクションを受け取ることが可能になります。参考文献:
How to read tap and double-tap gestures
SwiftUI can't tap in Spacer of HStackGeometryReader
View のサイズを画面サイズから計算して指定したい場合があると思います。今回は、 16:9 の View を作成する例とともにご紹介します。
var body: some View { GeometryReader { geometry in AVPlayerView(bundleDataName: "header") .frame(width: geometry.size.width, height: (9 * geometry.size.width) / 16) } }上記が UI 設計例になります。
GeometryReader
を用いることで その View の width と height を取得することが可能です。 ただ、これが端末の画面サイズを示すものではないことに注意してください(Safe Area のことも意識してあげてくださいね)。参考文献:
How to provide relative sizes using GeometryReaderSheet & Alert
画面遷移でプッシュ遷移は
List
を用いることでシンプルに実装が可能です。もう一つの遷移方法として存在する「モーダル遷移」を実現できるのがsheet
です。ここでは、先程も説明したTapGesture
とともに実装する例でご紹介します。@State private var isShowDetail: Bool = false var body: some View { GeometryReader { geometry in ... } .onTapGesture { self.isShowDetail = true } .sheet(isPresented: $isShowDetail) { VoiceDetailView() } }様々な実装パターンが考えられますが、上記のような処理でモーダル遷移が実現できます。仕組みとしては、まず画面遷移を行うかどうかを示すフラグを宣言しておき、
Button
やTapGesture
によるアクションキャッチ時にフラグを書き換えてあげます。そして、そのフラグの変化を検知したsheet
が画面遷移を行うといった動きになります。@State private var isShowAlert: Bool = false var body: some View { GeometryReader { geometry in ... } .onTapGesture { self.isShowAlert = true } .alert(isPresented: $isShowAlert) { Alert( title: Text("Title"), message: Text("message"), dismissButton: .default(Text("OK")) ) } }ついでに Alert の表示方法も同じ考え方なので載せておきます。非常にシンプルですね。
*Representable
私の GitHub や Qiita 記事、勉強会資料などで何度も登場してきた *Representable ですが、今回はちょっと実践的な内容となります。
今回は SwiftUI にはないパーツであるUIProgressView
を SwiftUI に対応させる例でご紹介します。struct ProgressView: UIViewRepresentable { @Binding var progress: Float var progressTintColor: UIColor func makeUIView(context: Context) -> UIProgressView { return UIProgressView().apply { this in this.progressTintColor = progressTintColor } } func updateUIView(_ uiView: UIProgressView, context: Context) { uiView.setProgress(progress, animated: true) } }これが実装例になります。
UIProgressView
はprogress
の値を変化させてあげなければいけません。そのため、単純にUIViewRepresentable
に準拠させてあげるだけでは動作してくれません。
そこで、@Binding
を用いて外部の値変化を監視できるようにしています。#if DEBUG struct ProgressViewPreviews: PreviewProvider { static var previews: some View { ProgressView(progress: .constant(0.5), progressTintColor: .blue) .previewLayout(.fixed(width: 300, height: 10)) } } #endifただ、先程作成した
ProgressView
をプレビュー可能にするとき、困ることがあります。@Binding var progress: Float
に渡す初期値です。対応方法は上記実装例にもあるように.constant()
に値をセットしたものを指定するという感じになります。参考文献:
How to use UIKit in SwiftUI
SwiftUI @Binding Initializeさいごに
説明間違えやアドバイス、ご指摘などありましたら遠慮なくコメントいただけると嬉しいです!
最後まで読んでいただき、ありがとうございます!!