48487


Swift初心者です。勉強のために、二次元図形（円）を描画し、設置したtextfieldに中心座標を入力してreturnを押すと、その座標を中心として円が再描画されるという簡単なアプリを作っています。

アプリ起動時に円を描画することはできましたが、textfieldに座標を入力してもそれが反映されず、円がアプリ起動時の座標のまま変化しません。  
おそらく再描画する処理ができていないせいだと思いますが、どのタイミング・場所で再描画を行えば良いかが分かりません。  
どなたか教えていただけるとありがたいです。

以下がコードになります。

[code]

    //ViewController.swift
    import UIKit
    
    class ViewController: UIViewController, UITextFieldDelegate {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
    
            // テキスト毎にタグを設定
            xText.delegate = self
            xText.tag = 1
            yText.delegate = self
            yText.tag = 2
            self.view.backgroundColor = UIColor.gray // 全体背景をグレーに設定
        }
    
    
        @IBOutlet weak var xLabel: UILabel!
        @IBOutlet weak var yLabel: UILabel!
        @IBOutlet weak var xText: UITextField!
        @IBOutlet weak var yText: UITextField!
    
    
        let nextView = TestDraw()
    
    
    
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            // キーボードを閉じる
            textField.resignFirstResponder()
            // switch文により、xtextに入力した場合とytextに入力した場合を分ける
            switch textField.tag {
            case 1:
                // xTextに入力した値をStringからCGFloatに変換
                let xDouble: Double = atof(textField.text)
                let x: CGFloat = CGFloat(xDouble)
                // 入力された値をTestDraw.swift内に存在する座標変数へと格納
                nextView.x = x
                nextView.setNeedsDisplay()
    
                break
            case 2:
                // 同様のことをyTextについてもやる
                let yDouble: Double = atof(textField.text)
                let y:CGFloat = CGFloat(yDouble)
                nextView.y = y
                nextView.setNeedsDisplay()
    
                break
            default:
                print("エラー")
                break
            }
            return true
        }
    
    }
    
[/code]

* * *
[code]

    //TestDraw.swift
    import UIKit
    
    class TestDraw: UIView {
    
        /*
        // Only override draw() if you perform custom drawing.
        // An empty implementation adversely affects performance during animation.
        */
        // ViewControllerのXtextからの値を格納する変数
        var x:CGFloat = 150 // 初期値
        // 同じくyTextからの値を格納する変数
        var y:CGFloat = 100 // 初期値
    
    
        override func draw(_ rect: CGRect) {
            // Drawing code
            // 円の描画
            let circle = UIBezierPath(arcCenter: CGPoint(x: x, y: y), radius:80, startAngle: CGFloat(Double.pi*2.0*0.0/360.0), endAngle:CGFloat(Double.pi*2.0*360.0/360.0), clockwise: true)
            let circleColor = UIColor.red
            circleColor.setStroke() // 線の色を設定
            circleColor.setFill() // 内側塗りつぶし色の設定
            circle.fill() // 内側塗りつぶし
            circle.lineWidth = 4
            circle.stroke()
    
            print(x) // ログ
            print(y)
        }
    
    
    }
    
[/code]

* * *

この状態でsimulatorを起動させると、以下の画像のようになります。

* * *

[![画像の説明をここに入力](https://i.stack.imgur.com/cgNVV.png)](https://i.stack.imgur.com/cgNVV.png)

* * *

中心のX座標の初期値は150にしていますので、textfieldに200を入れreturnを押しても、以下の画像のように円の位置が変わっていません。

* * *

[![画像の説明をここに入力](https://i.stack.imgur.com/fam6A.png)](https://i.stack.imgur.com/fam6A.png)

* * *

TestDraw.swiftのdraw内にログを仕込んでみたところ、setNeedsDisplay()をしてもdrawが呼ばれていませんでした。


CC BY-SA 4.0
2018-09-17T12:01:43.837
48485
2018-09-17T14:13:34.573
2018-09-17T12:22:15.297
19110
30140
question
swift ios
UIViewを再描画させたい。


今のあなたのコードでは、`ViewController`クラス内のこの行:

[code]

    let nextView = TestDraw()
    
[/code]

で、
**`TestDraw`クラスの新しいインスタンスを作成している**のに、それをViewControllerのview階層に付け加える処理がどこにもありません。Viewはインスタンスを作成しただけでは画面には表示されません。

だのに赤い円が表示されていると言うことは、
**画面に表示されているのは、`nextView`とは別のインスタンス**だと言うことになります。おそらくStoryboard上に`TestDraw`を配置されたので、iOSが`ViewController`のインスタンスを作成する時に一緒に作成されたものでしょう。画面に表示されているのはそちらで、`nextView`に入っているのとは全く別のインスタンスです。

画面に表示されていないインスタンスのプロパティを変更しようが、`setNeedsDisplay()`を呼び出そうが、そのインスタンスは画面に表示されているのとは別物ですから、`draw(_:)`は呼ばれませんし、当然画面表示は更新されません。

Swiftはオブジェクト指向言語ですから、通常は同じクラスからいくつもインスタンスを作成することができます。自分は一体どのインスタンスを操作しているのかと言うのを意識していかないと、今後も思わぬところで苦労することになるでしょう。

このような混乱を避けるためにも、`let nextView = TestDraw()`のような形の宣言は、
**意図的に新規のインスタンスを作成したい時にしか使ってはいけない** と言うのを頭に叩き込んでおいた方が良いでしょう。

* * *

コードの修正を最小限で済ませるなら、先の行を次のように修正して見てください。

[code]

    @IBOutlet weak var nextView: TestDraw!
    
[/code]

もちろんStoryboard上に配置した`TestDraw`からこのIBOutletへの接続を正しく行う必要があります。こうしておけば、iOSが、実際に画面に表示されている方のインスタンスを`nextView`にセットしてくれます

* * *

ただ、ついでなので、あまりSwiftっぽくない書き方をしているところを改めてみてはいかがでしょうか。

まずは、`UIView`のサブクラスを作る際には、できるだけ`setNeedsDisplay()`のようなメソッドを直接呼び出さなくても良いように作ります。例えば、あなたの`TestDraw`クラスはこんな風に書きなおせます。

[code]

    class TestDraw: UIView {
    
        // ViewControllerのXtextからの値を格納する変数
        var x:CGFloat = 150 // 初期値
        {
            didSet {
                if oldValue != x {
                    self.setNeedsDisplay()
                }
            }
        }
        // 同じくyTextからの値を格納する変数
        var y:CGFloat = 100 // 初期値
        {
            didSet {
                if oldValue != y {
                    self.setNeedsDisplay()
                }
            }
        }
    
        override func draw(_ rect: CGRect) {
            // Drawing code
            // 円の描画
            let circle = UIBezierPath(arcCenter: CGPoint(x: x, y: y), radius:80, startAngle: CGFloat(.pi*2.0*0.0/360.0), endAngle:CGFloat(.pi*2.0*360.0/360.0), clockwise: true)
            let circleColor = UIColor.red
            circleColor.setStroke() // 線の色を設定
            circleColor.setFill() // 内側塗りつぶし色の設定
            circle.fill() // 内側塗りつぶし
            circle.lineWidth = 4
            circle.stroke()
    
            print(x) // ログ
            print(y)
        }
    }
    
[/code]

Swiftのドット記法で型名が省略できるところは省略してしまっていますが、この辺は趣味の問題もありますので、必ずしも真似する必要はないでしょう。

上記のようにすることで、

[code]

            nextView.x = x
            nextView.setNeedsDisplay()
    
[/code]

のように、プロパティ`x`(または`y`)の値を変更するたびに呼んでいた`setNeedsDisplay()`の呼び出しを、Viewの側で行ってくれるので、使う側は

[code]

            nextView.x = x
    
[/code]

と書くだけで良くなります。

* * *

また、他にも

  * `String`型から`Double`型への変換には普通`Double`のイニシャライザを使う
  * その際にOptionalに対処する方法として、Nil Coalescing Operator `??`を使える
  * Swiftではswitch文の`break`は普通は書かない

辺りを気にして`textFieldShouldReturn(_:)`を書き換えるとこんな感じになります。

[code]

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        // キーボードを閉じる
        textField.resignFirstResponder()
        // switch文により、xtextに入力した場合とytextに入力した場合を分ける
        switch textField.tag {
        case 1:
            // xTextに入力した値をStringからCGFloatに変換
            let xDouble = Double(textField.text ?? "") ?? 0.0
            let x = CGFloat(xDouble)
            // 入力された値をTestDrawクラス内に存在する座標変数へと格納
            nextView.x = x
    
        case 2:
            // 同様のことをyTextについてもやる
            let yDouble = Double(textField.text ?? "") ?? 0.0
            let y = CGFloat(yDouble)
            nextView.y = y
    
        default:
            print("エラー")
        }
        return true
    }
    
[/code]

`atof`の挙動と`Double.init(_:)`の挙動は違いますから、意図的に使用されていたのかもしれませんし、守った方が良いコーディングルールと言うより趣味の範疇に属する部分もありますが、ご参考にしていただけると幸いです。


CC BY-SA 4.0
2018-09-17T13:48:42.667
48487
2018-09-17T14:13:34.573
2018-09-17T14:13:34.573
13972
13972
48485
answer