iOS向けレガシーコントローラ（その1、十字ボタン）

# はじめに

iOSデバイスをPCのコントローラとして使うため、以下の3つのレガシーコントローラViewを作りました。
_数年前に作ったものですが、新たにSwiftUI向けのラッパーを作りましたので、今回まとめておきます。_


1. 十字ボタン
2. ジョイスティック
3. 回転ツマミ（ロータリーエンコーダー）

今回は、その1として、3つに共通なコード（共通コード）と十字ボタン。

十字ボタンは、2つのフェース『十字』『四角』が選べます。


<img width="215" alt="4dir.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/8639cd7f-84e1-1b3c-510d-ea3156317ee4.gif" />


# 共通コード

レガシーコントローラで共通で使う関数やExtension、その他諸々。

十字キーを押した時や回転ツマミを回した時のクリック感をフィードバックするため、[UINotificationFeedbackGenerator](https://qiita.com/xxminamixx/items/eee882e59de50e186850)を使用しているが、iPadはバイブレータを内蔵しないから機能しない？

<details><summary>共通コードを表示</summary><div>

```Swift:Misc.swift
//
//  Misc.swift
//

import UIKit
import AudioToolbox

var isPad: Bool { UIDevice.current.userInterfaceIdiom == .pad }

public protocol Applicable {}
public extension Applicable {
    @discardableResult
    func apply(block: (Self) -> Void) -> Self {
        block(self)
        return self
    }
}

extension NSObject: Applicable {}

public extension UIBezierPath {
    convenience init(circleCenter center: CGPoint, radius: CGFloat) {
        self.init(arcCenter: center, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
    }
    convenience init(linePoint point: CGPoint, to: CGPoint) {
        self.init()
        self.move(to: point)
        self.addLine(to: to)
    }
}

func tick() {
    if isPad {
        let soundID = SystemSoundID(1104)
        AudioServicesPlaySystemSound(soundID)
    } else if #available(iOS 10.0, *) {
        Feedbacker.impact(style: .light)
    } else {
        let soundID = SystemSoundID(1519)
        AudioServicesPlaySystemSound(soundID)
    }
}

struct Feedbacker {
    static func notice(type: UINotificationFeedbackGenerator.FeedbackType) {
        if #available(iOS 10.0, *) {
            let generator = UINotificationFeedbackGenerator()
            generator.prepare()
            generator.notificationOccurred(type)
        }
    }
    static func impact(style: UIImpactFeedbackGenerator.FeedbackStyle) {
        if #available(iOS 10.0, *) {
            let generator = UIImpactFeedbackGenerator(style: style)
            generator.prepare()
            generator.impactOccurred()
        }
    }
    static func selection() {
        if #available(iOS 10.0, *) {
            let generator = UISelectionFeedbackGenerator()
            generator.prepare()
            generator.selectionChanged()
        }
    }
    
}

extension CGFloat {
    var int: Int { Int(self) }
}
extension Int {
    var uint8: UInt8 { UInt8(self & 0xFF) }
    var float: CGFloat { CGFloat(self) }
}
extension Character {
    var asciiCode: UInt8 { self.asciiValue ?? 0 }
    var string: String { String(self) }
}
extension String {
    var asciiCode: UInt8 { self.first?.asciiCode ?? 0 }
    var asciiCodes: [UInt8] { self.map { $0.asciiCode } }
}

extension CGPoint {
    static let origin: CGPoint = .zero
    var isOrigin: Bool { self == .zero }
    var distance: CGFloat { (self.x * self.x) + (self.y * self.y) }
}


extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: CGFloat, specifier: String) {
        appendLiteral(String(format: specifier, value))
    }
}

func *(_ lhs: (x: CGFloat, y: CGFloat), _ rhs: CGFloat) -> CGPoint {
    CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
}
func *(_ lhs: CGPoint, _ rhs: Int) -> CGPoint {
    CGPoint(x: lhs.x * rhs.float, y: lhs.y * rhs.float)
}
func *(_ lhs: CGPoint, _ rhs: CGFloat) -> CGPoint {
    CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
}
func +(_ lhs: CGPoint, _ rhs: CGPoint) -> CGPoint {
    CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
```


</div></details>



# 十字ボタンコントロール

Storyboardで開発する場合は、下記のNKFourDirectionsクラスを使用する。SwiftUIで使用する場合は、この後のFourDirectionsラッパーを使用する。

### NKFourDirectionsクラス

<details><summary>NKFourDirectionsクラスを表示</summary><div>

```swift:NKFourDirections.swift
//
//  NKFourDirections.swift
//

import UIKit

@objc public protocol NKFourDirectionsDelegate {
    @objc optional func fourDirectionsTouchDown(_ nkFourDirections: NKFourDirections)
    @objc optional func fourDirectionsTouchUp(_ nkFourDirections: NKFourDirections)
    @objc optional func fourDirectionsTouchDownRepeat(_ nkFourDirections: NKFourDirections)
}
@IBDesignable
public class NKFourDirections: UIControl {

    public enum NKDirection {
        case none, north, south, west, east
    }
    public enum NKEvent {
        case none, touchDown, touchUp, touchDownRepeat
    }
    
    struct NKDirectionOptions: OptionSet {
        let rawValue: Int
        
        static let north =  NKDirectionOptions(rawValue: 1 << 0)
        static let south =  NKDirectionOptions(rawValue: 1 << 1)
        static let west  =  NKDirectionOptions(rawValue: 1 << 2)
        static let east  =  NKDirectionOptions(rawValue: 1 << 3)
        
        static let all: NKDirectionOptions = [.north, .south, .west, .east]
        
        //getter
        var description: String {
            let mask = [Self.north, .south, .west, .east]
            let maskStr = ["north", "south", "west", "east"]
            var result = ""
            for n in 0 ..< mask.count {
                if self.contains(mask[n]) {
                    result += result.isEmpty ? "[" : ", "
                    result += maskStr[n]
                }
            }
            result += result.isEmpty ? "[]" : "]"
            return result
        }
    }

    private(set) var direction: NKDirection = .none
    private(set) var repeatCount: Int = 0
    private(set) var lastEvent: NKEvent = .none
    private(set) var lastDir: NKDirection = .none
    var needDirection: NKDirectionOptions = .all

    @IBInspectable public var isHold: Bool = false
    @IBInspectable public var isRepeatable: Bool = false
    @IBInspectable public var repeatDelay: TimeInterval = .zero
    @IBInspectable public var repeatInterval: TimeInterval = .zero
    @IBInspectable public var needCrossFace: Bool = true
    @IBOutlet public weak var delegate: NKFourDirectionsDelegate?

    fileprivate var captionLabel: UILabel!
    var caption: String? {
        get { captionLabel.text ?? nil }
        set {
            if captionLabel == nil {
                captionLabel = UILabel(frame: bounds).apply {
                    $0.numberOfLines = 0
                    $0.textAlignment = .center
                    self.addSubview($0)
                }
            }
            captionLabel?.text = newValue
        }
    }

    fileprivate var diagonal: CGFloat = .zero
    fileprivate var boundsCenter: CGPoint = .zero
    fileprivate var isRepeating: Bool =  false
    fileprivate var lastDirection: NKDirection = .none
    fileprivate var directionView: UIView?
    fileprivate var repeatingTimer: Timer?

    public override func layoutSubviews() {
        super.layoutSubviews()
        diagonal = min(bounds.size.width, bounds.size.height) / 2 - 8
        captionLabel?.frame = self.frame
        boundsCenter = CGPoint(x: bounds.midY, y: bounds.midX)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        direction = .none
        lastEvent = .none
        lastDirection = .none
        directionView = nil
        repeatDelay = 1.0
        repeatInterval = 0.2
        repeatingTimer = nil
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func reset() {
        direction = .none
        isRepeating = false
        lastEvent = .none
        lastDirection = .none
        directionView?.removeFromSuperview()
        directionView = nil
        repeatingTimer = nil
        isSelected = false
        tick()
    }
    override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
        let location = touch.location(in: self)
        if !setValue(location, event: .touchDown) { return true }
        if isHold {
            if isSelected && lastDir == direction {
                isSelected = false
                tick()
            } else {
                isSelected = true
            }
        }
        lastDir = direction
        delegate?.fourDirectionsTouchDown?(self)
        return true
    }
    
    override public func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
        lastDir = direction
        let location = touch.location(in: self)
        if setValue(location, event: .touchDown) {
            delegate?.fourDirectionsTouchDown?(self)
        }
        return true
    }
    
    override public func endTracking(_ touch: UITouch?, with event: UIEvent?) {
        lastDir = direction
        if let touch = touch {
            let location = touch.location(in: self)
            setValue(location, event: .touchUp)
        }
        lastEvent = .touchUp
        if !isSelected {
            delegate?.fourDirectionsTouchUp?(self)

            isRepeating = false
            direction = .none
            
            directionView?.removeFromSuperview()
            directionView = nil
            repeatingTimer?.invalidate()
            repeatingTimer = nil
        }
    }
    
    override public func draw(_ rect: CGRect) {
        super.draw(rect)
        
        UIBezierPath(rect: bounds).apply {
            UIColor.systemBackground.setFill()
            $0.fill()
        }
        
        if needCrossFace {
            //draw cross
            let t31 = bounds.width / 3
            let t32 = bounds.width * 2 / 3
            let rect = bounds.insetBy(dx: 2, dy: 2)
            
            UIBezierPath().apply {
                let r: CGFloat = 4
                let c3: CGFloat = 0, c6: CGFloat = .pi / 2, c9: CGFloat = .pi, c12: CGFloat = .pi + c6
                $0.move(to: CGPoint(x: t31, y: rect.origin.y + r))
                $0.addArc(withCenter: CGPoint(x: t31 + r, y: rect.origin.y + r), radius: r, startAngle: c9, endAngle: c12 , clockwise: true) //1
                $0.addLine(to: CGPoint(x: t32 - r, y: rect.origin.y))
                $0.addArc(withCenter: CGPoint(x: t32 - r, y: rect.origin.y + r), radius: r, startAngle: c12, endAngle: c3, clockwise: true) //2
                $0.addLine(to: CGPoint(x: t32, y: t31 - r))
                $0.addArc(withCenter: CGPoint(x: t32 + r, y: t31 - r), radius: r, startAngle: c9, endAngle: c6, clockwise: false) //3
                $0.addLine(to: CGPoint(x: rect.size.width - r, y: t31))
                $0.addArc(withCenter: CGPoint(x: rect.size.width - r, y: t31 + r), radius: r, startAngle: c12, endAngle: c3, clockwise: true) //4
                $0.addLine(to: CGPoint(x: rect.size.width, y: t32 - r))
                $0.addArc(withCenter: CGPoint(x: rect.size.width - r, y: t32 - r), radius: r, startAngle: c3, endAngle: c6, clockwise: true) //5
                $0.addLine(to: CGPoint(x: t32 + r, y: t32))
                $0.addArc(withCenter: CGPoint(x: t32 + r, y: t32 + r), radius: r, startAngle: c12, endAngle: c9, clockwise: false) //6
                $0.addLine(to: CGPoint(x: t32, y: rect.size.height - r))
                $0.addArc(withCenter: CGPoint(x: t32 - r, y: rect.size.height - r), radius: r, startAngle: c3, endAngle: c6, clockwise: true) //7
                $0.addLine(to: CGPoint(x: t31 + r, y: rect.size.height))
                $0.addArc(withCenter: CGPoint(x: t31 + r, y: rect.size.height - r), radius: r, startAngle: c6, endAngle: c9, clockwise: true) //8
                $0.addLine(to: CGPoint(x: t31, y: t32 + r))
                $0.addArc(withCenter: CGPoint(x: t31 - r, y: t32 + r), radius: r, startAngle: c3, endAngle: c12, clockwise: false) //9
                $0.addLine(to: CGPoint(x: rect.origin.x + r, y: t32))
                $0.addArc(withCenter: CGPoint(x: rect.origin.x + r, y: t32 - r), radius: r, startAngle: c6, endAngle: c9, clockwise: true) //10
                $0.addLine(to: CGPoint(x: rect.origin.x, y: t31 + r))
                $0.addArc(withCenter: CGPoint(x: rect.origin.x + r, y: t31 + r), radius: r, startAngle: c9, endAngle: c12, clockwise: true) //11
                $0.addLine(to: CGPoint(x: t31 - r, y: t31))
                $0.addArc(withCenter: CGPoint(x: t31 - r, y: t31 - r), radius: r, startAngle: c6, endAngle: c3, clockwise: false) //12
                $0.addLine(to: CGPoint(x: t31, y: rect.origin.y + r))
                UIColor.label.setStroke()
                $0.lineWidth = 2
                $0.stroke()
            }
            
        } else {
            //draw rectangle
            UIBezierPath(rect: bounds.insetBy(dx: 8, dy: 8)).apply {
                UIColor.label.setStroke()
                $0.lineWidth = 2
                $0.stroke()
            }
            
            let x0 = bounds.midX - diagonal + 2
            let x1 = bounds.midX + diagonal - 2
            let y0 = bounds.midY - diagonal + 2
            let y1 = bounds.midY + diagonal - 2
            UIBezierPath(linePoint: CGPoint(x: x0, y: y0), to: CGPoint(x: x1, y: y1)).apply {
                UIColor.systemGray.setStroke()
                $0.lineWidth = 2
                $0.stroke()
            }
            UIBezierPath(linePoint: CGPoint(x: x1, y: y0), to: CGPoint(x: x0, y: y1)).apply {
                UIColor.systemGray.setStroke()
                $0.lineWidth = 2
                $0.stroke()
            }
        }
    }
}

private extension NKFourDirections {
    func drawDirection() {
        tick()
        directionView?.removeFromSuperview()
        directionView = nil
        directionView = UIView(frame: self.bounds).apply {
            $0.backgroundColor = .tintColor.withAlphaComponent(0.5)
            $0.isUserInteractionEnabled = false
            
            let outline = UIBezierPath()
            outline.move(to: boundsCenter)
            
            if needCrossFace {
                //cross draw
                let t31 = bounds.width / 3
                let t32 = bounds.width * 2 / 3
                let rect = bounds.insetBy(dx: 2, dy: 2)
                
                let r: CGFloat = 4
                let c3: CGFloat = 0, c6: CGFloat = .pi / 2, c9: CGFloat = .pi, c12: CGFloat = .pi + c6
                
                switch direction {
                    case .none: break
                    case .north:
                        outline.addLine(to: CGPoint(x: t31, y: t31))
                        outline.addArc(withCenter: CGPoint(x: t31 + r, y: rect.origin.y + r), radius: r, startAngle: c9, endAngle: c12 , clockwise: true) //1
                        outline.addLine(to: CGPoint(x: t32 - r, y: rect.origin.y))
                        outline.addArc(withCenter: CGPoint(x: t32 - r, y: rect.origin.y + r), radius: r, startAngle: c12, endAngle: c3, clockwise: true) //2
                        outline.addLine(to: CGPoint(x: t32, y: t31))
                    case .south:
                        outline.addLine(to: CGPoint(x: t32, y: t32))
                        outline.addArc(withCenter: CGPoint(x: t32 - r, y: rect.size.height - r), radius: r, startAngle: c3, endAngle: c6, clockwise: true) //7
                        outline.addLine(to: CGPoint(x: t31 + r, y: rect.size.height))
                        outline.addArc(withCenter: CGPoint(x: t31 + r, y: rect.size.height - r), radius: r, startAngle: c6, endAngle: c9, clockwise: true) //8
                        outline.addLine(to: CGPoint(x: t31, y: t32))
                    case .west:
                        outline.addLine(to: CGPoint(x: t31, y: t32))
                        outline.addArc(withCenter: CGPoint(x: rect.origin.x + r, y: t32 - r), radius: r, startAngle: c6, endAngle: c9, clockwise: true) //10
                        outline.addLine(to: CGPoint(x: rect.origin.x, y: t31 + r))
                        outline.addArc(withCenter: CGPoint(x: rect.origin.x + r, y: t31 + r), radius: r, startAngle: c9, endAngle: c12, clockwise: true) //11
                        outline.addLine(to: CGPoint(x: t31, y: t31))
                    case .east:
                        outline.addLine(to: CGPoint(x: t32, y: t31))
                        outline.addArc(withCenter: CGPoint(x: rect.size.width - r, y: t31 + r), radius: r, startAngle: c12, endAngle: c3, clockwise: true) //4
                        outline.addLine(to: CGPoint(x: rect.size.width, y: t32 - r))
                        outline.addArc(withCenter: CGPoint(x: rect.size.width - r, y: t32 - r), radius: r, startAngle: c3, endAngle: c6, clockwise: true) //5
                        outline.addLine(to: CGPoint(x: t32, y: t32))
                }
                outline.close()
                
            } else {
                let x0 = bounds.minX + 8
                let x1 = bounds.maxX - 8
                let y0 = bounds.minY + 8
                let y1 = bounds.maxY - 8
                
                switch direction {
                    case .none: break
                    case .north:
                        outline.addLine(to: CGPoint(x: x0, y: y0))
                        outline.addLine(to: CGPoint(x: x1, y: y0))
                    case .south:
                        outline.addLine(to: CGPoint(x: x1, y: y1))
                        outline.addLine(to: CGPoint(x: x0, y: y1))
                    case .west:
                        outline.addLine(to: CGPoint(x: x0, y: y1))
                        outline.addLine(to: CGPoint(x: x0, y: y0))
                    case .east:
                        outline.addLine(to: CGPoint(x: x1, y: y0))
                        outline.addLine(to: CGPoint(x: x1, y: y1))
                }
                outline.close()
            }
            
            let maskLayer = CAShapeLayer().apply {
                $0.path = outline.cgPath
            }
            $0.layer.mask = maskLayer
            $0.center = boundsCenter
            
            self.addSubview($0)
        }
        
        
    }
    
    func timeout(_ timer: Timer?) {
        if !self.isRepeatable || !self.isRepeating || self.lastEvent == .none {
            timer?.invalidate()
            if !isSelected {
                directionView?.removeFromSuperview()
                directionView = nil
            }
            return
        }
        
        self.lastEvent = .touchDownRepeat
        self.repeatCount += 1
        self.delegate?.fourDirectionsTouchDownRepeat?(self)
        self.drawDirection()
        UIView.animate(withDuration: 0.2, animations: {
            self.directionView?.alpha = 0
        })
        repeatingTimer = Timer.scheduledTimer(withTimeInterval: repeatInterval, repeats: false, block: { self.timeout($0) })
    }
    
    @discardableResult
    func setValue(_ location: CGPoint, event: NKEvent) -> Bool {
        let previousDirection = direction
        
        lastEvent = event
        
        let degree = degreesCW(location)
        switch degree {
            case 0.000 ..< 0.125: direction = .north
            case 0.125 ..< 0.375: direction = .east
            case 0.375 ..< 0.625: direction = .south
            case 0.625 ..< 0.875: direction = .west
            case 0.875 ..< 1.000: direction = .north
            default: direction = .none
        }
        
        if direction == .north && !needDirection.contains(.north) { direction = .none }
        if direction == .south && !needDirection.contains(.south) { direction = .none }
        if direction == .east && !needDirection.contains(.east) { direction = .none }
        if direction == .west && !needDirection.contains(.west) { direction = .none }

        if direction == previousDirection { return false }
        
        drawDirection()
        
        if direction != .none && event == .touchDown && isRepeatable {
            repeatCount = 0
            if !isRepeating {
                isRepeating = true
                repeatingTimer = Timer.scheduledTimer(withTimeInterval: repeatDelay, repeats: false, block: { self.timeout($0) })
            }
        }
        
        return true
    }
    func degreesCW(_ point: CGPoint) -> CGFloat {
        var r = atan2(point.y - bounds.midY, point.x - bounds.midX)
        r += .pi / 2
        if r < 0 { r += 2 * .pi }
        return floor(1000 * r / (2 * .pi)) / 1000
    }
}
```

</div></details>

### FourDirectionsラッパー

<details><summary>FourDirectionsラッパーを表示</summary><div>

```swift:FourDirections.swift
//
//  FourDirections.swift
//
//      Wrapper for NKFourDirections
//

import SwiftUI

struct FourDirections: UIViewRepresentable {

    typealias Direction = NKFourDirections.NKDirection
    typealias Event = NKFourDirections.NKEvent
    typealias DirectionOptions = NKFourDirections.NKDirectionOptions

    private var caption: String? = nil
    private var isHold: Bool? = nil
    private var crossFace: Bool? = nil
    private var isRepeatable: Bool? = nil
    private var repeatDelay: TimeInterval? = nil
    private var repeatInterval: TimeInterval? = nil
    private var needDirection: DirectionOptions? = nil

    typealias Callback = (Direction, _ repeatCount: Int, Event) -> Void
    private var onChange: Callback?

    func makeUIView(context: Context) -> NKFourDirections {
        let view = NKFourDirections(frame: CGRect.zero)
        view.delegate = context.coordinator
        return view
    }
        
    func updateUIView(_ fourDir: NKFourDirections, context: Context) {
        if let caption = caption { fourDir.caption = caption }
        if let isHold = isHold { fourDir.isHold = isHold }
        if let crossFace = crossFace { fourDir.needCrossFace = crossFace }
        if let isRepeatable = isRepeatable { fourDir.isRepeatable = isRepeatable }
        if let repeatDelay = repeatDelay { fourDir.repeatDelay = repeatDelay }
        if let repeatInterval = repeatInterval { fourDir.repeatInterval = repeatInterval }
        if let needDirection = needDirection { fourDir.needDirection = needDirection }

        context.coordinator.onChangeCallback = onChange
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func onChange(callback: @escaping Callback) -> Self {
        var view = self
        view.onChange = callback
        return view
    }

    class Coordinator: NSObject, NKFourDirectionsDelegate {
        let parent: FourDirections
        var onChangeCallback: Callback?
        
        init(_ nkFourDirections: FourDirections) {
            parent = nkFourDirections
        }
        
        func fourDirectionsTouchDown(_ fourDir: NKFourDirections) {
            onChangeCallback?(fourDir.direction, fourDir.repeatCount, fourDir.lastEvent)
        }
        func fourDirectionsTouchUp(_ fourDir: NKFourDirections) {
            onChangeCallback?(fourDir.direction, fourDir.repeatCount, fourDir.lastEvent)
        }
        func fourDirectionsTouchDownRepeat(_ fourDir: NKFourDirections) {
            onChangeCallback?(fourDir.direction, fourDir.repeatCount, fourDir.lastEvent)
        }

    }
}

extension FourDirections {
    func caption(_ caption: String) -> Self {
        var view = self
        view.caption = caption
        return view
    }
    func hold(_ hold: Bool) -> Self {
        var view = self
        view.isHold = hold
        return view
    }
    func crossFace(_ crossFace: Bool) -> Self {
        var view = self
        view.crossFace = crossFace
        return view
    }
    func repeatable(_ repeatable: Bool, interval: TimeInterval = 0.2, delay: TimeInterval = 1.0) -> Self {
        var view = self
        view.isRepeatable = repeatable
        view.repeatDelay = delay
        view.repeatInterval = interval
        return view
    }
    func needDirection(_ needDirection: DirectionOptions) -> Self {
        var view = self
        view.needDirection = needDirection
        return view
    }
}

class FourDirectionsViewModel: ObservableObject {
    private weak var fourDirections: NKFourDirections?
    
    func setFourDirections(_ fourDirections: NKFourDirections) {
        self.fourDirections = fourDirections
    }
    
    func reset() {
        fourDirections?.reset()
    }
}
```

</div></details>


# 使用例

SwiftUIでの使用例を以下に示す。Previewでもちゃんと動作します。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    @State var event: FourDirections.Event = .none
    @State var dir: FourDirections.Direction = .none
    @State var repeatCount: Int = 0
    var body: some View {
        VStack {
            FourDirections()
                .caption("four dir")
                .repeatable(true)
                .onChange { dir, repeatCount, event in
                    self.dir = dir
                    self.repeatCount = repeatCount
                    self.event = event
                }
                .frame(width:200, height: 200)

            let text = "event: \(event), dir: \(dir), repeatCount: \(repeatCount)"
            Text(text)
                .lineLimit(1)
                .minimumScaleFactor(0.5)
                .frame(height: 40)
            
            FourDirections()
                .crossFace(false)
                .hold(true)
                .onChange { dir, repeatCount, event in
                    self.dir = dir
                    self.repeatCount = repeatCount
                    self.event = event
                }
                .frame(width:200, height: 200)
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

以下のオプションを用意しており、view modifierで指定する。

```swift:オプション
FourDirections()

	//四角いフェースとする場合はfalse。デフォルトはtrueで十字フェース
	.crossFace(false)

	//ボタンの中央に表示する見出し
	.caption("four dir") 

	//押し続けた時に自動リピートする場合にtrue。
	//リピート開始までの遅延時間とリピート間隔を指定できる
	.repeatable(true, interval: 0.2, delay: 1.0)

	//タップして指を離しても『押したまま』とする場合にtrue。再度タップすると戻る
	.hold(true)

	//必要とする方向を限定する場合に指定する。省略すると全方向
	.needDirection([.north, .south]) //上下のみ


	//イベントハンドラ
	//dir: タップした方向、repeatCount: リピート回数、event: イベント
    //dir: north, south, west, east
    //event: touchDown, touchUp, touchDownRepeat
	.onChange { dir, repeatCount, event in <code> }


	//サイズを必ず指定して正方形とすること
	.frame(width:200, height: 200)
```


# デモ動画

前項の使用例のコードを実行させた動画
<p>

<iframe width="214" height="446" src="https://www.youtube.com/embed/GAxID3C7USs?rel=0" title="iOS向けレガシーコントローラを作る（その1、十字ボタン）" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

</p>
<hr>
よかったら使ってみてください。<br>
以上

## 改訂履歴

- 改訂1　2023.6.22 <br>・FourDirectionsラッパーのイベントハンドラを集約<br>・デモ動画をシミュレータ画面に変更

- 改訂2　2023.6.23 <br>・共通コード最新化

- 改訂3　2023.6.25 <br>・必要とする十字方向を指定できるようにした。指定されていない方向のイベントは通知しない