- 投稿日:2020-01-12T22:26:53+09:00
わいわいswiftc ワークショップ Vol.3 福岡 事前準備
コレ何
https://iosdiscord.connpass.com/event/151576/ の資料です
はじめに
このドキュメントは、わいわいswiftc ワークショップ Vol.3 福岡の事前準備のドキュメントです。
Swiftコンパイラというマンモスプロジェクトをビルドするので、これをやらないと当日ワークショップに参加しても作業できないのでご注意ください。コンパイラのビルドにかかなり時間を要するので、寝てる間にビルドを進めるのがおすすめです。
5/16: Publicにしました。が課題レポジトリは消えてるので注意。
1/14: 必要なものを更新しました。Python2系じゃないとcloneスクリプトとビルドスクリプトは動きません。
1/20: 手順を更新しました。cmakeのバージョン3.15.1をビルドしないといけないです必要なもの
- Xcode 11.x (動作確認をしているので11.3が望ましい)
- ninja (最新版に上げていると良い, Homebrewなどでインストール可能です)
- cmake 3.15.1 (3.16 以降だと動かないです。cmakeを個別でビルドする方法を下記に掲載してあるので、そちらを参照してください)
- 55GB程度の空きStorage
- Python 2系
- ※ 現在入ってるPythonが3系の場合、pyenv等のPythonバージョン管理ツールでpython2を入れることをおすすめします。
準備手順
レポジトリをCloneする
Terminalを開いて、ホームディレクトリでも好きなところで
waiwai-workshop-3rd
というディレクトリを作ってください。
※waiwai-workshop-3rd
じゃなくてもいいです。$ cd ~ # 例ではホームディレクトリですが、どこでも良いです $ mkdir waiwai-workshop-3rd
次に、Swiftコンパイラをgithubから削除しました。waiwai-workshop-3rd
にクローンします。(https://github.com/freddi-kit/waiwai-swiftc-silopt-workshop~)今回は、ワークショップ向けにカスタマイズしたSwiftコンパイラを利用します。
必ずクローンしたものはswiftというディレクトリにリネームしてください。$ cd waiwai-workshop-3rd $ git clone git@github.com:freddi-kit/waiwai-swiftc-silopt-workshop.git swift $ cd swift以下、
swift
ディレクトリにいる想定で書きます。依存してるプロジェクトをクローンする
次に、Swiftコンパイラに依存しているプロジェクトをクローンします。swiftディレクトリにスクリプトがあるので、以下のコマンドを叩いてください。
# swift ディレクトリにて $ utils/update-checkout --clone※ 設定では各プロジェクトのクローンするブランチを意図的に変えています。
※ 前のセクションでswift
にリネームする作業を怠っていると、ここらへんからつまづきます。一応ブランチを確認して、
waiwai-question
になっているかを確認してください。# swift ディレクトリにて $ git branch * waiwai-questioncmake version 3.15.1をビルドする
cmakeが必要なのですが、cmakeのバージョンが 3.15.1でないとビルドスクリプトが正常に動かないです。
なので、cmakeを個別にビルドしてそれをビルドスクリプトに利用します。先程の
依存してるプロジェクトをクローンする
にて、cmakeはclone済みなので、そちらに移動します。# swift ディレクトリにて $ cd ../cmakeそして下記のコマンドを叩いてください。
# cmake ディレクトリにて $ ./bootstrap && make暫く待つと、
./bin/
以下にcmakeがビルドされています。
途中でjava関係のポップアップが出る可能性がありますが、無視してOK
を押してください。※ わかる人向け:
make install
しないでください。ビルドが終わったら、swiftディレクトリに戻りましょう。
# cmake ディレクトリにて $ cd ../swift # もといたswiftディレクトリに戻るコンパイラをビルドする
最後に、ビルドスクリプトを使ってビルドをします。
--cmake
オプションで先程ビルドしたcmakeを指定します。# swift ディレクトリにて $ utils/build-script --debug --xcode --skip-build-benchmarks --cmake ../cmake/bin/cmakeワークショップではこれで生成されたXcode Projectを使って、Swiftコンパイラのコードを書きかえていきます。
かなり時間がかかるので注意してください。おすすめは寝る前にやって朝起きたらもう準備できている状態。
※Ctrl+C
とかでビルドを中断すると、最悪ビルドしたコンパイラがちゃんと動かなくなる可能性があるので時間があってかつマシンパワーに余力があるときに行いましょうもしコンパイラのビルド失敗したら?
もしビルド等に失敗したら、
- 最新のレポジトリの状態をcloneして最初からやり直してみる
- 使っている環境とは別のpython 2系の別環境をpyenvで導入してやってみてください。
- それでもだめであれば、エラーログを書いて質問してください
ただし、下記の
docs_html
での失敗は、ドキュメントの生成が失敗してるだけでXcode Projectが正常に生成されてある可能性が高いので無視して次のステップに行ってみてください。=== BUILD AGGREGATE TARGET docs_html OF PROJECT Swift WITH CONFIGURATION Debug === Check dependencies # ~~~~~~~~~中略~~~~~~~~~ ** BUILD FAILED ** The following build commands failed: PhaseScriptExecution CMake\ Rules /パス/waiwai-workshop-3rd/build/Xcode-DebugAssert/swift-macosx-x86_64/docs/Swift.build/Debug/docs_html.build/Script-225217FDFF4641299E9F0141.sh (1 failure) utils/build-script: fatal error: command terminated with a non-zero exit status 65, abortingXcode Projectを開く
では、Xcode Projectを開いていきます。Xcodeは
swift
ディレクトリから見て次の場所にあるのでopenコマンドなどを使って開きましょう。# swift ディレクトリにて $ open ../build/Xcode-DebugAssert/swift-macosx-x86_64/Swift.xcodeproj/最初Xcode Projectを開くと、「targetが多すぎるのでschemeどうするか」という旨のポップアップが出ますが、targetは以降の作業で指定するので「Manual」っぽいことを書いてある選択肢を選んでください。
このポップアップはCloseで大丈夫です。
Xcode Projectに
sil-opt
Schemeを追加するでは、そのSchemeを追加します。今回のワークショップでは
sil-opt
コマンドを使うので、それをビルドするSchemeを追加します。プルダウンが出てくるので、
New Scheme
を選択します。
ここで、どのtargetでSchemeを作るかが出てきます。まず、
target
を選択します。
このような大量のリストが出てくるので、
sil-opt
を探して選択してください。キーボードでなにかしら文字を入力をすると絞り込み検索ができます。図では、sil-opt
で絞り込みをした結果です。
※ 絞り込みをしてもだいぶスクロールして探さないと見つからないので注意してください。
sil-opt
Schemeを編集して、コマンドラインからデバッグできるようにする次に、コマンドラインから
sil-opt
コマンドを叩いても、Xcodeで設定したBreak Pointなどが働くようにします。さっきのスキームをクリックして、
Edit Scheme
を選択します。
設定ポップアップが出るので、左の
Run
という項目を選択して、Info
タブのLaunch
という項目にある、Wait for executable to be launched
にチェックしてください。
Close
して、設定はおわりです。ここまでお疲れさまでした。?設定がうまく行っているかテストする
では、実際に
sil-opt
がうまく準備してあるかをテストしましょう。では実際にSILOptimizerのコードを書き換えます。
Command + Shift + o
でプロジェクト内検索を開き、WaiWaiOptimizer
と入力します。すると、WaiWaiOptimizer.cpp
が見つかるので、それを開きます。
以下のようなコードが出てくると思います。
WaiWaiOptimizer.cpp#include "swift/SILOptimizer/PassManager/Passes.h" #include "swift/SIL/SILFunction.h" #include "swift/SIL/SILInstruction.h" #include "swift/SIL/SILModule.h" #include "swift/SILOptimizer/Utils/Local.h" #include "swift/SILOptimizer/PassManager/Transforms.h" #include "llvm/Support/CommandLine.h" #include <iostream> using namespace swift; using namespace std; namespace { class WaiWaiOptimizer : public swift::SILFunctionTransform { /// The entry point to the transformation. void run() override { } }; } SILTransform *swift::createWaiWaiOptimizer() { return new WaiWaiOptimizer(); }これはC++のコードです。今回のワークショップはこのコードを使った課題があるので、実際に事前に触ってみましょう。
run()
関数の中身を以下のように変更してみます。cout
はSwiftでの
endl
というのが文末にありますが、これがないと改行されません。WaiWaiOptimizer.cppの16行目らへん... void run() override { cout << "Hello, Optimizer!" << endl; } ...変更後はこんなコードになりますね。
WaiWaiOptimizer.cpp#include "swift/SILOptimizer/PassManager/Passes.h" #include "swift/SIL/SILFunction.h" #include "swift/SIL/SILInstruction.h" #include "swift/SIL/SILModule.h" #include "swift/SILOptimizer/Utils/Local.h" #include "swift/SILOptimizer/PassManager/Transforms.h" #include "llvm/Support/CommandLine.h" #include <iostream> using namespace swift; using namespace std; namespace { class WaiWaiOptimizer : public swift::SILFunctionTransform { /// The entry point to the transformation. void run() override { cout << "Hello, Optimizer!" << endl; } }; } SILTransform *swift::createWaiWaiOptimizer() { return new WaiWaiOptimizer(); }このコードを必要な部分だけ少しだけ解説します。
Swiftを書いたことがあるならチョット読めると思いますが、
WaiWaiOptimizer
というクラスはSILFunctionTransform
を継承しています。WaiWaiOptimizerクラスの宣言部class WaiWaiOptimizer : public swift::SILFunctionTransform {これは、「
WaiWaiOptimizer
というOptimizerのPassがあって、これは関数を最適化(Optimize)するPassである」という意味になります。Passとはなにかについては詳しくはワークショップで話しますが、SILをOptimizeするモジュールと考えてください。SwiftコンパイラはSILOptimizerというフェーズでコードを最適化しますが、Passと呼ばれるモジュールがそれぞれ目的の最適化を行います。
また、Passの最適化のアルゴリズムは
run
関数に書いていきます。今回は、Hello, Optimizer!
と画面に表示するだけで何も最適化しないPassを作りました。では、試しにこのPassを読んだら本当に
Hello, Optimizer!
と出るかを確かめてみます。
その前に、さっき追加したcout
のところにBreak Pointをつけたら動くかも確かめてみましょう。
ここまで来たら、
Command + R
を押して、ビルドと実行をします。時間はそこまでかからないです。
Runをする準備ができたら、Wainting to attach to sil-opt : sil-opt
と出るので、コマンドライン上からsil-opt
起動してみます。
その前に、
sil-opt
で最適化するためのSILコードを用意します。適当にそこまで複雑じゃないコードのSILコードを以下のコマンドを叩いて用意してください。$ swiftc example.swift -emit-silgen -o example.sil先程の
swift
ディレクトリの目線に戻ります。ビルドされた
sil-opt
コマンドは、../build/Xcode-DebugAssert/swift-macosx-x86_64/Debug/bin/
にあるので、そこにあるsil-opt
コマンドを以下のように叩きます。
sil-opt
に-wai-wai-optimizer
オプションを付けると、先程のWaiWaiOptimizer
が呼ばれます。# swift ディレクトリにて $ ../build/Xcode-DebugAssert/swift-macosx-x86_64/Debug/bin/sil-opt -wai-wai-optimizer さっき生成したSILのあるディレクトリ/example.sil -o optimized-example.sil下記のように
Hello, Optimizer!
がいくつか出力されて、かつBreak Pointが働いたら成功です。# swift ディレクトリにて $ ../build/Xcode-DebugAssert/swift-macosx-x86_64/Debug/bin/sil-opt -wai-wai-optimizer さっき生成したSILのあるディレクトリ/example.sil -o optimized-example.sil Hello, Optimizer! Hello, Optimizer! Hello, Optimizer! ... # SILのコードによってHello, Optimizer!の数が違うので注意成果物は、
optimized-example.sil
として出力されます。しかし、WaiWaiOptimizer
のrun
関数には最適化するものを何も書いてないので、元のexample.sil
と比較しても特に目立った変更ははありません。おわり
ここまで来たら、準備は完了です。ワークショップ当日に問題をPushしますので、それをPullして解いていくことになります。
お疲れさまでした、当日は頑張ってください!皆様のご来場お待ちしております〜!
追記事項
ワークショップ本体のリンク載せておきます。
https://qiita.com/freddi_/items/43130e5f2a6e9d33b0ca
https://qiita.com/freddi_/items/9690f53e267855ffe4b7
https://qiita.com/freddi_/items/38df6bd64904b5ccaa73
- 投稿日:2020-01-12T21:24:39+09:00
【swift】PageViewControllerとcontainerViewを用いたページ遷移機能に関して
前置き
スワイプによるページ遷移、ボタン押下によるページ遷移、までは実装できたものの、スワイプによるページ遷移時のtopviewのデザインの変更ができないとつまずいた人間の、備忘録。
環境
xcode 11.3
swift 5.1.3実現したい機能
- スワイプでページ遷移 : 完了
- ボタン押下でページ遷移 : 完了
- ページ遷移に従い、ページ遷移しない部分のデザインの変更 : これ!!!
現状
ファイル構成
- TopViewController.swift <- 元となるViewController
- PageViewController.swift <- page遷移機能をまとめたViewController
- View1Controller.swift
- View2Controller.swift
- View3Controller.swift
storybord
- 元となるViewControllerが存在
- その中に、3つのボタンと、ページのタイトルラベル、そしてcontainerViewが存在
- containerViewの先はpageViewControllerとなっている。
- 遷移先のページとして、viewControllrが3つ(view1,view2,view3)存在している。
- 各viewには、それぞれtopView,view1,view2,view3のstorybordIDをつけている
コード
TopViewController.swiftimport UIKit class TopViewController: UIViewController { // pageのタイトル @IBOutlet weak var pageTitle: UILabel! // buttons @IBOutlet weak var toView1Btn: boundButton! @IBOutlet weak var toView2Btn: boundButton! @IBOutlet weak var toView3Btn: boundButton! // toView1Btn押下時 @IBAction func toView1Action(_ sender: Any) { let vc = storyboard?.instantiateViewController(withIdentifier: "view1") as! View1Controller pageViewController!.setViewControllers([vc], direction: .forward, animated: false, completion: nil) toView1Design() } // toView2Btn押下時 @IBAction func toView2Action(_ sender: Any) { let vc = storyboard?.instantiateViewController(withIdentifier: "view2") as! View2Controller pageViewController!.setViewControllers([vc], direction: .reverse, animated: false, completion: nil) toView2Design() } // toView3Btn押下時 @IBAction func toView3Action(_ sender: Any) { let vc = storyboard?.instantiateViewController(withIdentifier: "view3") as! View3Controller pageViewController!.setViewControllers([vc], direction: .reverse, animated: false, completion: nil) toView3Design() } // 画面遷移用 var pageViewController: UIPageViewController? override func viewDidLoad() { super.viewDidLoad() // 最初はview1を出しとく toView1Design() } } // デザイン変更用関数のまとめ extension TopViewController { // view1に遷移するとき呼ばれる func toView1Design() { // pageTitle変更 pageTitle.text = "view1" // ボタンの色変更(選ばれているボタンを赤色に、それ以外は灰色に) toView1Btn.setTitleColor(UIColor.red, for: []) toView2Btn.setTitleColor(UIColor.lightGray, for: []) toView3Btn.setTitleColor(UIColor.lightGray, for: []) } // view2に遷移するとき呼ばれる func toView2Design() { // pageTitle変更 pageTitle.text = "view2" // ボタンの色変更(選ばれているボタンを赤色に、それ以外は灰色に) toView1Btn.setTitleColor(UIColor.lightGray, for: []) toView2Btn.setTitleColor(UIColor.red, for: []) toView3Btn.setTitleColor(UIColor.lightGray, for: []) } // view3に遷移するとき呼ばれる func toView3Design() { // pageTitle変更 pageTitle.text = "view3" // ボタンの色変更(選ばれているボタンを赤色に、それ以外は灰色に) toView1Btn.setTitleColor(UIColor.lightGray, for: []) toView2Btn.setTitleColor(UIColor.lightGray, for: []) toView3Btn.setTitleColor(UIColor.red, for: []) } }PageViewController.swiftimport UIKit class PageViewController: UIPageViewController { override func viewDidLoad() { super.viewDidLoad() self.setViewControllers([getView1()], direction: .forward, animated: true, completion: nil) self.dataSource = self } // それぞれ、viewControllerを返す関数 func getView1() -> View1Controller { return storyboard!.instantiateViewController(withIdentifier: "view1") as! View1Controller } func getView2() -> View2Controller { return storyboard!.instantiateViewController(withIdentifier: "view2") as! View2Controller } func getView3() -> View3Controller { return storyboard!.instantiateViewController(withIdentifier: "view3") as! View3Controller } func getTop() -> TopViewController { return storyboard!.instantiateViewController(withIdentifier: "topView") as! TopViewController } } //datasource用の関数まとめ extension PageViewController : UIPageViewControllerDataSource { // 現在表示されているページの、Beforeに位置する、つまり左側に位置するviewを呼び出す関数 func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { print(viewController) if viewController.isKind(of : View3Controller.self) { // 3の時 -> 2 return getView2() } if viewController.isKind(of : RepoViewController.self) { // 2の時 -> 1 return getView1() } if viewController.isKind(of : CalendarViewController.self) { // 1の時 -> 3 return getView3() } return nil } // 現在表示されているページの、Afterに位置する、つまり右側に位置するviewを呼び出す関数 func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { print(viewController) if viewController.isKind(of : CalendarViewController.self) { // 1の時 -> 2 return getView2() } if viewController.isKind(of : RepoViewController.self) { // 2の時 -> 3 return getView3() } if viewController.isKind(of : UserViewController.self) { // 3の時 -> 1 return getView1() } return nil } }課題
スワイプによるページ遷移に伴うTopViewController上のデザインの変更メソッドを呼ぶ場所がわからない。
datasource用の関数が呼ばれるタイミングは、現在表示されているviewcontrollerとずれている。解決策
検索結果
調べた結果、delegateメソッドでスワイプしたタイミングを検知してくれるメソッドがあるらしい。これを使う。
UIPageViewControllerでページ遷移時に処理したいことを書くときのTip
Apple Developer : pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:)実装
PageViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() self.setViewControllers([getView1()], direction: .forward, animated: true, completion: nil) self.dataSource = self // ここから追記 self.delegate = self // ここまで追記 } // 以下を追記 extension PageViewController : UIPageViewControllerDelegate{ // スワイプによるページ遷移が行われたときに呼ばれるメソッド func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool){ // 現在のviewcontrollerを取得 let currentVC = pageViewController.viewControllers![0] // topViewControllerを取得 let topView = getTop() // デザインを変更処理 if currentVC!.isKind(of : View1Controller.self) { topView.toView1Design() } if currentVC!.isKind(of : View2Controller.self) { topView.toView2Design() } if currentVC!.isKind(of : View3Controller.self) { topView.toView3Design() } } }課題2
しかしながらこのコードでは、デザイン変更関数が呼ばれた時に、toView1Btnがない、と言ったエラーが発生する。
pageViewController上でデザイン変更関数のみを呼び出しており、TopViewController上で行われている変数設定等を行っていないからだ。解決策2
検索結果2
どうやら、pageVioewControllerをTopViewController内部で設定する方法もあるらしい。
この方法を用いれば、上記のエラーも発生しなくなるはず。qiita : 【Swift】UIPageViewControllerとContainerViewを利用してタップで画面遷移
実装2
PageViewController.swiftを削除し、TopViewController.swiftに書き足し。
TopViewController.swiftimport UIKit class TopViewController: UIViewController { // pageのタイトル @IBOutlet weak var pageTitle: UILabel! // buttons @IBOutlet weak var toView1Btn: boundButton! @IBOutlet weak var toView2Btn: boundButton! @IBOutlet weak var toView3Btn: boundButton! // toView1Btn押下時 @IBAction func toView1Action(_ sender: Any) { let vc = storyboard?.instantiateViewController(withIdentifier: "view1") as! View1Controller pageViewController!.setViewControllers([vc], direction: .forward, animated: false, completion: nil) toView1Design() } // toView2Btn押下時 @IBAction func toView2Action(_ sender: Any) { let vc = storyboard?.instantiateViewController(withIdentifier: "view2") as! View2Controller pageViewController!.setViewControllers([vc], direction: .reverse, animated: false, completion: nil) toView2Design() } // toView3Btn押下時 @IBAction func toView3Action(_ sender: Any) { let vc = storyboard?.instantiateViewController(withIdentifier: "view3") as! View3Controller pageViewController!.setViewControllers([vc], direction: .reverse, animated: false, completion: nil) toView3Design() } // 画面遷移用 var pageViewController: UIPageViewController? override func viewDidLoad() { super.viewDidLoad() // ここから追加 // PageViewControllerの設定 pageViewController = children.first! as? UIPageViewController pageViewController!.setViewControllers([getView1()], direction: .forward, animated: true, completion: nil) pageViewController!.dataSource = self pageViewController!.delegate = self // ここまで追加 // 最初はview1を出しとく toView1Design() } } // デザイン変更用関数のまとめ extension TopViewController { // view1に遷移するとき呼ばれる func toView1Design() { // pageTitle変更 pageTitle.text = "view1" // ボタンの色変更(選ばれているボタンを赤色に、それ以外は灰色に) toView1Btn.setTitleColor(UIColor.red, for: []) toView2Btn.setTitleColor(UIColor.lightGray, for: []) toView3Btn.setTitleColor(UIColor.lightGray, for: []) } // view2に遷移するとき呼ばれる func toView2Design() { // pageTitle変更 pageTitle.text = "view2" // ボタンの色変更(選ばれているボタンを赤色に、それ以外は灰色に) toView1Btn.setTitleColor(UIColor.lightGray, for: []) toView2Btn.setTitleColor(UIColor.red, for: []) toView3Btn.setTitleColor(UIColor.lightGray, for: []) } // view3に遷移するとき呼ばれる func toView3Design() { // pageTitle変更 pageTitle.text = "view3" // ボタンの色変更(選ばれているボタンを赤色に、それ以外は灰色に) toView1Btn.setTitleColor(UIColor.lightGray, for: []) toView2Btn.setTitleColor(UIColor.lightGray, for: []) toView3Btn.setTitleColor(UIColor.red, for: []) } } // ここから下全部追記 // view取得系の関数まとめ extension TopViewController { // それぞれ、viewControllerを返す関数 func getView1() -> View1Controller { return storyboard!.instantiateViewController(withIdentifier: "view1") as! View1Controller } func getView2() -> View2Controller { return storyboard!.instantiateViewController(withIdentifier: "view2") as! View2Controller } func getView3() -> View3Controller { return storyboard!.instantiateViewController(withIdentifier: "view3") as! View3Controller } func getTop() -> TopViewController { return storyboard!.instantiateViewController(withIdentifier: "topView") as! TopViewController } } // datasource用の関数まとめ extension TopViewController : UIPageViewControllerDataSource { // 現在表示されているページの、Beforeに位置する、つまり左側に位置するviewを呼び出す関数 func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { print(viewController) if viewController.isKind(of : View3Controller.self) { // 3の時 -> 2 return getView2() } if viewController.isKind(of : RepoViewController.self) { // 2の時 -> 1 return getView1() } if viewController.isKind(of : CalendarViewController.self) { // 1の時 -> 3 return getView3() } return nil } // 現在表示されているページの、Afterに位置する、つまり右側に位置するviewを呼び出す関数 func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { print(viewController) if viewController.isKind(of : CalendarViewController.self) { // 1の時 -> 2 return getView2() } if viewController.isKind(of : RepoViewController.self) { // 2の時 -> 3 return getView3() } if viewController.isKind(of : UserViewController.self) { // 3の時 -> 1 return getView1() } return nil } } // delegate用の関数まとめ extension TopViewController : UIPageViewControllerDelegate{ // スワイプによるページ遷移が行われたときに呼ばれるメソッド func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool){ // 現在のviewcontrollerを取得 let currentVC = pageViewController.viewControllers?.first! if currentVC!.isKind(of : View1Controller.self) { toView1Design() } if currentVC!.isKind(of : View2Controller.self) { toView2Design() } if currentVC!.isKind(of : View3Controller.self) { toView3Design() } } }
- 投稿日:2020-01-12T21:24:39+09:00
【swift】ボタンタップ&スワイプによるページ遷移機能の実装
前置き
スワイプによるページ遷移、ボタン押下によるページ遷移、までは実装できたものの、スワイプによるページ遷移時のtopviewのデザインの変更ができないとつまずいた人間の、備忘録。
環境
xcode 11.3
swift 5.1.3実現したい機能
- スワイプでページ遷移 : 完了
- ボタン押下でページ遷移 : 完了
- ページ遷移に従い、ページ遷移しない部分のデザインの変更 : これ!!!
現状
ファイル構成
- TopViewController.swift <- 元となるViewController
- PageViewController.swift <- page遷移機能をまとめたViewController
- View1Controller.swift
- View2Controller.swift
- View3Controller.swift
storybord
- 元となるViewControllerが存在
- その中に、3つのボタンと、ページのタイトルラベル、そしてcontainerViewが存在
- containerViewの先はpageViewControllerとなっている。
- 遷移先のページとして、viewControllrが3つ(view1,view2,view3)存在している。
- 各viewには、それぞれtopView,view1,view2,view3のstorybordIDをつけている
コード
TopViewController.swiftimport UIKit class TopViewController: UIViewController { // pageのタイトル @IBOutlet weak var pageTitle: UILabel! // buttons @IBOutlet weak var toView1Btn: boundButton! @IBOutlet weak var toView2Btn: boundButton! @IBOutlet weak var toView3Btn: boundButton! // toView1Btn押下時 @IBAction func toView1Action(_ sender: Any) { let vc = storyboard?.instantiateViewController(withIdentifier: "view1") as! View1Controller pageViewController!.setViewControllers([vc], direction: .forward, animated: false, completion: nil) toView1Design() } // toView2Btn押下時 @IBAction func toView2Action(_ sender: Any) { let vc = storyboard?.instantiateViewController(withIdentifier: "view2") as! View2Controller pageViewController!.setViewControllers([vc], direction: .reverse, animated: false, completion: nil) toView2Design() } // toView3Btn押下時 @IBAction func toView3Action(_ sender: Any) { let vc = storyboard?.instantiateViewController(withIdentifier: "view3") as! View3Controller pageViewController!.setViewControllers([vc], direction: .reverse, animated: false, completion: nil) toView3Design() } // 画面遷移用 var pageViewController: UIPageViewController? override func viewDidLoad() { super.viewDidLoad() // 最初はview1を出しとく toView1Design() } } // デザイン変更用関数のまとめ extension TopViewController { // view1に遷移するとき呼ばれる func toView1Design() { // pageTitle変更 pageTitle.text = "view1" // ボタンの色変更(選ばれているボタンを赤色に、それ以外は灰色に) toView1Btn.setTitleColor(UIColor.red, for: []) toView2Btn.setTitleColor(UIColor.lightGray, for: []) toView3Btn.setTitleColor(UIColor.lightGray, for: []) } // view2に遷移するとき呼ばれる func toView2Design() { // pageTitle変更 pageTitle.text = "view2" // ボタンの色変更(選ばれているボタンを赤色に、それ以外は灰色に) toView1Btn.setTitleColor(UIColor.lightGray, for: []) toView2Btn.setTitleColor(UIColor.red, for: []) toView3Btn.setTitleColor(UIColor.lightGray, for: []) } // view3に遷移するとき呼ばれる func toView3Design() { // pageTitle変更 pageTitle.text = "view3" // ボタンの色変更(選ばれているボタンを赤色に、それ以外は灰色に) toView1Btn.setTitleColor(UIColor.lightGray, for: []) toView2Btn.setTitleColor(UIColor.lightGray, for: []) toView3Btn.setTitleColor(UIColor.red, for: []) } }PageViewController.swiftimport UIKit class PageViewController: UIPageViewController { override func viewDidLoad() { super.viewDidLoad() self.setViewControllers([getView1()], direction: .forward, animated: true, completion: nil) self.dataSource = self } // それぞれ、viewControllerを返す関数 func getView1() -> View1Controller { return storyboard!.instantiateViewController(withIdentifier: "view1") as! View1Controller } func getView2() -> View2Controller { return storyboard!.instantiateViewController(withIdentifier: "view2") as! View2Controller } func getView3() -> View3Controller { return storyboard!.instantiateViewController(withIdentifier: "view3") as! View3Controller } func getTop() -> TopViewController { return storyboard!.instantiateViewController(withIdentifier: "topView") as! TopViewController } } //datasource用の関数まとめ extension PageViewController : UIPageViewControllerDataSource { // 現在表示されているページの、Beforeに位置する、つまり左側に位置するviewを呼び出す関数 func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { print(viewController) if viewController.isKind(of : View3Controller.self) { // 3の時 -> 2 return getView2() } if viewController.isKind(of : RepoViewController.self) { // 2の時 -> 1 return getView1() } if viewController.isKind(of : CalendarViewController.self) { // 1の時 -> 3 return getView3() } return nil } // 現在表示されているページの、Afterに位置する、つまり右側に位置するviewを呼び出す関数 func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { print(viewController) if viewController.isKind(of : CalendarViewController.self) { // 1の時 -> 2 return getView2() } if viewController.isKind(of : RepoViewController.self) { // 2の時 -> 3 return getView3() } if viewController.isKind(of : UserViewController.self) { // 3の時 -> 1 return getView1() } return nil } }課題
スワイプによるページ遷移に伴うTopViewController上のデザインの変更メソッドを呼ぶ場所がわからない。
datasource用の関数が呼ばれるタイミングは、現在表示されているviewcontrollerとずれている。解決策
検索結果
調べた結果、delegateメソッドでスワイプしたタイミングを検知してくれるメソッドがあるらしい。これを使う。
UIPageViewControllerでページ遷移時に処理したいことを書くときのTip
Apple Developer : pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:)実装
PageViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() self.setViewControllers([getView1()], direction: .forward, animated: true, completion: nil) self.dataSource = self // ここから追記 self.delegate = self // ここまで追記 } // 以下を追記 extension PageViewController : UIPageViewControllerDelegate{ // スワイプによるページ遷移が行われたときに呼ばれるメソッド func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool){ // 現在のviewcontrollerを取得 let currentVC = pageViewController.viewControllers![0] // topViewControllerを取得 let topView = getTop() // デザインを変更処理 if currentVC!.isKind(of : View1Controller.self) { topView.toView1Design() } if currentVC!.isKind(of : View2Controller.self) { topView.toView2Design() } if currentVC!.isKind(of : View3Controller.self) { topView.toView3Design() } } }課題2
しかしながらこのコードでは、デザイン変更関数が呼ばれた時に、toView1Btnがない、と言ったエラーが発生する。
pageViewController上でデザイン変更関数のみを呼び出しており、TopViewController上で行われている変数設定等を行っていないからだ。解決策2
検索結果2
どうやら、pageVioewControllerをTopViewController内部で設定する方法もあるらしい。
この方法を用いれば、上記のエラーも発生しなくなるはず。qiita : 【Swift】UIPageViewControllerとContainerViewを利用してタップで画面遷移
実装2
PageViewController.swiftを削除し、TopViewController.swiftに書き足し。
TopViewController.swiftimport UIKit class TopViewController: UIViewController { // pageのタイトル @IBOutlet weak var pageTitle: UILabel! // buttons @IBOutlet weak var toView1Btn: boundButton! @IBOutlet weak var toView2Btn: boundButton! @IBOutlet weak var toView3Btn: boundButton! // toView1Btn押下時 @IBAction func toView1Action(_ sender: Any) { let vc = storyboard?.instantiateViewController(withIdentifier: "view1") as! View1Controller pageViewController!.setViewControllers([vc], direction: .forward, animated: false, completion: nil) toView1Design() } // toView2Btn押下時 @IBAction func toView2Action(_ sender: Any) { let vc = storyboard?.instantiateViewController(withIdentifier: "view2") as! View2Controller pageViewController!.setViewControllers([vc], direction: .reverse, animated: false, completion: nil) toView2Design() } // toView3Btn押下時 @IBAction func toView3Action(_ sender: Any) { let vc = storyboard?.instantiateViewController(withIdentifier: "view3") as! View3Controller pageViewController!.setViewControllers([vc], direction: .reverse, animated: false, completion: nil) toView3Design() } // 画面遷移用 var pageViewController: UIPageViewController? override func viewDidLoad() { super.viewDidLoad() // ここから追加 // PageViewControllerの設定 pageViewController = children.first! as? UIPageViewController pageViewController!.setViewControllers([getView1()], direction: .forward, animated: true, completion: nil) pageViewController!.dataSource = self pageViewController!.delegate = self // ここまで追加 // 最初はview1を出しとく toView1Design() } } // デザイン変更用関数のまとめ extension TopViewController { // view1に遷移するとき呼ばれる func toView1Design() { // pageTitle変更 pageTitle.text = "view1" // ボタンの色変更(選ばれているボタンを赤色に、それ以外は灰色に) toView1Btn.setTitleColor(UIColor.red, for: []) toView2Btn.setTitleColor(UIColor.lightGray, for: []) toView3Btn.setTitleColor(UIColor.lightGray, for: []) } // view2に遷移するとき呼ばれる func toView2Design() { // pageTitle変更 pageTitle.text = "view2" // ボタンの色変更(選ばれているボタンを赤色に、それ以外は灰色に) toView1Btn.setTitleColor(UIColor.lightGray, for: []) toView2Btn.setTitleColor(UIColor.red, for: []) toView3Btn.setTitleColor(UIColor.lightGray, for: []) } // view3に遷移するとき呼ばれる func toView3Design() { // pageTitle変更 pageTitle.text = "view3" // ボタンの色変更(選ばれているボタンを赤色に、それ以外は灰色に) toView1Btn.setTitleColor(UIColor.lightGray, for: []) toView2Btn.setTitleColor(UIColor.lightGray, for: []) toView3Btn.setTitleColor(UIColor.red, for: []) } } // ここから下全部追記 // view取得系の関数まとめ extension TopViewController { // それぞれ、viewControllerを返す関数 func getView1() -> View1Controller { return storyboard!.instantiateViewController(withIdentifier: "view1") as! View1Controller } func getView2() -> View2Controller { return storyboard!.instantiateViewController(withIdentifier: "view2") as! View2Controller } func getView3() -> View3Controller { return storyboard!.instantiateViewController(withIdentifier: "view3") as! View3Controller } func getTop() -> TopViewController { return storyboard!.instantiateViewController(withIdentifier: "topView") as! TopViewController } } // datasource用の関数まとめ extension TopViewController : UIPageViewControllerDataSource { // 現在表示されているページの、Beforeに位置する、つまり左側に位置するviewを呼び出す関数 func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { print(viewController) if viewController.isKind(of : View3Controller.self) { // 3の時 -> 2 return getView2() } if viewController.isKind(of : RepoViewController.self) { // 2の時 -> 1 return getView1() } if viewController.isKind(of : CalendarViewController.self) { // 1の時 -> 3 return getView3() } return nil } // 現在表示されているページの、Afterに位置する、つまり右側に位置するviewを呼び出す関数 func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { print(viewController) if viewController.isKind(of : CalendarViewController.self) { // 1の時 -> 2 return getView2() } if viewController.isKind(of : RepoViewController.self) { // 2の時 -> 3 return getView3() } if viewController.isKind(of : UserViewController.self) { // 3の時 -> 1 return getView1() } return nil } } // delegate用の関数まとめ extension TopViewController : UIPageViewControllerDelegate{ // スワイプによるページ遷移が行われたときに呼ばれるメソッド func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool){ // 現在のviewcontrollerを取得 let currentVC = pageViewController.viewControllers?.first! if currentVC!.isKind(of : View1Controller.self) { toView1Design() } if currentVC!.isKind(of : View2Controller.self) { toView2Design() } if currentVC!.isKind(of : View3Controller.self) { toView3Design() } } }参考サイト
二つの画面をpageViewControllerで繋ぎ、スワイプでページ遷移
タブを作成して、タブ操作とページ遷移を紐付ける
UICollectionViewControllerとUIPageViewControllerでSmartNewsっぽいあのUIをお手軽に実現する
- 投稿日:2020-01-12T19:03:22+09:00
fallthroughはwhere条件を無視するので非排他的条件には使えなさそう
概要
Swiftのswitch文では、通常caseまたはdefaultを1つしか通りません。
しかし以降のcaseも続けて実行したい場合には、 fallthroughを使うことで実現することが可能です
これは 判定値がcaseに当てはまらなかったとしても実行されます。func testFallthrough(x: Int) { switch (x) { case 0: print("zero") fallthrough case 1: // x != 1 だがfallthroughしているので実行される print("one") fallthrough default: // x != default だがfallthroughしているので実行される print("non-binary") } } print(testFallthrough(x: 0)) // "zero" "one" "non-binary" が出力されるやりたかったこと
これを利用し、 ある2次元の座標点Pが、事前に定義した各領域に含まれるかどうかを判定するため、switch文で当たり判定の算出を試みました
例として、2つの領域rightAbove
とleftBelow
を定義し、その中にPが含まれている場合にその情報を配列で返したいとします。座標が領域に含まれることをチェックするcase文では、 whereにより判定をかけています。
ここで自分は「fallthroughはcaseにある条件を無視するのであってwhere句の条件は無視されない」と勝手に思い込んで、以下のような実装をしてしまいました。
enum AreaType: String { case rightAbove = "右上" case leftBelow = "左下" } func checkHitAreas(x: Int, y: Int) -> [AreaType] { var hitAreas: [AreaType] = [] switch (x, y) { case let (x, y) where -5...10 ~= x && -5...10 ~= y: hitAreas.append(.rightAbove) fallthrough case let (x, y) where -10...5 ~= x && -10...5 ~= y: hitAreas.append(.leftBelow) fallthrough default: break } return hitAreas }しかし実行してみると、見事にwhere句の条件も無視されていました。
print(checkHitAreas(x: 0, y: 0).map { $0.rawValue }) // 期待: [右上, 左下] -> 実際: [右上, 左下] print(checkHitAreas(x: 6, y: 6).map { $0.rawValue }) // 期待: [右上] -> 実際: [右上, 左下] print(checkHitAreas(x: -6, y: -6).map { $0.rawValue }) // 期待: [左下] -> 実際: [右上, 左下]fallthroughはcaseの指定だけでなくwhereの条件も全て無視するんですね
わざわざwhereが書いてると、直感的には判定に使ってくれそうな気がしていて勘違いしていました![]()
正しいコード
今回のように排他的ではない条件で判定をしたい場合は、素直にif文で書く他ないようです
func checkHitAreas(x: Int, y: Int) -> [AreaType] { var hitAreas: [AreaType] = [] if -5...10 ~= x && -5...10 ~= y { hitAreas.append(.rightAbove) } if -10...5 ~= x && -10...5 ~= y { hitAreas.append(.leftBelow) } return hitAreas } print(checkHitAreas(x: 0, y: 0).map { $0.rawValue }) // [右上, 左下] print(checkHitAreas(x: 6, y: 6).map { $0.rawValue }) // [右上] print(checkHitAreas(x: -6, y: -6).map { $0.rawValue }) // [左下]参考リンク
- 投稿日:2020-01-12T16:40:05+09:00
【swift5】漢字をひらがなにするAPIを使ってアプリ作ってみた
今回はAPIを使用してテキストフィールドに入力した漢字を平仮名にするといったものです。
OpenWeatherのAPIを使って天気予報のアプリを作ることはできたのですが、あれは位置情報を取得して自動的に天気をGETするというシンプルなものだったので今回はPOSTしたものをAPIを使って編集するといったものを作成しようと思いました。
文字にするとすごく簡単に見えるのですがとても大変でした。。。
Githubにソースをアップしているので全体像を知りたい方はこちらから
https://github.com/sventouz/kanji_to_hiragana_app
説明
ひらがなAPIキーの取得
gooさんのひらがなAPIというのを使用しました。
APIキーを以下から取得してください。
https://labs.goo.ne.jp/apiusage/
ソース
import UIKit class ViewController: UIViewController { @IBOutlet weak var convertText: UITextField! @IBOutlet weak var convertedText: UILabel! @IBOutlet weak var errorText: UILabel! override func viewDidLoad() { super.viewDidLoad() } @IBAction func convertButton(_ sender: Any) { let convertTextForApi = convertText.text! if convertTextForApi == "" { errorText.text = "漢字を入力してください。" return } else { errorText.text = "" } // URLRequstの設定 var request = URLRequest(url: URL(string: "https://labs.goo.ne.jp/api/hiragana")!) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") //POSTするデータをURLRequestに持たせる let postData = PostData(app_id: "xxxxxxxxxxxxxxxxxxxxxxx", request_id: "record003", sentence: convertTextForApi, output_type: "hiragana") guard let uploadData = try? JSONEncoder().encode(postData) else { print("json生成に失敗しました") return } request.httpBody = uploadData //APIへPOSTしてresponseを受け取る let task = URLSession.shared.uploadTask(with: request, from: uploadData) { data, response, error in if let error = error { print ("error: \(error)") return } guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else { print ("server error") return } if response.statusCode == 200 { guard let data = data, let jsonData = try? JSONDecoder().decode(Rubi.self, from: data) else { print("json変換に失敗しました") return } print(jsonData.converted) DispatchQueue.main.async { self.convertedText.text = jsonData.converted } } else { print("サーバエラー ステータスコード: \(response.statusCode)\n") } } task.resume() } } struct Rubi:Codable { var request_id: String var output_type: String var converted: String } struct PostData: Codable { var app_id:String var request_id: String var sentence: String var output_type: String }storyboard
シンプルなUIになっています。
テキストボックスで漢字を入力すると「変換後のテキスト」の部分に入力した漢字が平仮名になって返ってきます。
ちなみに何も入力せずに「変換!」をクリック(タップ)をするとエラーが表示されます。
GIF
圧縮したらなんかゆっくりになったけど笑
参考URL
- 投稿日:2020-01-12T15:21:00+09:00
文字列→塩基配列の相互変換ツールをつくってみた(アプリ版)
はじめに
この記事で公開したアプリの中身についてです。
この記事(どこまでショボいアプリがAppleの審査に通るのか試してみた)をみてわりと機能が少なくてもアプリ公開できるのか!と思い正月休みにアプリをつくってみました。
以前作ったこれ(文字列→塩基配列の相互変換ツールをつくってみた(PHP))をアプリにしてリリースしました。
リリースしたアプリ
つくったアプリは有料です。(目指せ!!トータルダウンロード数25!!!)
- Mac, iOS: ¥120
- Android : ¥100
Macアプリ
ターゲット:MacOS Catalina以降
iOSアプリ
ターゲット:iOS13以降
Androidアプリ
ターゲット:Android6.0以降
Web版
こんなアプリに金払いたくねぇよって人はぜひWeb版をどうぞ
http://adventam10.php.xdomain.jp/dna/index.php
アプリ概要
機能は極小で文字列⇔塩基配列を相互変換し、Twitterに投稿できるアプリです。(一応英語版もつくりました)
文字列->塩基配列 塩基配列->文字列 Macアプリ
Macアプリは一発で審査が通ったのでiOSと比べると機能が少ないです。
機能
- 文字列⇔塩基配列を相互変換
- Twitterに投稿機能
- 塩基配列の他アプリへの共有機能
- 塩基配列のテキストファイルへの書き出し機能
- 塩基配列のペーストボードへコピー機能
iOSアプリ
iOSアプリは4回リジェクトされたので他と比べると機能が多いです。
機能
- 文字列⇔塩基配列を相互変換
- Twitterに投稿機能
- 塩基配列の他アプリへの共有機能
- 塩基配列のテキストファイルへの書き出し機能
- 塩基配列のペーストボードへコピー機能
- 塩基配列の履歴機能(10件まで)
- 音声入力機能
1回目の審査では共有機能が使えないけど?バグじゃね?って理由でリジェクトされたのですが、2回目の審査で下記が追加されました
Guideline 4.2 - Design - Minimum Functionality履歴機能追加 -> リジェクト、音声入力機能追加 -> 通過
音声入力機能追加後にアプリをアップしようとすると下記のようなメールが来ました
ITMS-90683: Missing Purpose String in Info.plist - Your app's code references one or more APIs that access sensitive user data. The app's Info.plist file should contain a NSSpeechRecognitionUsageDescription key with a user-facing purpose string explaining clearly and completely why your app needs the data. Starting Spring 2019, all apps submitted to the App Store that access user data are required to include a purpose string. If you're using external libraries or SDKs, they may reference APIs that require a purpose string. While your app might not use these APIs, a purpose string is still required. You can contact the developer of the library or SDK and request they release a version of their code that doesn't contain the APIs. Learn more (https://developer.apple.com/documentation/uikit/core_app/protecting_the_user_s_privacy).ローカライズ対応していたので Info.plist にキーは追加せずに InfoPlist.strings に下記のように記述していたのですがそれではダメなようです。
"NSSpeechRecognitionUsageDescription" = "音声入力するために必要です"; "NSMicrophoneUsageDescription" = "音声入力するために必要です";Info.plist にもキー追加して同じように記述してやると通りました。Info.plist にも記載するとこちらの記載が優先されて InfoPlist.string の文字が表示されないと思ったのですがそうでもないようです。(ちゃんとローカライズされてました。)
Androidアプリ
Androidアプリはあんまさわったことがなかったので、最小構成です。(がんばってiOSアプリを追従するようにします!)
機能
- 文字列⇔塩基配列を相互変換
- Twitterに投稿機能
Web版
Webもほぼさわったことないので、最小構成です。(一応レスポンシブ対応はしてます。)
機能
- 文字列⇔塩基配列を相互変換
- Twitterに投稿機能
アプリのコードについて
このアプリのきもは文字列⇔塩基配列なのですがそこのコードについてです。
ソースは全部 GitHub で公開してます。方法
変換方法は間に16進数をかませてやってます。
最初は4進数に変換してそれぞれ [0, 1, 2, 3] -> [A, T, C, G] のように変換していたのですが、以前コメントで 0⇔AA みたいに2文字ずつやれば16進数でいけるよと教えていただきました
文字列->塩基配列
- 文字列 -> 2進数に変換
- 2進数 -> 16進数に変換
- 16進数 -> 塩基配列に変換(ATCG)
塩基配列->文字列
- 塩基配列 -> 2文字ずつに分割
- 分割文字列 -> 16進数に変換
- 16進数 -> 2進数に変換
- 2進数 -> 文字列に変換
変換コード
もっといい方法があればぜひ教えて下さい!!
swift
swift では String の Extension で文字列から16進数への変換、16進数から2進数への変換、文字列を2文字ずつ分割する変数とメソッドをつくりました。(swift が一番めんどくさい感じになってしまいました...
)
StringExtensions.swiftpublic extension String { // 16進数->2進数への変換 var hexadecimal: Data? { var data = Data(capacity: count / 2) let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive) regex.enumerateMatches(in: self, range: NSRange(startIndex..., in: self)) { match, _, _ in let byteString = (self as NSString).substring(with: match!.range) let num = UInt8(byteString, radix: 16)! data.append(num) } guard data.count > 0 else { return nil } return data } // 文字列->16進数の変換 var hex: String { let data = self.data(using: .utf8)! return data.map { String(format: "%02X", $0)}.joined() } // 指定文字数で文字を分割する func splitInto(_ length: Int) -> [String] { var str = self for i in 0 ..< (str.count - 1) / max(length, 1) { str.insert(",", at: str.index(str.startIndex, offsetBy: (i + 1) * max(length, 1) + i)) } return str.components(separatedBy: ",") } }private let dnaHexValues: [String: String] = ["AA": "0", "AT": "1", "AC": "2", "AG": "3", "TA": "4", "TT": "5", "TC": "6", "TG": "7", "CA": "8", "CT": "9", "CC": "a", "CG": "b", "GA": "c", "GT": "d", "GC": "e", "GG": "f"] // 文字列->塩基配列 func convertToDNA(_ text: String?) -> Result<String, DNAConvertError> { if isEmptyText(text) { return .failure(.empty) } var result = text!.hex.lowercased() dnaHexValues.forEach { dna, hex in result = result.replacingOccurrences(of: hex, with: dna) } return .success(result) } // 塩基配列->文字列 func convertToLanguage(_ text: String?) -> Result<String, DNAConvertError> { if isEmptyText(text) { return .failure(.empty) } if isInvalidDNA(text) { return .failure(.invalid) } let hex = text!.splitInto(2).compactMap { dnaHexValues[$0] }.joined() if hex.isEmpty { return .failure(.invalid) } if let data = hex.hexadecimal, let result = String(data: data, encoding: .utf8) { return .success(result) } return .failure(.invalid) }kotlin
kotlinが一番スッキリした感じにかけました。
val dnaHexValues = mapOf( "AA" to "0", "AT" to "1", "AC" to "2", "AG" to "3", "TA" to "4", "TT" to "5", "TC" to "6", "TG" to "7", "CA" to "8", "CT" to "9", "CC" to "a", "CG" to "b", "GA" to "c", "GT" to "d", "GC" to "e", "GG" to "f" ) // 文字列->塩基配列 fun convertToDNA(text: String?): String? { if (text.isNullOrEmpty()) { return null } val hex = text.toByteArray().map { b -> String.format("%02X", b) }.joinToString("") var result = hex.toLowerCase() dnaHexValues.forEach { (k, v) -> result = result.replace(v, k) } return result } // 塩基配列->文字列 fun convertToLanguage(text: String?): String? { if (text.isNullOrEmpty()) { return null } if (isInvalidDNA(text)) { return null } var index = 0 val strings: MutableList<String> = mutableListOf() while (index < text.length) { strings.add(text.substring(index, index+2)) index += 2 } val hex = strings.map { n -> dnaHexValues[n] }.joinToString("").toUpperCase() val result = ByteArray(hex.length / 2) { hex.substring(it * 2, it * 2 + 2).toInt(16).toByte() } return String(result) }PHP
PHPは変換のときにバックスラッシュいれないといけなくてなんか冗長な感じになりました。
// 文字列->塩基配列 function convertToDNA($text){ $hex = bin2hex($text); $nucleotideArray = array("AA", "AT", "AC", "AG", "TA", "TT", "TC", "TG", "CA", "CT", "CC", "CG", "GA", "GT", "GC", "GG"); $hexArray = array("/0/", "/1/", "/2/", "/3/", "/4/", "/5/", "/6/", "/7/", "/8/", "/9/", "/a/", "/b/", "/c/", "/d/", "/e/", "/f/"); $result = preg_replace($hexArray, $nucleotideArray, $hex); return $result; } // 塩基配列->文字列 function convertToLanguage($text){ $strArray = str_split($text, 2); $resultArray = array_map("dnaDecode", $strArray); $hex = implode("", $resultArray); return hex2bin($hex); } function dnaDecode($nucleotide){ $nucleotideArray = array("/AA/", "/AT/", "/AC/", "/AG/", "/TA/", "/TT/", "/TC/", "/TG/", "/CA/", "/CT/", "/CC/", "/CG/", "/GA/", "/GT/", "/GC/", "/GG/"); $hexArray = array("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"); $result = preg_replace($nucleotideArray, $hexArray, $nucleotide); return $result; }さいごに
変換後の塩基配列をどうにか圧縮したいのですが、そうすると圧縮した印が必要になったり...(TATAボックスでも付けるか
)
圧縮するにしてもswift, kotlin, PHPで方法は揃える必要があるし...悩みは尽きないです。
- 投稿日:2020-01-12T00:44:14+09:00
[Swift]ドラムロールボタン(くるくる回転して選択するボタン)をUIPickerViewで作る〜GAFAのサイトにジャンプするアプリを例に〜
↑みたいにくるくる回転させて選択するボタンを作ろうとしたら、一筋縄ではいかなかった(ライブラリみたいなものがなかった)ので作り方をシェアします。完成イメージ(GIF)
動作環境
Xcode 11.3
iOS 13.3
iPhone 11 Pro Max (シミュレーター)コード
今回はstoryboardを使わずに全てコードで実装しています。
ViewController.swift
// Created by japanesebonobo on 2020/01/10. // Copyright © 2020 japanesebonobo. All rights reserved. // import UIKit import SafariServices class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource, UITextFieldDelegate { private var myTextField: UITextField! let pickerView = UIPickerView() // ドラムロールボタンの選択肢を配列にして格納 let dataSource = ["Google", "Apple", "Facebook", "Amazon"] override func viewDidLoad() { super.viewDidLoad() // UITextFieldの配置するx,yと幅と高さを設定. let tWidth: CGFloat = 150 let tHeight: CGFloat = 30 let posX: CGFloat = (self.view.bounds.width - tWidth)/2 let posY: CGFloat = (self.view.bounds.height - tHeight)/2 // UITextFieldを作成する. myTextField = UITextField(frame: CGRect(x: posX, y: posY, width: tWidth, height: tHeight)) // 表示する文字を代入する. myTextField.text = "START" myTextField.textAlignment = .center // Delegateを自身に設定する myTextField.delegate = self // 枠を表示する. myTextField.borderStyle = .bezel //カーソル(キャレット)を非表示 myTextField.tintColor = UIColor.clear // myTextFieldをViewに追加する self.view.addSubview(myTextField) // pickerViewの配置するx,yと幅と高さを設定. pickerView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: pickerView.bounds.size.height) // Delegateを自身に設定する pickerView.delegate = self // 選択肢を自身に設定する pickerView.dataSource = self // pickerViewをViewに追加する let vi = UIView(frame: pickerView.bounds) vi.backgroundColor = UIColor.white vi.addSubview(pickerView) // UITextField編集時に表示されるキーボードをpickerViewに置き換える myTextField.inputView = vi } func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return dataSource[row] } func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return dataSource.count } // 各選択肢が選ばれた時の操作 func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { switch row { case 0: // Googleが選ばれたらHPにアクセスする guard let url = URL(string: "https://www.google.com/?client=safari") else { return } let safariController = SFSafariViewController(url: url) present(safariController, animated: true, completion: nil) case 1: guard let url = URL(string: "https://www.apple.com/jp/") else { return } let safariController = SFSafariViewController(url: url) present(safariController, animated: true, completion: nil) case 2: guard let url = URL(string: "https://www.facebook.com") else { return } let safariController = SFSafariViewController(url: url) present(safariController, animated: true, completion: nil) case 3: guard let url = URL(string: "https://www.amazon.co.jp") else { return } let safariController = SFSafariViewController(url: url) present(safariController, animated: true, completion: nil) default: break } } }コードの解説
・ドラムロールボタン実装のポイントはUITextFieldを編集する際に出てくるキーボードをUIPickerViewに置き換えることです。
・UITextViewでSTARTボタンを作って、UIPickerViewを作った後、UITextField.inputView(キーボードが出てくるところ)をUIPickerViewに置き換えてます。
・GAFAの各サイトに移動する際にはSafariServicesを用いることで簡単に実装できてます。雑感
・選択肢をドラッグして離したら選択という感じになっちゃってる。選択肢を確定するボタンがあると便利かも。
参考
UITextView - iPhoneアプリ開発の虎の巻
textfieldの青い棒を消したい参考にさせていただきました。ありがとうございました。