20200112のiOSに関する記事は6件です。

[Unity] ビルド関連処理を自分なりに基盤化し始めました

目的

  • アプリビルドのCIなどで要求される機能を実装

現在の機能

  • 設定ファイルおよびSchemeに応じたビルド設定の反映
    • ライブラリの追加を避けつつデシリアライズ出来るようXMLに記述
    • が、今後やりたい事を踏まえるとスクリプトでも扱いやすい形式にすべきか悩み中
  • KeystoreやAppleDeveloperTeamIdなども設定ファイルから切り替え
  • ログを詳細と簡易で出し分け
  • AndroidでExport Projectしつつapk出力まで対応

実行

  • macOSでもWindowsでもbashから以下のコマンドで実行可
sh Batch/AppBuild.sh -p [プラットホーム] -c [設定名] -s [scheme] -e [実行メソッド名]

成果物

追加でやりたい事

  • アプリアイコンの差し替え
  • バージョン変更(BuildNumberのインクリメント)
  • テスト公開用の各ストアへのアップロード
  • Deploygateへのアップロード
  • Build And Run対応

今後

  • 自作アプリ開発と合わせて機能追加や改修が入り次第フィードバックしていきます
  • Scriptable Build Pipeline を使えばもっと簡易化できるかもです
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

iOS, Mac, Androidのアプリをリリースしてみた

はじめに

アプリのリリース方法の備忘録です。

今からアプリをリリースしたい人はとりあえずデベロッパー登録してください

登録まで2日くらいかかります。休日はさむともっとかかるのでアプリ作る前にデベロッパー登録しておく方がスムーズに進みます。

  • Apple: 年会費 ¥12,980(2020/1時点)
  • Google: 登録費 $25.00(2020/1時点)

全部 Macbook Pro macOS Catalina(10.15.2)で作業しました。

リリースしたアプリ

Macアプリ

ターゲット:MacOS Catalina以降

DNA変換

iOSアプリ

ターゲット:iOS13以降

DNA変換

Androidアプリ

ターゲット:Android6.0以降

DNA変換

Macアプリのリリース

開発環境

Xcode 11.3

リリース時に必要なもの

  • アプリアイコン(iOSと共通可)
  • プライバシーポリシーのサイト(iOSと共通可)
  • ストア用のスクショ2枚(iOSと別々)

アプリアイコン作成

GIMP を使って1024 x 1024 ピクセルのアイコンを作成しました。
Googleで「iOSアイコン グリッド」検索して出てきたグリッドガイドを参考に適当にこのへんの画像を配置してつくりました。

アイコンのリサイズ

MakeAppIcon を使ってリサイズしました。

アイコンをアップしたくないって人はGIMP用のスクリプトつくったのでどうぞ

導入方法はこちらを参考にしてください。GIMPで1024x1024の画像を開いてフィルター(R)->Python-Fu->iOS-resizeIconsで実行できます。実行するとapp_iconsフォルダにリサイズ後のアイコンがあるはずです。

デベロッパー登録

ここ Apple Developer Program - Apple Developer で登録します。

下記参考に登録しました。

【2016年最新版】わかりやすく徹底解説!Apple Developer Programへの登録手順(個人)

スクショ作成

審査提出時にアスペクト比16:10の下記のいずれかのサイズのスクショが最低2枚必要です。(ローカライズ対応する場合は同じ画像でも申請できますが個別に用意した方がいいです)

  • 1280 x 800 ピクセル
  • 1440 x 900 ピクセル
  • 2560 x 1600 ピクセル
  • 2880 x 1800 ピクセル

参考:App Store Connect ヘルプースクリーンショットの仕様

わたしは Mac でアプリをフルスクリーン状態にしたらアス比が 16:10 だったので、フルスクリーン状態のスクショをとって 2880 x 1800 ピクセルにリサイズしました。この方法は文字とかめっちゃ小さくなるのであまりおすすめできません...(どなたかいい方法教えて下さい:bow:)

プライバシーポリシーのサイト作成

アプリをリリースするには、データ収集する機能があるかないかに関わらず、プライバシーポリシーが必要になりました。

下記参考に作成しました。

めんどくさかったのでGitHub Pagesで対応しました。

プライバシーポリシーの中身は下記で作成しました。

App Privacy Policy Generator

ローカライズ対応する場合も英語版のみで審査は通過しました。

提出時にサポートURLも入れないといけないようですがとりあえずプライバシーポリシーと同じURLを入れると通りました...(なんか作成しないといけないんだろうか?:sweat_smile:)

アプリをApp Store Connectで審査提出

証明書つくったり、アーカイブしたりしてするのは下記参考にしました。

審査

ここまで準備すると審査に出せるはずです。しばらくすると下記のようなメールが届きます。

The status of your (macOS) app, アプリ名, is now "Waiting For Review"

審査は驚くほど早く3時間ほどで再びメールが来ました!!下記のようなメールが来ると審査通過です。

The status of your (macOS) app, アプリ名, is now "In Review"

残念ながら下記のようなメールが来るとリジェクトです。。。

New Message from App Store Review Regarding アプリ名

続いて下記の様なメールが来た場合は審査は通ったけどストアにあげるのは保留中です。

The status of your (macOS) app, アプリ名, is now "Pending Contract"

わたしの場合は有料アプリでローカライズ対応していたので App Store Connect の「契約/税金/口座情報」で各種情報を入力する必要があります。

ステータスは他にも色々あるようです

App Store Connect ヘルプーApp ステータス

契約/税金/口座情報の入力

おそらく無料アプリの場合は必要ないかと思います。
このあたりはあまりよくわからないままやったのであってるか不明です...(自己責任でお願いします:bow:)

ここまでやるとしばらくして下記のメールが届き無事リリースされました:tada:

Welcome to the App Store

iOSアプリのリリース

下記は Mac アプリと共通です。

  • アプリアイコン作成
  • デベロッパー登録
  • プライバシーポリシーのサイト作成
  • 審査
  • 契約/税金/口座情報の入力

開発環境

Xcode 11.3

リリース時に必要なもの

  • アプリアイコン(Macと共通可)
  • プライバシーポリシーのサイト(Macと共通可)
  • ストア用のスクショ3枚(Macと別々)

スクショ作成

iPhone, iPad 対応だったので下記スクショが必要でした。

  • iPhone 6.5インチディスプレイ
  • iPhone 5.5インチディスプレイ
  • iPad Pro (第3世代) 12.9インチディスプレイ
  • iPad Pro (第2世代) 12.9インチディスプレイ

それぞれ1枚ずつでいけました。(iPad は同じ画像でいけました)

ローカライズ対応する場合は同じ画像でも申請できますが個別に用意した方がいいです。

Appetize.io にアップすると色々な端末のスクショが取れて便利らしいです。

参考:iOSアプリをストアにあげる時に必要なものメモ

これであとはMacアプリと同じ手順で申請できます:tada:

Androidアプリリリース

開発環境

Android Studio 3.5.3

リリース時に必要なもの

  • アプリアイコン
  • ストア用のスクショ2枚
  • ストアのフィーチャー グラフィック用画像

アプリアイコン作成

GIMP を使って 512 x 512 ピクセルのアイコンを作成しました。

デベロッパー登録

下記参考に登録しました。

Google Play Developerに登録する

スクショ作成

スマホとタブレットで別れており、全体で最低2枚のスクショが必要らしいです。

わたしはスマホ用2枚のみアップしました。

フィーチャー グラフィック用画像

横 1,024 x 縦 500の JPG または 24 ビット PNG(アルファなし)画像が必要なようです。

参考:Google Playガイドー注目を集めるフィーチャー グラフィックでアプリをアピールする

アプリをGoogle Play Consoleで審査提出

アプリの提出は下記を参考にしました。

Google Play Storeにアプリを公開する

(わたしはレーティング設定のアンケートを1つ回答し忘れていてずっと処理中で止まってました:neutral_face:)

提出後、5日くらいで下記のメールが届き無事リリースされました:tada:

IARC Live Rating Notice: アプリ名

公開後はすぐにはストアの検索にはひっかからないようで24時間位かかるそうです。それまではURL直打ちとかじゃないとたどり着けませんでした:frowning2:

おまけ(Web)

Web版も公開しました。

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

注意:httpです。スマホでみると広告出ます(PCは出ないはず)

方法

ファビコン作成

特に必須ではありませんがせっかくなんでファビコンを作成しました。

下記に Android のアイコン(512 x 512)をアップしてつくりました。

様々なファビコンを一括生成。favicon generator

公開

下記参考にXFREEで公開しました。

無料でできるPHPのWEBサイト公開(テスト用)

さいごに

今まではソースごと納品とかストア公開は別の人が担当とかでストア公開したことありませんでしたが、これでようやくわたしもデベロッパーを名乗れます!!

つくったアプリは有料です:nerd:

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

目標は Google Play の登録料の $25.00 のトータルダウンロード数25!!!

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

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

FlutterのListViewでリストを削除した際のA dismissed Dismissible widget is still part of the tree

はじめに

最近Flutterの勉強を開始しました。
初めてのアプリということで、ToDoアプリの開発を行っていました。
 
ListViewというwidgetを使っているのですが、途中エラーが発生し困ったので解消法の共有です。
超初心者向けなのであしからず。
 

現象

スクリーンショット 2020-01-10 0.03.41.png

上記のようなTodoリストを作成し、各 TodoはFlipすることで削除できるようにしました。
しかし、test3→test2→test1の順で削除すると、問題なく削除されるのですが、test3を削除する前にtest2を削除するとエラーが発生するという事象に会いました。

エラー内容

Performing hot reload...                                                
Reloaded 1 of 478 libraries in 396ms.
flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
flutter: The following assertion was thrown building Dismissible-[<'0'>](dirty, dependencies:
flutter: [Directionality], state: _DismissibleState#74b1e(tickers: tracking 2 tickers)):
flutter: A dismissed Dismissible widget is still part of the tree.
flutter: Make sure to implement the onDismissed handler and to immediately remove the Dismissible widget from
flutter: the application once that handler has fired.
flutter:
flutter: The relevant error-causing widget was:
flutter:   Dismissible-[<'0'>]
flutter:   file:///Users/kazuma/Desktop/myproject/flutter-study/todo_app/lib/main.dart:53:20
flutter:
flutter: When the exception was thrown, this was the stack:
flutter: #0      _DismissibleState.build.<anonymous closure> (package:flutter/src/widgets/dismissible.dart:526:11)
flutter: #1      _DismissibleState.build (package:flutter/src/widgets/dismissible.dart:535:8)
flutter: #2      StatefulElement.build (package:flutter/src/widgets/framework.dart:4334:27)
flutter: #3      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4223:15)
flutter: #4      Element.rebuild (package:flutter/src/widgets/framework.dart:3947:5)
flutter: #5      StatefulElement.update (package:flutter/src/widgets/framework.dart:4413:5)
flutter: #6      Element.updateChild (package:flutter/src/widgets/framework.dart:2977:15)
flutter: #7      SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:545(ddd78475600b5492fc67889427724d06.png)════════════════════════════════════════════════════════════════

エラーとなっているコード

    Widget build(BuildContext context) => Scaffold(
          key: _scaffoldKey,
          appBar: AppBar(
            title: Text('Todoリスト'),
          ),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () {
              // Todo入力画面への遷移
              _navigateAndInputTodo(context);
            },
          ),
          body: ListView.builder(
            itemCount: _todos.length, //ここで表示可能なリスト数を制限しないと、表示できなくなった時にエラーになる。
            itemBuilder: (BuildContext context, int index) {
              return Dismissible( //**ここでエラー発生**
                // KeyはFlutterが要素を一意に特定できるようにするための値を設定する。
                key: Key(index.toString()),
                // onDismissedの中にスワイプされた時の動作を記述する。
                // directionにはスワイプの方向が入るため、方向によって処理を分けることができる。
                onDismissed: (direction) {
                  setState(() {
                    // スワイプされた要素をデータから削除する
                    _todos.removeAt(index);
                  });
                  // スワイプ方向がendToStart(画面左から右)の場合の処理
                  if (direction == DismissDirection.endToStart) {
                    Scaffold.of(context).removeCurrentSnackBar();
                    Scaffold.of(context)
                        .showSnackBar(SnackBar(content: Text("削除しました")));
                    // スワイプ方向がstartToEnd(画面右から左)の場合の処理
                  } else {
                    Scaffold.of(context).removeCurrentSnackBar();
                    Scaffold.of(context)
                        .showSnackBar(SnackBar(content: Text("削除しました")));
                  }
                },
                // スワイプ方向がendToStart(画面左から右)の場合のバックグラウンドの設定
                background: Container(
                  alignment: Alignment.centerLeft,
                  color: Colors.redAccent[700],
                  child: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 0.0, 0.0),
                      child: Icon(Icons.delete_forever, color: Colors.white)),
                ),

                // スワイプ方向がstartToEnd(画面右から左)の場合のバックグラウンドの設定
                secondaryBackground: Container(color: Colors.blue),

                child: Card(
                  child: ListTile(
                      title: Text(_todos[index].title),
                      onTap: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                              builder: (context) =>
                                  DetailScreen(todo: _todos[index])),
                        );
                      }),
                ),
              );
            },
          ),
        );

当時参考にしたサイト

https://stackoverflow.com/questions/55792521/how-to-fix-a-dismissed-dismissible-widget-is-still-part-of-the-tree-error-in
https://stackoverflow.com/questions/58470821/a-dismissed-dismissible-widget-is-still-part-of-the-tree-in-flutter

結局ほとんどのサイトで、onDismissedの時に、ちゃんとsetStateでオブジェクトを削除しようってことが書いていたんですよね。
ただ、当時、自分の実装もそうしており、、、下記です。

                onDismissed: (direction) {
                  setState(() {
                    // スワイプされた要素をデータから削除する
                    _todos.removeAt(index);
                  });

結論

DismissiblekeyKey(index.toString())を指定しているのが原因でした。
これだと、途中のTODOが削除された場合、削除したindexが他のTODOで使いまわされてしまい、不整合が起きるので、エラーになっているようです。完全に初心者の過ちでした。。(恥ずかしい)

DismissiblekeyObjectKey(_todos[index])にするとTODOごとにユニークなキーになるので、エラーが発生しなくなりました。

Flutter、面白いですね。勉強引き続き頑張ります。
これから頑張っていくので、githubフォローしていただけるか、Starつけていただけるとモチベーションに繋がるので、もしよかったらよろしくお願いします。:flushed:
https://github.com/kazumaz/flutter-study

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