フリープログラマー日記

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

簡単に、スマートに!

SwiftUI ってよくできてます。
次のような図形ってよくありそうでしょ?
f:id:momonga117:20211108232941p:plain

で、これがSwiftで書いたプログラム。

import SwiftUI

struct Room: View {
    // まず、ルームを定義。
    // セル数を与えられて、正方形数個でできた矩形領域を生成。
    let roomColumn: Int     // ルームは何段か
    let roomRow: Int        // 一段の正方形の数
    var body: some View {
        VStack(spacing:1) {
            ForEach(0..<roomColumn) { c in
                HStack(spacing:1) {
                    ForEach(0..<roomRow) { r in
                        Text("1") // これが正方形パーツ
                    }
                    .frame(width: 30, height: 30, alignment: .center)
                    .background(Color.yellow)
                }
            }
            .background(Color.black)
        }
        .background(Color.black)
    }
}

struct Room_Previews: PreviewProvider {
    static var previews: some View {
        Room(roomColumn: 4, roomRow: 4)
    }
}[f:id:momonga117:20211108232941p:plain


Text("1")というのが正方形パーツで、それを4回、さらに4回ループさせて
並べただけです。
すごく簡単にできました。(5分か10分くらい(^^)v)

SwiftUI、その後

SwiftUIをさわり始めてから、10日ほどでしょうか。
何を作ろうかと考えてたら、本棚にナンプレ本が・・・
なぜか、ノーマルより、4X4、5X5や合体ナンプレの好きな私は、これをテーマに組み始めてしまいました。
まずはメニュー画面。
f:id:momonga117:20211107111743p:plain
正方形ナンプレ5種類と合体ナンプレ2種類をメニュー画面に表示し、リンクを貼って、画面遷移をするようにしています。
リンクで飛んだ先はこんな感じです。
f:id:momonga117:20211107111752p:plain
ボードをクリックをすると、選んだ数字が入力されていきます。さらに、入力ができないところを色付けしています。
これは28X28ナンプレで画素数の必要性から、iPad Pro 12.9inch シュミレータを使っています。
これだけViewパーツを貼り付けると結構重たいです。

SwiftUIを使うと、画面構成を制作するのががものすごく楽になったという印象です。
ソースは少しずつ公開していきます。では。

第2章 SwiftUIの攻略 No.1

SwiftUIをターゲットに絞って、再出発。

Apple Developer に swiftチュートリアルがあります。英語に悪戦苦闘しながら、なんとか食らい付いている次第です。

不思議なことの一つ。
今まで、AppDelegate.swiftが始まりで(古いかも)、そこにはAppDelegateクラスが書かれていたのに、今は xxxxxxxApp.swiftが始まりで、そこにはContentView() という呼び出し文が書かれているだけ・・・すごい。かっこいい。進歩!

要するに、(推測に過ぎないですが)アプリ自体は「xxxxxxxApp」構造体(structで宣言されてます)をインスタンス化し、iPhone画面などに表示するようになっているということみたいですね。(あってるかな?)

呼び出される ContentView 構造体(これもstructで宣言)は ContentView.swift で宣言されていて、ここに自作のパーツを並べていけば良さそうです。

めっちゃスッキリしましたね。これは、仕事が捗るかも。

ブログ再開します

諸般の事情によりブログ更新ができずにいましたが、再開することにしました。
今、ハマっているのは、「swiftUI」なるインターフェース。
初心にかえって、もう一度勉強してみたい。

で、現在のところ。

Xcode version 13.0

swift の version ・・・ どこで見るのかわからん。(今のところ)


追記:ターミナル開いて

 >swift ってしたら

Welcome to Swift version 5.5-dev. って出た。

 >swift -v でもう少し詳しく出て

ずらずらっと出てくるけど、
Apple Swift version 5.5 (swiftlang-1300.0.31.1 clang-1300.0.29.1)
という一文から
version は5.5であってそうですね。

第44回 プロパティとフィールド、よくわかりません。

実はハマりまくって、ブログ更新がおろそかに・・・

そもそも、

Kotlinのクラスは、フィールドを持つことができません。(引用1)

この一文が混乱の元になりました。

じゃあ、フィールドって何?ってなると、Java解説本では

クラスの内部の定義した変数をフィールドと呼びます。(引用2)

さらに、swift解説本では

プロパティとは、クラスや構造体に所属する変数のことです。(引用3)

私ゃ日本人なんで、クラスの中には変数や関数が定義できて変数の処理するのに関数をつかんだな〜ぐらいにしか認識してないのに、この絶望的な矛盾はなんなんだろ?

いや、引用2と引用3はいいんです。それぞれの言語で、呼び名が違うくらいはいいんです。

じゃ、Kotlinで書いたこのクラスでは

class Member(private val myId: Int) {
    // 選手名
    var name: String? = null
}

この後、名前を読み出す関数とかあるのですが、このnameはクラス内の変数に違いないが、同じ書き方であるにも関わらず、Javaでフィールドと呼び、Kotlinではプロパティと呼び、フィールドではないということらしい。

素人なもので、まあいいか〜みたいなスルーでもよかったのですが、読んでくださる方もいるわけで、調べようかと思ったらとんでもないことになりそうです。

Kotlinでは

    member.name = "青木"

とすると、これは、セッターを呼び出していることになるようなんですね。つまり、直接、インスタンス変数にアクセスしていないということを言いたいようなんです。

Javaでは直接、フィールド変数にアクセスできるようで、カプセル化のため、フィールド変数をprivateにし、getter,setterを自分で書くことが多いようです。

あと、Kotlinではバッキングフィールドなる言葉が出てきて、何かしらフィールドが作られてるようなんですが・・・

本音を言ってしまえば、同じ書き方して、同じ動作をすればそれでいいんじゃないっていうところ。

ただ、人に説明する時は、内部でやってることは少し(かなり)違うんですよって付け加える必要がありそうです。

Kotlinまだ麓でウロウロってとこです。

第43回 KotlinでコンパイラWarningが出てくる

Kotlin でお遊びしてて気づいた。
いつも Warningが表示されています。
何も手を加えていない最初のプログラムをコンパイルした時の結果がこれ。


f:id:momonga117:20180620021110p:plain

勝手に作っておいて、どうしてコンパイラエラーなんか出すの!
と、言っててもしょうがないので、原因を探ると・・・

f:id:momonga117:20180620021458p:plain

implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" のところに何やら横線が引かれているじゃありませんか!

さらに。
f:id:momonga117:20180620021758p:plain

ライブラリファイルなんでしょうか?これ。

"org.jetbrains.kotlin:kotlin-stdlib-jre7のところを

org.jetbrains.kotlin:kotlin-stdlib-jdk7に変えろということらしいです。

で、取り替えて見たら、コンパイラエラーは消えました。

でもさ〜。親切にヒントくれるくらいなら、どうして最初からjdkの方にしてくれないのって思ってしまった。


というところで、
Kotlin始めて分かってきたことを2、3書くと。

1.クラスの外側に関数を置くことができる!
(Kotlinできる、Javaできない、Swiftできる)

これは、関数もクラスと同等のオブジェクトであるということらしい。しかし、Swift もできたとは驚き。
これの使用法については、考え中。

2.pair や triple が使える!
例えば、判定結果として、(x、y)の2つの数字を返すような関数を作る時、swiftで

       func Jadge(x: CGFloat, y: CGFloat) -> (i,j) {
         let i = x / 40
         let j = y / 40
         return (i,j)
      }

みたいなことをしていました。
swiftではタプルという仕組みらしいですが、Javaにはなく、最近は使っていませんでした。
Kotlinで使えるなら、また使って見たいなと思っているところです。

swift と Kotlin が似てるようで、微妙に違うのがかえって混乱を起こすことがあります。nil と null は似てるが、swiftの場合、必ず、?か!をつけますよね?
ところが、Kotlinは宣言時に?をつけるところは同じなのに、何もつけないで使ってたり、!!をつけてみたり。

迷っているのは、タッチイベントの時、event: MotionEvent? で渡ってくるので、このeventの扱いが悩みで・・・

    override fun onTouchEvent(event: MotionEvent?): Boolean {

            if (event != null) {
                val touch = event.action      // タッチの種類を格納する
                val x = event.x
                val y = event.y

                when (touch) {
                    MotionEvent.ACTION_UP -> {
                        performClick()
                        val width = width.toFloat()
                        val height = height.toFloat()


                        // 4種の判定
                        // 4コマンド(エントリー・設定・ラップ計測・記録)
                        val y1 = height / 2 - width * 0.4f
                        val y2 = height / 2 - width * 0.3f
                        val y3 = height / 2 + width * 0.3f
                        val y4 = height / 2 + width * 0.4f
                        val x1 = width * 0.05f
                        val x2 = width * 0.3f
                        val x3 = width * 0.7f
                        val x4 = width * 0.95f
                        // 判定
                        if (y > y1 && y < y2) {
                            if (x > x1 && x < x2) {
                                // エントリーメニューの呼び出し
                                entryViewRunnable!!.run()
                            } else if (x > x3 && x < x4) {
                                // 設定メニューの呼び出し
                                settingViewRunnable!!.run()
                }
            }
        return true
    }

このeventは null ではないはずです。多分・・・
でも、教科書的に、null の時は処理しないようにしました。
そしたら、event変数のところにメッセージが???

Smart cast to android.view.MotionEvent ← 何これ?って感じです。
MotionEventについてスマートキャストしてるよ。で、いい?
警告ではなさそうなので、このメッセージが出てもいいのかな。

まだまだ、Kotlin 見えてこないです。Kotlin霧中。(ダジャレ〜、わかるかな?)

ただ、シングルトンに関しては結構簡単に実現できそうなので、楽かも。

swift <-> Kotlin の移植性高いと嬉しいが、どうなんだろね。

第42回 Kotlin に static ないって、知ってた?

Kotlinさわって、1週間。

衝撃的だったのは、staticキーワードがないこと!
まあ、static あるかないかで言語を分ければ、きっとないほうが多いのだろう。(調査する気はありません)

ま、そこは、objectキーワードでなんとかしようかなと思ってますが、Kotlinを使っていこうかどうか検討中にちょっと便利だなと思った書き方がこれです。

val s = (1000/4).toDouble()

単に型変換をするだけですが、評価すべき式を()に入れて、 .to って打つと、.toDouble()だけでなく、いっぱい候補が出てきて、型変換が楽なのだ。

f:id:momonga117:20180616125734p:plain

あと、

Class の継承やってる時に出てきた書き方。

class MainMenu(context: Context) : View(context) {

Mainmenuクラスに渡されたコンテキストを継承元のViewクラスにそっくり渡しているのですが、面倒だなって思った反面、javaではあえて

public class MainMenu extends View {    
    // コンストラクタ
    public MainMenu(Context context) {
        super(context);
    }

というように式を書く必要があったので、楽かもしれない。

java と Kotlin、結論を出すのはまだ早いような気がします。
ま、googleの方針では、Kotlinにシフトしそうなので、Kotlin無視できないなと感じているところなんですが。

私の感覚です。異論は多々あると思いますが。

Java は、3つの中では、一番泥臭い。nullの扱いとか、暗黙の型変換とか。
あと、セミコロンが必要ってとこも、面倒かな。
Kotlin は、今回も触れたように static がないのが良くも悪くも大きな特徴で、null の扱いはswift の nil に似てるといえば似ているのかな。
(null は nil に似る・・・オヤジかー)

static をバンバン使ったswift プログラムから Kotlin への移植はちょっと困るかもしれない。
こういうケースはjava ですかね〜。

swift は外しようがないので、受け入れるしかないです。
私、obj-cやってないので。

当分、この3つを比較しながら、記事を書いていこうと思っています。

第41回 そろそろ、Kotlin?

 ふと、手にした本が、Kotlin書だったために、ブログの更新も忘れて、読みふけってました。
 Kotlin使って見たいけど、Kotlinの情報は少ない。
私にとってもメリットは、javaーswift の差よりも、kotlinーswiftの差の方が近いのではというその一点だけだったのですが。

例えば、
swift (変数名):(変数の型)
java (変数の型) (変数名)
kotlin(変数名):(変数の型)
ご覧のように、javaだけが反対なんですから。


次のようなjavaファイルをそのまま、kotlinファイルにコピペしてみました。

class MySize {

    // このクラスにはいろいろなビューのサイズを保管する

    //基本の画面サイズ
    public static int screenWidth;
    public static int screenHeight;

    // 初期化の必要なのはこの2つだけ
    public static void setWidth(int width) {
        screenWidth = width;
    }
    public static void setHeight(int height) {
        screenHeight = height;
    }

    // 各種パーツの長さ

    // 文字サイズ
    // TypedValue.COMPLEX_UNIT_PX で指定する
    public static class TextSize {
        // テキストサイズ
        static float normal = (float) screenWidth * 0.08f;
        static float small = (float) screenWidth * 0.05f;
    }


ペーストの直後、kotlinに変換しますか?みたいなメッセージが出て、そのままOKするとできたのが、次のファイル。

internal object MySize {

    // このクラスにはいろいろなビューのサイズを保管する

    //基本の画面サイズ
    var screenWidth: Int = 0
    var screenHeight: Int = 0

    // 初期化の必要なのはこの2つだけ
    fun setWidth(width: Int) {
        screenWidth = width
    }

    fun setHeight(height: Int) {
        screenHeight = height
    }

    // 各種パーツの長さ

    // 文字サイズ
    // TypedValue.COMPLEX_UNIT_PX で指定する
    object TextSize {
        // テキストサイズ
        internal var normal = screenWidth.toFloat() * 0.08f
        internal var small = screenWidth.toFloat() * 0.05f
    }

    // アプリ共通の線幅
    object LineWidth {
        // 線幅
        internal var normal = screenWidth / 100
        internal var thin = screenWidth / 150
        internal var thich = screenWidth / 80
    }

何がわかったかというと、javaではスタティックなクラスしか使えなかったのが、親切な変換をしてもらったら、classからobjectに変わってた。
static もきれいに排除されています。


で、よくよく、object構文を見てみると、もしかして、swiftのstructに当たるのでは?と思い始めたわけなんです。

swiftのstructは使ったことないです。解説を読んでみると、class も struct もインスタンスを作ることができるそうです。だから、structなくてもプログラム組めるじゃんって、無視してきました。でも、swift-structはもしかしたら、staticなクラスから、構造体への置き換えで使えるんじゃって思って、今、色々実験中です。案外うまくいくかもしれません。しばらくは、Kotlin でこんなことできた、こんなことしたよって、記事を書いてみたいと思います。

第40回 昔はサブルーチン、今は継承です。

 あるクラスの処理と同じことを別のクラスにも書くってよくありますよね。
2つや3つなら問題ないのでしょう。きっと。
 昔、BASICをさわった人ならわかるでしょうが、GOSUBなんてキーワードがありました。今でも、クラス内の処理で関数を作って呼び出すのは、GOSUB感覚ですね。(この文章のターゲットは明らかにBASIC世代です。すみません。)

 で、別クラスの処理が一部同じものだったら、どうしてますか?単純な方法なら、コピペで同じ処理を書いておく・・・ですよね?

f:id:momonga117:20180610114618p:plain

前回の、氏名登録画面ですが、選手クラスのオブジェクトが5個(Entry = true なら黄色背景)、そして、氏名登録ボタンは別クラスです。

実は、枠線部分の描画ルーチンはコピペしてます。

今のままで、動きます。なんの問題もないです。

ちょっとだけ、コード部分を。

import UIKit

class RunnerEntryView: UIView {
    
    /***
     * エントリー用のビュー一人分を作成する
     * 一人につき1個のビューが必要となる。
     */

    
    let myNumber: Int
    private var myName: String        //氏名表示
    private var entry: Bool = false  // 表示用
    
    // Runnerクラスへのアクセス’Handler
    private var entryOk: (() -> Void)?
    private var entryCancel: (() -> Void)?
    
    // 自前のイニシャライザ
    init(number: Int, name: String) {
        myNumber = number
        myName = name
        
        super.init(frame: MySettings.Entry.runnerEntryViewFrame(num: number))
        setBackGroundColor()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // ハンドラーのセッター
    public func setEntryOk(handler: @escaping () -> Void) {
        self.entryOk = handler
    }
    
    public func setEntryCancel(handler: @escaping () -> Void) {
        self.entryCancel = handler
    }
    
    private func setBackGroundColor() {
        // 背景色をセット、エントリー時には色を変える
        if entry {
            backgroundColor = MyColor.selectYellow
        } else {
            backgroundColor = MyColor.unselectedGrey
        }
    }
    
    override func draw(_ rect: CGRect) {
        
        let myWidth = MySettings.Entry.runnerErentryViewWidth()
        let myHeight = MySettings.Entry.runnerErentryViewHeight()
        let lineWidth = myWidth * 0.01
        
        // 番号枠,氏名枠の幅計算
        let numberWidth = myWidth * 0.22
        let nameWidth = myWidth - numberWidth - lineWidth * 3
        
        // コンテキストの取得
        let context = UIGraphicsGetCurrentContext()
    
        // 両サイド
        context?.setLineWidth(lineWidth)
        context?.move(to: CGPoint(x: lineWidth/2, y: 0))
        context?.addLine(to: CGPoint(x: lineWidth/2, y: myHeight))
        context?.move(to: CGPoint(x: myWidth-lineWidth/2, y: 0))
        context?.addLine(to: CGPoint(x: myWidth-lineWidth/2, y: myHeight))
        context?.strokePath()
        
        // 上下サイド
        context?.setLineWidth(lineWidth*0.3)
        context?.move(to: CGPoint(x: 0, y: lineWidth*0.15))
        context?.addLine(to: CGPoint(x: myWidth, y: lineWidth*0.15))
        context?.move(to: CGPoint(x: 0, y: myHeight - lineWidth*0.15))
        context?.addLine(to: CGPoint(x: myWidth, y: myHeight - lineWidth*0.15))
        context?.strokePath()
        
        // 見た目が悪いので、最初の分だけ上の線を太く。
        if myNumber == 1 {
            context?.setLineWidth(lineWidth*0.8)
            context?.move(to: CGPoint(x: 0, y: lineWidth*0.4))
            context?.addLine(to: CGPoint(x: myWidth, y: lineWidth*0.4))
        }
        // 中間の仕切り線
        context?.setLineWidth(lineWidth*0.6)
        context?.move(to: CGPoint(x: numberWidth, y: 0))
        context?.addLine(to: CGPoint(x: numberWidth, y: myHeight))
        context?.strokePath()
        
        // 番号の描画
        let textcolor = myName=="未登録" ? UIColor.lightGray : UIColor.black
        let numCenter: CGPoint = CGPoint(x:numberWidth*0.5, y: myHeight*0.5)
        centerString(string: String(myNumber), charSize: myHeight*0.5, center: numCenter, color: textcolor)
        
        // 氏名の描画
        let nameCenter: CGPoint = CGPoint(x: numberWidth + lineWidth + nameWidth * 0.5, y: myHeight*0.5)
        centerString(string: myName, charSize: myHeight*0.6, center: nameCenter, color: textcolor)
    }

    func centerString(string: String, charSize: CGFloat, center: CGPoint, color: UIColor) {
        // 文字列、文字サイズ、中央を示す座標、
        let stringAttributes: [NSAttributedStringKey : Any] = [
            NSAttributedStringKey.foregroundColor : color,
            NSAttributedStringKey.font : UIFont.systemFont(ofSize: charSize)
        ]
        let nsstr = string as NSString
        // サイズを取得
        let textSize: CGSize = nsstr.size(withAttributes: stringAttributes)
        let textTopLeft = CGPoint(x: center.x - textSize.width/2, y: center.y - textSize.height/2)
        // 文字列の描画
        nsstr.draw(in: CGRect(origin: textTopLeft, size: textSize), withAttributes: stringAttributes)
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        if entry {
            entry = false
            if entryCancel != nil {
                entryCancel!()      // Runnerクラスへのアクセス
            }
        } else {
            entry = true
            if entryOk != nil {
                entryOk!();  // Runnerクラスへのアクセス
            }
        }       
        setBackGroundColor()
    }
}
class AddNameButton: UIView {
    
    /**
     * スクロールビューにのせるパーツ
     * タップされた時の動作は、EntryViewに書く
     */
    
    // 氏名入力起動のためのハンドラー
    private var nameInputHandler: (() -> Void)?
    
    override init(frame: CGRect) {
        
        super.init(frame: frame)
        
        backgroundColor = UIColor.white
    
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // ハンドラーのセット
    func setnameInputHandler(handler: @escaping () -> Void) {
        self.nameInputHandler = handler
    }
    
    override func draw(_ rect: CGRect) {
        
        let myWidth = MySettings.Entry.runnerErentryViewWidth()
        let myHeight = MySettings.Entry.runnerErentryViewHeight()
        let lineWidth = myWidth * 0.01
        

        
        // コンテキストの取得
        let context = UIGraphicsGetCurrentContext()
        
        // 両サイド
        context?.setLineWidth(lineWidth)
        context?.move(to: CGPoint(x: lineWidth/2, y: 0))
        context?.addLine(to: CGPoint(x: lineWidth/2, y: myHeight))
        context?.move(to: CGPoint(x: myWidth-lineWidth/2, y: 0))
        context?.addLine(to: CGPoint(x: myWidth-lineWidth/2, y: myHeight))
        context?.strokePath()
        
        // 上下サイド
        context?.setLineWidth(lineWidth*0.3)
        context?.move(to: CGPoint(x: 0, y: lineWidth*0.15))
        context?.addLine(to: CGPoint(x: myWidth, y: lineWidth*0.15))
        context?.move(to: CGPoint(x: 0, y: myHeight - lineWidth*0.15))
        context?.addLine(to: CGPoint(x: myWidth, y: myHeight - lineWidth*0.15))
        context?.strokePath()
        
        // ボタン面
        let nameCenter: CGPoint = CGPoint(x: myWidth * 0.5, y: myHeight*0.5)
        centerString(string: "氏名登録 >>>", charSize: myHeight*0.5, center: nameCenter, color: UIColor.black)
        
    }

(ちょっとと言いながら長いです・・・すみません)

コピペがありありとわかりますよね。

でも、継承を使うとコピペ部分がいらなくなります。

この2つのクラスはUIViewを継承してるんですが、それなら、UIViewを継承した枠線だけを描く中間クラスを作り、そのクラスを継承してやれば、どちらのクラスにも、枠線を描くコードは不要になりますよね?
イメージとしては次のような感じです。

f:id:momonga117:20180610121815p:plain

まあ、できちゃったものをあえて書き換える必要はないんですが、継承の具体的な例として書いてみました。

以前作ったアプリで、6個くらいの縦につながった継承クラスを作りましたが、あれはやりすぎだったかなと反省。

でも、うまく継承を使うと、スッキリするかもです。
 

第39回 ソフトウェアキーボード・・・出ないよ〜(iOS)

最初は大丈夫だったんです。
TextFieldにbecomeFirstResponderを当てておくと、キーボードがでてきて、よかったって済ませてました。ところが・・・

f:id:momonga117:20180608212630p:plain

先ほどより、左の状態に!
何もいじってないし、実機でやると問題ない。iPad Airなどのシミュレータもちゃんとでてる。

そういえば、なんかAppleからUpdateのお知らせがあって、シミュレータも最新版にしたかもって思って、調べてみました。

問題はシミュレータ設定にありました。

f:id:momonga117:20180608213210p:plain

コネクト ハードウェア キーボード?

要するにMacのキーボードと接続するよって設定らしい。
これを外すと、右側のように元に戻りました。

突如変わったもので、あたふたしました。

なお、この設定は機種ごとに別々に設定するみたいです。


やっと、名前の登録ができるところまできました。
左が名前の登録中、右が登録後です。

f:id:momonga117:20180608214319p:plain