フリープログラマー日記

iOS,アンドロイド開発を気ままにしながら生きてるおじさんのブログです。

第34回 removeFromSuperview()でハマる

 氏名入力をするために、スクロールビューのボタンから、氏名を入力する画面になっています。次に名前を登録のボタンを押したら、スクロールビューに名前が入り、全画面に戻るというのが私のシナリオですが・・・

f:id:momonga117:20180601205646p:plain

 なんということでしょう。
名前を登録ボタンの飛び先のコードにremoveFromSuperview()と書くと、Thread 1:signal SIGABRTというエラーメッセージが出て、怒られてしまいました。コンパイルは通るが実行時に不具合が生じるとよくこのエラーになるようです。しかも、このエラーの対処がなかなか見つからない・・・
なんども言います。素人なんです、私。エラーログ読んだりとかしないので。

そこで、原因を考えてみると、オブジェクトとして、NameInputViewがあり、そこにボタンの記述やらボタン処理やら書いています。そのボタン処理の中で、NameInputViewのインスタンスを、removeFromSuperview()・・・
と、ここで、処理中のプログラムが消えてしまうってのが、原因らしい。
走っている電車の前にあるレールを取っ払うみたいなことをしてた。これはまずいです。
 そこで、急遽、Teamクラスにコードを置き、Teamクラスの中から、消すことにしました。

こんな感じです。

    public func makeNameInputView() {
        
        let view: UIView = UIView(frame: MySettings.getFullFrame())
        view.backgroundColor = UIColor(red: 0, green: 155/255, blue: 155/255, alpha: 220/255)
        view.tag = MySettings.Entry.NameInputArea
        self.view.addSubview(view)
        
        
        // 走者名を登録します
        let infomation: UILabel = UILabel(frame:MySettings.Entry.imfomationFrame())
        infomation.text = "走者名を登録します"
        infomation.textAlignment = .center
        infomation.backgroundColor = UIColor.white
        view.addSubview(infomation)
        //入力用textField
        let nameField: UITextField = UITextField(frame: MySettings.Entry.nameFieldFrame())
        nameField.backgroundColor = UIColor.white
        view.addSubview(nameField)
        nameField.resignFirstResponder()
        
        //OKボタン
        let okButton: UIButton = UIButton(frame: MySettings.Entry.okButtonFrame())
        okButton.setTitle("名前を登録", for: .normal)
        okButton.setTitleColor(UIColor.black, for: .normal)
        okButton.backgroundColor = UIColor.lightGray
        okButton.tag = MySettings.Entry.OkButtonTag
        okButton.addTarget(self, action: #selector(onClick), for: UIControlEvents.touchUpInside)
        view.addSubview(okButton)
        
        //キャンセルボタン
        let cancelButton: UIButton = UIButton(frame: MySettings.Entry.cancelButtonFrame())
        cancelButton.setTitle("キャンセル", for: .normal)
        cancelButton.setTitleColor(UIColor.black, for: .normal)
        cancelButton.backgroundColor = UIColor.lightGray
        cancelButton.tag = MySettings.Entry.CanselButtonTag
        cancelButton.addTarget(self, action: #selector(onClick), for: UIControlEvents.touchUpInside)        
        view.addSubview(cancelButton)
        
        nameField.becomeFirstResponder()
    }
    
    // ボタンの動作
    @objc func onClick(sender: UIButton) {
        if sender.tag == MySettings.Entry.OkButtonTag {
            // OKボタンの時だけ、新規のメンバーを作成
        }
        let view = self.view.viewWithTag(MySettings.Entry.NameInputArea)
        view?.removeFromSuperview()
    

途中のaddTargetの書式、action: #selector(onClick)っていうところ、昔の本と違いますよね。なんか面倒になってます。

ま、これで、無事消えることとなりました。

第33回 氏名入力は、UITextFieldを使って入れればいいんだね。

 ゲームでは、あんまり使わない、文字入力なんですが、これはもう本当にプロの人から見れば一笑に付されるようなことしていると思います。

とりあえず、iPhoneでこんな画面を作りました。

f:id:momonga117:20180601205646p:plain

 左側のウインドウはスクロールビューで、氏名が入るとことなんですが、最後に氏名登録ボタンを設置しました。これをタップすることで、氏名入力画面が出てくるようにしています。
 
 ところで、オブジェクトの階層でいうと、EntryView(全体) の下にスクロールビューや出場者の表示、その他のボタンがあり、スクロールビューの中に、登録された選手名(今は0人)とこの登録ボタンがあります。
 で、この登録ボタンがトリガーになって、右側の状態になるというのが今回考えた部分ですが、右側の画面は、一つのクラスになっていて、独立したものです。

import UIKit

class NameInputView: UIView,UITextFieldDelegate {
    override init(frame: CGRect) {
        super.init(frame: frame)
        // 背景色は半透明の青色
        backgroundColor = UIColor(red: 0, green: 155/255, blue: 155/255, alpha: 220/255)
        
        // 走者名を登録します
        let infomation: UILabel = UILabel(frame:MySettings.Entry.imfomationFrame())
        infomation.text = "登録する氏名を入力してください"
        infomation.textAlignment = .center
        infomation.backgroundColor = UIColor.white
        addSubview(infomation)
        
        //入力用textField
        let nameField: UITextField = UITextField(frame: MySettings.Entry.nameFieldFrame())
        nameField.backgroundColor = UIColor.white
        addSubview(nameField)
        nameField.resignFirstResponder()
        
        //OKボタン
        let okButton: UIButton = UIButton(frame: MySettings.Entry.okButtonFrame())
        okButton.setTitle("名前を登録", for: .normal)
        okButton.setTitleColor(UIColor.black, for: .normal)
        okButton.backgroundColor = UIColor.lightGray
        addSubview(okButton)
        
        //キャンセルボタン
        let cancelButton: UIButton = UIButton(frame: MySettings.Entry.cancelButtonFrame())
        cancelButton.setTitle("キャンセル", for: .normal)
        cancelButton.setTitleColor(UIColor.black, for: .normal)
        addSubview(cancelButton)
        
        nameField.becomeFirstResponder()   // キーボードの表示
        
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}


 まあ、どこかの教科書に載ってるような、だらっとしたプログラムですが、素人がすることだから、こんなものでしょう。
 最後の .becomeFirstResponder() はキーボードを表示するもので、入力モードにしてしまいたいので入れてあります。


 この後は、名前を登録ボタンを押した時、Runnerクラスに実装する手順になります。

 私のこの連載を続けて読んでいただいている方はお分かりだと思いますが、Android,iOSの両方で、同じ構造、同じ動作をするプログラムを作るのが目的です。この部分はまだAndroid版に取り掛かっていませんが、今回のiOSでのUILabel、UITextField、UIButtonはAndroidにも同じ趣旨のものがあるようなので、基本的な部品を使って、(手抜きして)作って見ました。

第32回 凡ミス。setPuddingとsetMerginsについて

第20回で、setMarginsが使えない。みたいなことを書きましたが、凡ミスでした。

setPadding も setMargins もちゃんとあるのですが、どこか見つけられなかっただけと言うお粗末な結論です。

しかし、これは、きちんと書き留めなければいけないと言うことで、書きます。

setPaddingの方はViewクラスに定義されたメソットということですね。

なので、

        View view = new View(getContext());
        view.setPadding(10,10,10,10);

とやればいいんです。

そして、setMarginsを同じように探したために、失敗してしまいました。
setMarginsはレイアウトパラメータにセットするものだったんですね。

私、RelativeLayoutしか使わないので。

        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
                RelativeLayout.LayoutParams.WRAP_CONTENT,
                RelativeLayout.LayoutParams.WRAP_CONTENT
        );
        params.setMargins(10,10,10,10);

とにかく、解決してよかった。これで、レイアウトが自由にかけるようになります。

第31回 JAVA の ArrayLiist は便利!

 アプリの画面が少しだけ前に進んでいます。
懸案だった横に長い氏名表示域を短くし、右側にエントリーされた選手名を表示するようにしました。

f:id:momonga117:20180529200450p:plain

 注目すべきは、タップした順に出場者が表示されている事で、なんとこれは自然になってしまったというjava の仕様なんですね。
 最初考えたときはそうなればいいなと思ったけど、面倒なプログラムが必要だろうな、やめようかなと思っていたのですが、ArrayListを使うと後ろに追加、要素を探して削除、というのがとても簡単にできるのです。

 まず、次のようなstatic なデータ置き場を作っていきます。

import java.util.ArrayList;

public class EntryNames {

    static ArrayList<String> entryMember = new ArrayList<>();

    public static ArrayList<String> getEntryMember() {
        return entryMember;
    }

    public static void addEntryName(String name) {
        entryMember.add(name);
    }

    public static void removeName(String name) {
        entryMember.remove(name);
    }

    public static int count() {
        return entryMember.size();
    }
}

最初の static ArrayList entryMember = new ArrayList<>(); がデータの本体です。初期化を忘れずに。
これは、氏名(文字列)の入れ物となります。
氏名を入れるのは メソッドの entryMember.add(name); で引数の名前を追加してくれます。
そして、entryMember.remove(name); でその名前と同じ要素を削除してくれます。
 なんと素晴らしい事。こんな簡単に実現するのだ。驚き。

そして、これは static なものだから、別のクラスから簡単にアクセスできてしまう。

結論から言うと、データの共有化は static に限る!

ちょっと
言い過ぎですかね。

でも、実際、左側の名簿から、右側のリストにどうやって名前を移すか、すごい長い時間、考えていました。
一歩前進です。

次は swift でやってみよう。

第30回 static を使いまくる

 何をしてるのか・・・

 しかし、かっこいいから、時間を忘れてプログラムを改造中。

元のプログラム。

        if (entry) {
            setBackgroundColor(Color.argb(255,255,255,30));
        } else {
            setBackgroundColor(Color.argb(255,200,200,200));
        }


こうなりました。

       if (entry) {
            setBackgroundColor(MySettings.Color.selectYellow);
        } else {
            setBackgroundColor(MySettings.Color.unselectGrey);
        }

そして、MySettings.javaには、こんな風に書いてます。

    // アプリ専用のカラー定義
    static class Color {

        static int lightBlue = android.graphics.Color.argb(255,100,240,255);
        static int unselectGrey = android.graphics.Color.argb(255,200,200,200);
        static int selectYellow = android.graphics.Color.argb(255,255,255,30);
        
    }


他のところで、色を使う時、流用しやすいのではと思っています。

最初に気づけよ〜〜って言っても遅いんだから。

第29回 Static が便利だった!

 今まであんまり使ったことのない static キーワード。何かカウントするときとか便利だよ〜とかって、それくらいの知識だけで。
 でも、これって、インスタンス作らんでも使えるやんってことで、ビューを作る情報が散乱してるのを集めてみることにしました。

import UIKit

class MySettings  {
    
    // このクラスにはいろいろなビューのサイズを保管する
    static var width: CGFloat = 0
    static var height: CGFloat = 0
    static var sizeRetio: CGFloat = 0.24 // 縦横の比 横を100に固定
    
    // エントリー用のCGSizeを返す
    static func runnerEntryViewSize() -> CGSize {
        let w = width * 0.4
        let h = w * sizeRetio
        return CGSize(width: w, height: h)
    }
    
    static func runnerErentryViewWidth() -> CGFloat {
        return runnerEntryViewSize().width
    }
    
    static func runnerErentryViewHeight() -> CGFloat {
        return runnerEntryViewSize().height
    }
    
    // エントリービューのフレーム、とりあえず、全画面
    static func entryViewFrame() -> CGRect {
        return CGRect(x: 0, y: 0, width: width, height: height)
    }
    
    // エントリー用のCGRectを返す.スクロールビューでの座標
    static func runnerEntryViewFrame(num: Int) -> CGRect {
        // 引数num によって縦位置が変わる。
        let o: CGPoint = CGPoint(x: 0, y: runnerErentryViewHeight() * CGFloat(num-1))
        return CGRect(origin: o, size: runnerEntryViewSize())
    }
    
    // エントリー用スクロールビューのframe これは見える部分
    static func entryScrollViewFrame() -> CGRect{
        let frame: CGRect = CGRect(x: width*0.5, y: width*0.15, width: width*0.4, height: height - width * 0.4)
        return frame
    }
    
    // エントリー用スクロールビューのContentSize 中身のサイズ
    static func entryViewContentSize(num: Int) -> CGSize {
        let size: CGSize = CGSize(width: runnerErentryViewWidth(), height: runnerErentryViewHeight() * CGFloat(num))
        return size
    }
}

名前が長い目ですが、後で利用することを考えるとわかりやすい方がいいのかなと。

で、使う方は、こんな感じです。

    // 自前のイニシャライザ
    init(number: Int, name: String) {
        myNumber = number
        myName = name
        super.init(frame: MySettings.runnerEntryViewFrame(num: number))
        setBackGroundColor()
    }

    private func setBackGroundColor() {
        // 背景色をセット、エントリー時には色を変える 色は未調整
        if entry {
            backgroundColor = UIColor(red: 255/255, green: 255/255, blue: 30/255, alpha: 255/255)
        } else {
            backgroundColor = UIColor(red: 200/255, green: 200/255, blue: 200/255, alpha: 255/255)
        }
    }

 デザインを変えたい時には、MySettings.swiftだけいじれば良いのでとても楽になりました。でも、これって、プログラムの世界じゃ常識なのかもしれない・・・(未熟だったな〜)

今回は、static変数、static関数を用いて、プログラムを見やすくしようということでした。

MySettingsの中身をいじるだけで、ビューの位置を自由に変えられるようになりました。

f:id:momonga117:20180527113641p:plain

第28回 アプリ設定ファイル

 アプリ作る時って、やっぱりアプリ設定ファイルは作るんですよね?
いい方法があるのかもしれないですが、私の場合は、settings クラスを作って、そこにアクセスして作っています。これが簡単な気がする。
 なぜ、こんな話になっているとというと、とってもまずい状態になっていて、コードの中にいろいろな定数(例えば、ビューの幅や、色の値や、その他諸々・・・)を埋め込んでしまっているので、変更しきれなくなっているのです。(泣)
 これは先に考えておくべきでした。改造に取り掛かります。

次の図は、現在のxcodeとAndroidStudioのファイル構成です。
AndroidStudioの方は強制的にアルファベット順なので、整合していません。しかし、基本的には同じ構成になっています。違いは2〜3箇所くらいは出てきますが。このまま進めると大変そうなので、Settingsクラスを作ってみようと思っています。
こんなことしてるので、アプリ作りはなかなか進みません。

f:id:momonga117:20180526164539p:plain

うーむ、Runnerから始まるクラスが多すぎる・・・

第27回 Androidスクロールビューでタッチイベント

スクロールビューに名前の入ったビューアイテムが乗っています。スクロールビューのタッチイベントは嫌なので、このビューアイテムにタッチイベントをセットすることにしました。

私は、アマプログラマーなので、やれることだけしかしません。できるだけ難しいことは避ける。厳しいルートは通らない軟弱な登山家みたいなものです。

さて、タッチイベントですが、今回はAndroid版だけです。


RunnerEntryView.java(抜粋)

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int touch = event.getAction();      // タッチの種類を格納する
        float posX = event.getX();            // 位置を保存する
        float posY = event.getY();

        switch (touch) {
            case MotionEvent.ACTION_DOWN:
                // code
                break;
            case MotionEvent.ACTION_MOVE:
                // code
                break;
            case MotionEvent.ACTION_UP:
                if (entry) {
                    entry = false;
                    entryCancel.run();  // Runnerクラスへのアクセス
                } else {
                    entry = true;
                    entryOk.run();  // Runnerクラスへのアクセス
                }

                invalidate();       // このビューの再描画


                break;
            case MotionEvent.ACTION_CANCEL:
                // cede
                break;
            default:
                break;
        }
        return true;
    }

タッチアップが発生すると、内部のフラグentryを確認し、true、falseを切り替えています。それだけです。簡単ですね。

ただ、後で、利用するため、runnerオブジェクトに状態を通知します。
そのためにランナブルがついています。

もう一つ、ユーザーに状態を示すために、invalidate()が。

これは、Viewに対して、再描画と指示するものです。(と理解するのが単純でいいのです。直訳では「無効にする」なんですが。)

で、invalidate()で動作するのが、drawメソッドなんですね。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (entry) {
            setBackgroundColor(Color.argb(255,255,255,30));
        } else {
            setBackgroundColor(Color.argb(255,200,200,200));
        }

        final float numberWidth = myWidth * 0.22f;

        // アンドロイドではcanvasを使用する,canvasは呼び出し元にあるので引数で受け取る

        // android では Paintクラスを使って色やスタイルを決めていく
        Paint paint = new Paint();

        // 両サイド
        paint.setStrokeWidth(lineWidth);
        paint.setColor(Color.BLACK);
        canvas.drawLine(lineWidth/2, 0, lineWidth/2, myHeight,paint);
        canvas.drawLine(myWidth-lineWidth/2, 0, myWidth-lineWidth/2, myHeight,paint);

        // 上下サイド
        paint.reset();
        paint.setColor(Color.BLACK);
        paint.setStrokeWidth(lineWidth*0.3f);
        canvas.drawLine(0,lineWidth*0.15f,myWidth,lineWidth*0.15f,paint);
        canvas.drawLine(0,myHeight-lineWidth*0.15f,myWidth,myHeight-lineWidth*0.15f,paint);
(以下略)

entryの状態を見て、背景色を変えています。

タップすると、色が変わります。

これはライティングのゲームには使えそうですね。
いい感じで反応してくれます。

f:id:momonga117:20180526093704p:plain

第26回 氏名の入出力!

ちょっと苦戦します。スクロールビューのタッチイベントの扱いって難しいですね。

話は、少し前になりますが、Androidのスクロールビューは単一のレーアウトにまとめてのせるしか方法がなかったので、iOSとは同じ書き方ができませんでした。Androidの方にひとつ余分にクラスを作る必要があります。

この余分なレイアウトクラスに働きを持たせると整合性がなくなっていくので、このレイアウトにはあまり仕事をさせたくありません。

ということは、iOSではスクロールビューに置いたRunnerオブジェクト、Androidではレイアウトクラスに置いたRunnerオブジェクトにエントリしたよ〜みたいな挙動をさせるのが一番かなって思います。

ただ、オブジェクト本体はTeamクラスに所属しているメンバーで、ビューに乗っているのは、Runnerクラスから出てきたオブジェクトなので、元のオブジェクトのEntryフラグをいじる必要がある。???

とても複雑ですよね。どうやっても簡単には説明できない・・・

これはクロージャの出番になりそうです。う〜ん、頭が痛い。

昨日から、メンバー登録の部分を作っています。
関数だけ。

Swift版です。Team.swiftに置いてます。

    // 選手の追加・削除
    func addRunner(name: String) {
    
        // 新しいrunnerオブジェクトを作り、氏名を保存する
        lastId += 1
        saveLastId()
        let runner: Runner = Runner(number: lastId)
        runner.saveName(name: name)
        runners.append(runner)
        
    }
||< 

java版です。Team.javaに置いてます。
>|java|
   // 選手の追加・削除
    void addRunner(String name) {

        // 新しいrunnerオブジェクトを作り、氏名を保存する
        lastId++;
        saveLastId();
        final Runner runner = new Runner(lastId,myContext);
        runner.saveName(name);
        runners.add(runner);

    }

かなり、似たプログラムになっています。

途中、出てくるsaveLastIdという関数は、今ここまでのIDを使ってますよと記録しておき、次にリストを作るときは、ファイルからその番号までを読み込むことになります。

この関数をテストしてみました。

f:id:momonga117:20180526005039p:plain

スペーシングがいまいちですね〜。センスのなさが・・・悲しい。
名前をもう少し左に寄せて、右側にエントリーボタンを作ってみたいと思っています。

第25回 クラスの設計

この連載を読んでいただき、ありがとうございます。

「大発見やー」と自画自賛していることを書いてみたいと思いますが、もしかしたらそんなのは当たり前のことなのかもしれません。プロのプログラマーは隠してるんじゃないの?みたいに思えてきますが。

まず、私の設計方針である「iOSAndroidで同じクラス設計をする」というところから振り返りますが、図式化すると次のようになります。

f:id:momonga117:20180525133314p:plain

最初のクラスはアプリの管理クラスですね。
続いて呼ばれるのが、画面の管理クラス。
実装はいろいろあると思いますし、ここでstoryboard,XMLファイルが登場するのが教科書では多いですよね。

で、私の場合は、この画面を提供するクラスから、画面表示パーツを呼び出しています。

f:id:momonga117:20180525133754p:plain

一つのビューを作ってそれを表示するだけの教科書的な説明では、この形でも十分だとは思うのですが。本屋さんにある解説書はほとんどそうですよね。
今まで、これが自然な流れだと思っていたし、なんの疑いも持たなかったのですが、このままだと次のようなことが起こってきます。

Teamクラスを作って、それをEntryViewを作るとき渡す。終われば返してもらう。
Runningクラスを作るとき、渡す。終われば返してもらう。
渡したとき、Teamクラスの中身をいじるのが普通ですよね?
エントリーされたら、フラグを立てたり、新規のメンバーを追加したり、ラップデータを記録したり。

Teamクラスが移動していくのはとても不合理です。だいたい、うまく返るのかどうかも怪しいです。

Teamクラスが動くのがいけないんだと考えたとき、閃いたのが、「Team クラスを中心にしよう。」だったわけです。コペルニクス的転回ですよね。かつて、天動説に取って代わって地動説が合理的と判断されたのと同じです。

さて、クラス設計は次のように変わりました。

f:id:momonga117:20180525135442p:plain

あるメニュー表示するとき、Teamクラスに「このメニューを表示するよ」と伝わります。すると、Teamクラスは、下位のビューを作るクラスに指示を出し、必要な名前を渡して、ビューを返してもらいます。それを画面を表示するクラスに返して、表示するという寸法です。

このような実装に変わっています。(java版)

呼び出し側(抜粋)

    public void showEntryMenu() {

        // 以前のビューは消しておく
        this.removeAllViews();
        // 端末のサイズを取得
        final int width = this.getWidth();
        final int height = this.getHeight();

        // Entry のインスタンスを作成。
        EntryView entryView = myTeam.entryMenuView(width,height);
        
        entryView.setBackToMenu(new Runnable() {
            @Override
            public void run() {
                showMainMenu();
            }
        });

        // Androidではレイアウトパラメーターを使用
        LayoutParams mainMenuParams = new LayoutParams(
                width,height
        );
        mainMenuParams.addRule(CENTER_IN_PARENT);
        mainMenuParams.addRule(CENTER_IN_PARENT);
        // ビューを画面中央に表示
        addView(entryView, mainMenuParams);

    }

10行目のところがTeam.entryMenuView(width,height); という形に変わっています。Teamクラスに作るように指示を出しているわけです。


Teamクラス(抜粋)

public EntryView entryMenuView(int width, int height) {

        ScrollView scrollView = new ScrollView(myContext);
        RunnerList listView = new RunnerList(myContext,width*8/10,width*13/100);
        // 選手名と番号を入れるメソッド
        for (int i=0; i<runners.size(); i++) {
            listView.setRunnerView(i+1,runners.get(i).getName());
        }

        scrollView.addView(listView);

        return new EntryView(myContext, width,height,scrollView);
    }

Teamクラスでは
RunnerListを作った後、氏名を送って、氏名入りのビューを完成させます。

これで、TeamオブジェクトやRunnerオブジェクトをコピーしたりするややこしいことはしなくて良くなって、一安心。

管理すべきデータを中心に考えるというのは多分当たり前?

それなら、今までやってたのはなんだったんだろう?