フリープログラマー日記

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

第15回 swift、ハンドラーとかクロージャとか

よくわからないんです。
初心者にとっては絶壁に登るような感覚ですよね?

なので、初心者目線で、説明して見ます。

クロージャというのは、「次にここから実行してね。」と伝える手段という見方でいいと思います。
あるエリアがタップされたら、次は(同じクラスの)ここだよってボタンに設定するのは、イベントハンドラーで、あちこちに例題があるし、割と簡単なんです。

でも、2つのクラスにまたがっているともう大変です。
呼び出したクラスのビューでタップされて、呼び出した側のある場所から実行したいとなると途端に難しくなる。

手順としては、次のようになります。

1.呼び出される側に、ハンドラーを置く。
2.呼び出し側から、このハンドラーに「どこに制御が移るか」を教える。この実態がクロージャ
3.呼び出される側のイベント発生時に、このハンドラーを使って、呼び出し側に制御を移す。

えーっ、これだけ〜?
そうなんです。これだけなんです。
初心者目線でしょ?簡単な気がしませんか?

じゃ、swift で書いていきますね。

まず、1の手順。
クラス内の変数にハンドラーを置くのでした。
ハンドラーを置くと同時に、ハンドラーのセッターも書いておきます。セッターはハンドラー1つにつき1つずつ作ります。
ハンドラーはプロパティーでセットしないで(多分できない?)セッターを使うのがよろしいようで。

私はあえて、private設定です。

import UIKit

class MainMenu: UIView {
        
    // メニューへ飛ぶためのハンドラー
    private var entryViewHandler: (() -> Void)?
    private var settingViewHandler: (() -> Void)?
    private var runningViewHandler: (() -> Void)?
    private var recordViewHandler: (() -> Void)?
    
    // 分岐先を渡すためのハンドラーのセッター(4種)
    func setEntryViewHandler(handler: @escaping () -> Void) {
        self.entryViewHandler = handler
    }
    
    func setsettingViewHandler(handler: @escaping () -> Void) {
        self.settingViewHandler = handler
    }
    
    func setRunningViewHandler(handler: @escaping () -> Void) {
        self.runningViewHandler = handler
    }
    
    func setRecordViewHandler(handler: @escaping () -> Void) {
        self.recordViewHandler = handler
    }

私のアプリでは4つの飛び先があるので、ハンドラーは4つです。
必要に応じて作ればいいですね。

続いて、2番のハンドラーのセット。

こちらは呼び出し側にあります。

    func showMainMenu() {
        
        // 以前のビューは消しておく
        for myview in self.view.subviews {
            myview.removeFromSuperview()
        }
        // MainMenu のインスタンスを作成。
        let mainMenu:MainMenu = MainMenu(frame: CGRect.zero)
       
        // ハンドラーのセット
        mainMenu.setEntryViewHandler(handler: showEntry)
        mainMenu.setsettingViewHandler (handler: showSetting)
        mainMenu.setRunningViewHandler (handler: showRunning)
        mainMenu.setRecordViewHandler (handler: showRecord)
        
        // ビューのサイズを設定
        mainMenu.frame = CGRect(x: 0, y: 0, width: width, height: height)
        
        mainMenu.center = CGPoint(x: width/2, y: height/2)
        self.view.addSubview(mainMenu)

    }

メインメニューのインスタンスを作った後に入れてます。
ハンドラーをセットするのも4個ですね。


最後にこのハンドラーの実行部分です。
結構ハマりやすいので気をつけてください。

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        // タッチアップの位置で判定する
        let touches = event?.allTouches
        let touch = touches?.first?.location(in: self)
        // 4コマンド(エントリー・設定・ラップ計測・記録)
        let y1 = viewHeight/2 - viewWidth*0.4
        let y2 = viewHeight/2 - viewWidth*0.3
        let y3 = viewHeight/2 + viewWidth*0.3
        let y4 = viewHeight/2 + viewWidth*0.4
        let x1 = viewWidth * 0.05
        let x2 = viewWidth * 0.3
        let x3 = viewWidth * 0.7
        let x4 = viewWidth * 0.95
        // 判定
        if touch!.y > y1 && touch!.y < y2 {
            if touch!.x > x1 && touch!.x < x2 {
                // エントリーメニューの呼び出し
                if entryViewHandler != nil {
                    entryViewHandler!()
                }
            } else if touch!.x > x3 && touch!.x < x4 {
                // 設定メニューの呼び出し
                if settingViewHandler != nil {
                    settingViewHandler!()
                }
            }
        } else if touch!.y > y3 && touch!.y < y4 {
            if touch!.x > x1 && touch!.x < x2 {
                // ラップ計測画面の呼び出し
                if runningViewHandler != nil {
                    runningViewHandler!()
                }
            } else if touch!.x > x3 && touch!.x < x4 {
                // 記録呼び出し画面へ
                if recordViewHandler != nil {
                    recordViewHandler!()
                }
            }
        }
    }

4箇所のタッチアップを判定してます。
ここで、if文でnilでないことを確認しないと、わけのわからないエラーが発生します。

Expression resolves to an unused l-value などというエラーに付き合いたくないなら、この構文をお勧めします。

もし、私のプログラムを実行させようと考えているなら、
EntryVIew.swiftなどの飛び先になるクラスを作ってお試しください。