LoginSignup
0
1

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

Last updated at Posted at 2023-06-21

はじめに

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

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

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

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

4dir.gif

共通コード

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

十字キーを押した時や回転ツマミを回した時のクリック感をフィードバックするため、UINotificationFeedbackGeneratorを使用しているが、iPadはバイブレータを内蔵しないから機能しない?

共通コードを表示
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)
}

十字ボタンコントロール

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

NKFourDirectionsクラス

NKFourDirectionsクラスを表示
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
    }
}

FourDirectionsラッパー

FourDirectionsラッパーを表示
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()
    }
}

使用例

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

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で指定する。

オプション
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)

デモ動画

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


よかったら使ってみてください。
以上

改訂履歴

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

  • 改訂2 2023.6.23
    ・共通コード最新化

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1