20200112のSwiftに関する記事は7件です。

わいわい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-question

cmake version 3.15.1をビルドする

cmakeが必要なのですが、cmakeのバージョンが 3.15.1でないとビルドスクリプトが正常に動かないです。
なので、cmakeを個別にビルドしてそれをビルドスクリプトに利用します。

先程の依存してるプロジェクトをクローンするにて、cmakeはclone済みなので、そちらに移動します。

# swift ディレクトリにて
$ cd ../cmake

そして下記のコマンドを叩いてください。

# cmake ディレクトリにて
$ ./bootstrap && make   

暫く待つと、./bin/以下にcmakeがビルドされています。
途中でjava関係のポップアップが出る可能性がありますが、無視してOKを押してください。

image.png

※ わかる人向け: 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, aborting

Xcode Projectを開く

では、Xcode Projectを開いていきます。Xcodeはswiftディレクトリから見て次の場所にあるのでopenコマンドなどを使って開きましょう。

# swift ディレクトリにて
$ open ../build/Xcode-DebugAssert/swift-macosx-x86_64/Swift.xcodeproj/

最初Xcode Projectを開くと、「targetが多すぎるのでschemeどうするか」という旨のポップアップが出ますが、targetは以降の作業で指定するので「Manual」っぽいことを書いてある選択肢を選んでください。

image.png

このポップアップはCloseで大丈夫です。

image.png

Xcode Projectにsil-optSchemeを追加する

では、そのSchemeを追加します。今回のワークショップではsil-optコマンドを使うので、それをビルドするSchemeを追加します。

まず、Xcode左上のNo Schemeをクリックします。
image.png

プルダウンが出てくるので、New Schemeを選択します。
image.png

ここで、どのtargetでSchemeを作るかが出てきます。まず、targetを選択します。
image.png

このような大量のリストが出てくるので、sil-optを探して選択してください。キーボードでなにかしら文字を入力をすると絞り込み検索ができます。図では、sil-optで絞り込みをした結果です。
※ 絞り込みをしてもだいぶスクロールして探さないと見つからないので注意してください。

image.png

元のポップアップでOKを押すとSchemeが追加されます。
image.png

sil-optSchemeを編集して、コマンドラインからデバッグできるようにする

次に、コマンドラインからsil-optコマンドを叩いても、Xcodeで設定したBreak Pointなどが働くようにします。

さっきのスキームをクリックして、Edit Schemeを選択します。
image.png

設定ポップアップが出るので、左のRunという項目を選択して、InfoタブのLaunchという項目にある、Wait for executable to be launchedにチェックしてください。
image.png

Closeして、設定はおわりです。ここまでお疲れさまでした。?

設定がうまく行っているかテストする

では、実際にsil-optがうまく準備してあるかをテストしましょう。

では実際にSILOptimizerのコードを書き換えます。

Command + Shift + oでプロジェクト内検索を開き、WaiWaiOptimizerと入力します。すると、WaiWaiOptimizer.cppが見つかるので、それを開きます。
image.png

以下のようなコードが出てくると思います。

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でのprintにあたるものと思ってください。
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をつけたら動くかも確かめてみましょう。
image.png

ここまで来たら、Command + Rを押して、ビルドと実行をします。時間はそこまでかからないです。
Runをする準備ができたら、Wainting to attach to sil-opt : sil-optと出るので、コマンドライン上からsil-opt起動してみます。
image.png

その前に、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!の数が違うので注意

image.png

成果物は、optimized-example.silとして出力されます。しかし、WaiWaiOptimizerrun関数には最適化するものを何も書いてないので、元のexample.silと比較しても特に目立った変更ははありません。

おわり

ここまで来たら、準備は完了です。ワークショップ当日に問題をPushしますので、それをPullして解いていくことになります。

お疲れさまでした、当日は頑張ってください!皆様のご来場お待ちしております〜!

追記事項

ワークショップ本体のリンク載せておきます。
https://qiita.com/freddi_/items/43130e5f2a6e9d33b0ca
https://qiita.com/freddi_/items/9690f53e267855ffe4b7
https://qiita.com/freddi_/items/38df6bd64904b5ccaa73

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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をつけている

スクリーンショット 2020-01-12 20.14.45.png

コード

TopViewController.swift
import 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.swift
import 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.swift
    override 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.swift
import 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()
        }
    }

}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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をつけている

スクリーンショット 2020-01-12 20.14.45.png

コード

TopViewController.swift
import 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.swift
import 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.swift
    override 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.swift
import 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で繋ぎ、スワイプでページ遷移

【決定版】UIPageViewControllerの使い方(Swift)

タブを作成して、タブ操作とページ遷移を紐付ける

UICollectionViewControllerとUIPageViewControllerでSmartNewsっぽいあのUIをお手軽に実現する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

fallthroughはwhere条件を無視するので非排他的条件には使えなさそう

概要

Swiftのswitch文では、通常caseまたはdefaultを1つしか通りません。
しかし以降のcaseも続けて実行したい場合には、 fallthroughを使うことで実現することが可能です :checkered_flag:
これは 判定値が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文で当たり判定の算出を試みました :triangular_ruler:
例として、2つの領域 rightAboveleftBelow を定義し、その中にPが含まれている場合にその情報を配列で返したいとします。

image.png

座標が領域に含まれることをチェックする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の条件も全て無視するんですね :warning:
わざわざwhereが書いてると、直感的には判定に使ってくれそうな気がしていて勘違いしていました :droplet:

正しいコード

image.png

今回のように排他的ではない条件で判定をしたい場合は、素直にif文で書く他ないようです :relieved:

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 }) // [左下]

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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

スクリーンショット 2020-01-12 16.19.29.png

シンプルなUIになっています。

テキストボックスで漢字を入力すると「変換後のテキスト」の部分に入力した漢字が平仮名になって返ってきます。

ちなみに何も入力せずに「変換!」をクリック(タップ)をするとエラーが表示されます。

GIF

圧縮したらなんかゆっくりになったけど笑

10emd-y9ykc.gif

参考URL

http://harumi.sakura.ne.jp/wordpress/2019/06/29/%E3%81%B2%E3%82%89%E3%81%8C%E3%81%AA%E5%8C%96api%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

文字列→塩基配列の相互変換ツールをつくってみた(アプリ版)

はじめに

この記事で公開したアプリの中身についてです。

この記事(どこまでショボいアプリがAppleの審査に通るのか試してみた)をみてわりと機能が少なくてもアプリ公開できるのか!と思い正月休みにアプリをつくってみました。

以前作ったこれ(文字列→塩基配列の相互変換ツールをつくってみた(PHP))をアプリにしてリリースしました。

リリースしたアプリ

つくったアプリは有料です。(目指せ!!トータルダウンロード数25!!!)

  • Mac, iOS: ¥120
  • Android : ¥100

Macアプリ

ターゲット:MacOS Catalina以降

DNA変換

iOSアプリ

ターゲット:iOS13以降

DNA変換

Androidアプリ

ターゲット:Android6.0以降

DNA変換

Web版

こんなアプリに金払いたくねぇよって人はぜひWeb版をどうぞ

http://adventam10.php.xdomain.jp/dna/index.php

アプリ概要

機能は極小で文字列⇔塩基配列を相互変換し、Twitterに投稿できるアプリです。(一応英語版もつくりました)

文字列->塩基配列 塩基配列->文字列
ios_dna_1 ios_dna_2

Macアプリ

Macアプリは一発で審査が通ったのでiOSと比べると機能が少ないです。

機能

  • 文字列⇔塩基配列を相互変換
  • Twitterに投稿機能
  • 塩基配列の他アプリへの共有機能
  • 塩基配列のテキストファイルへの書き出し機能
  • 塩基配列のペーストボードへコピー機能

iOSアプリ

iOSアプリは4回リジェクトされたので他と比べると機能が多いです。

機能

  • 文字列⇔塩基配列を相互変換
  • Twitterに投稿機能
  • 塩基配列の他アプリへの共有機能
  • 塩基配列のテキストファイルへの書き出し機能
  • 塩基配列のペーストボードへコピー機能
  • 塩基配列の履歴機能(10件まで)
  • 音声入力機能

1回目の審査では共有機能が使えないけど?バグじゃね?って理由でリジェクトされたのですが、2回目の審査で下記が追加されました:scream:

Guideline 4.2 - Design - Minimum Functionality

履歴機能追加 -> リジェクト、音声入力機能追加 -> 通過:tada:

音声入力機能追加後にアプリをアップしようとすると下記のようなメールが来ました

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進数でいけるよと教えていただきました:heart_eyes:

文字列->塩基配列

  1. 文字列 -> 2進数に変換
  2. 2進数 -> 16進数に変換
  3. 16進数 -> 塩基配列に変換(ATCG)

塩基配列->文字列

  1. 塩基配列 -> 2文字ずつに分割
  2. 分割文字列 -> 16進数に変換
  3. 16進数 -> 2進数に変換
  4. 2進数 -> 文字列に変換

変換コード

もっといい方法があればぜひ教えて下さい!!

swift

swift では String の Extension で文字列から16進数への変換、16進数から2進数への変換、文字列を2文字ずつ分割する変数とメソッドをつくりました。(swift が一番めんどくさい感じになってしまいました...:cry:

StringExtensions.swift
public 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ボックスでも付けるか:thinking:

圧縮するにしてもswift, kotlin, PHPで方法は揃える必要があるし...悩みは尽きないです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift]ドラムロールボタン(くるくる回転して選択するボタン)をUIPickerViewで作る〜GAFAのサイトにジャンプするアプリを例に〜

EF0DBB08-CC7D-4ECF-9C09-4C1DB01CE7BD.jpeg
↑みたいにくるくる回転させて選択するボタンを作ろうとしたら、一筋縄ではいかなかった(ライブラリみたいなものがなかった)ので作り方をシェアします。

完成イメージ(GIF)

GIFをクリックで拡大して見れます。
ezgif.com-video-to-gif.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の青い棒を消したい

参考にさせていただきました。ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む