20200912のSwiftに関する記事は8件です。

本日学んだ、オプショナル型の基礎。【 ! の混同】

本日学んだ、オプショナル型の基礎をまとめます。
自分用の記録用の皮算用。

  ! を混同していた。

"!"はオプショナルではない、nilを許さないというのは間違いです。
このことも勘違いしている人が多いかもしれません。

勘違いしていました。

変数の宣言で型に !をつける場合、その変数は「オプショナル型」

なので、強制的アンラップの!とは別物らしい。

    // 変数の宣言で型に ! をつける場合、その変数は「オプショナル型」
    // (= 強制的アンラップの ! とは別物)
    var hoge: String! // nil

 暗黙的アンラップ型 (= IUO)

Implicitly Unwrapped Optional

  • オプショナル型の1つ (= nliを許容)
  • 使用するとき、必ず強制的アンラップ!をする。
  • つまり、自動でアンラップをしてくれる

但し、nilでアンラップすると、アプリが落ちる?ため、
100%絶対に値がある(= nilでない) 場合のみ、使用可能。

最初は nil で宣言したいが、使うときには絶対に値が入っている、という場合に使用。

 他のアンラップ方法

debugPrint(error?.localizedDescription ?? "")

guard letif letとかの「オプショナル バインディング」の他に、

  • オプショナル チェイニング(?)
  • nil結合演算子 (??)

があるみたいなので、以下にメモっておく。

 オプショナル チェイニング? (= Optional Chaining)

- 値がある場合

アンラップする。

但し、それに続くプロパティやメソッドの戻り値は「オプショナル型」になる。
(-> nilの場合 nil を返すため)

- nilの場合

nil を返す。それに続く処理をすべてキャンセル (-> なので安全)

 nil結合演算子? (nil-coalescing)

A ?? B ... Aに値があるとAをアンラップ。 Aが nil だとBを返す。  

SwiftのNil結合演算子

おしまい。

 参考サイト

どこよりも分かりやすいSwiftの"?"と"!"
-> LGTMの数が異常。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIとCombineの練習がてらmacOS向けのピアノアプリ作ってみた

はじめに

SwiftUIとCombine、Swiftコーダーのみなさまは勉強なさっているでしょうか?私は今まで近いうちに勉強しよう明日やろうと言いながら、アプリのメンテを優先してずっと後回しにしてきていました。しかし、ついに会社でも使う機会が訪れ、ようやく重い腰を上げ勉強し始めました。ただ、文法や仕組みを学ぶだけでは全く身につかないなぁと思ったので、作りたいものを決めて実装しながら学ぶことにしました。

今回は、ある程度複雑な形状のViewを複数組み合わせる必要があり、Viewに対するアクションに基づいてリアルタイムに動作が変わるものが良さそうだったので、ピアノのシミュレーターアプリを作る事にしました。

成果物 PianoSample

⬇️デモ動画リンク
Piano Demo
ウィンドウにピアノの鍵盤が表示され、クリックやドラッグをする事で実際に音を鳴らして弾くことができるアプリを作りました。開発にかかった時間は10時間くらいだったと思います。コードはGitHubに置いてあります。
https://github.com/Kyome22/PianoSample

学び(SwiftUI編)

「ピアノの鍵盤をドラッグしている時にカーソルが鍵盤の上にある時はViewが押されているよう描画し、カーソルが鍵盤の上から外れた場合にはViewが押されていないよう描画する」という機能を実装する際にかなりハマりました。

関門1

  • Viewに対するアクションのイベントハンドラには、onTapGestureonLongPressGestureはあるが、タッチの開始と終了を検知して処理を実行する術がデフォルトで容易されていない

この問題に対処するため、DragGestureを用いてドラッグ中のタッチ点の座標を取得して、Viewに対する当たり判定を行う事にしました。

@State var location: CGPoint = .zero

let drag = DragGesture(minimumDistance: 0.0, coordinateSpace: .global)
    .onChanged({ drag in
        self.location = drag.location
    })
    .onEnded({ _ in
        self.location = .zero
    })

関門2

  • Viewでは@Stateをつけた変数を宣言すると、その値を変更することが可能になり、変更された際にViewが再描画される
  • Viewの再描画中には@Stateの変数の値を変更してはいけない

この二つの法則から、ある変数に従ってViewを描画したいが、その変数の更新はViewの描画時にしかできないという状態に陥りました。具体的には、上の当たり判定をスクラッチで実装するために、Viewのサイズを取得したかったのですが、それをするにはGeometryReaderなるものでViewの要素を包んでGeometryProxyを経由してViewのサイズを取得する必要がありました。

var body: some View {
    GeometryReader { geometry in
        let frame = geometry.frame(in: .global) // とか
        // なんかView
    }
}

そこで、GeometryProxyを用いてViewのサイズを取得して、当たり判定をしようと思ったのですが、上のジレンマ状態に陥ったのです。

struct SomethingView: View {
    @State var isHit: Bool = false
    @Binding var location: CGPoint

    var body: some View {
        GeometryReader { geometry in
            self.hitTest(geometry: geometry)
        }
    }

    func hitTest(geometry: GeometryProxy) -> some View {
        let frame = geometry.frame(in: .global)
        isHit = frame.contains(location) // こんな感じでisHitを更新しようとするとエラーになる
        if isHit {
            return Color.black
        } else {
            return Color(white: 0.2)
        }
    }

}

isHithitTestのローカル変数にすればいいのでは?と思うかもしれませんが、後でこのisHitの状態に応じて音を再生/停止するイベントを発火したかったので、外で持つ必要がありました。

この問題に対しては、Viewで何らかのModelを持ち、その中でisHitを持つという方法で解決しました。

関門3

  • DragGesture.Value.locationは左上が原点
  • GeometryProxyで取得できるframeは左下が原点
  • ShapeのPathのcontains(CGPoint)による当たり判定は上下が反転している(?)

ここで、macOSとiOSの座標系のずれの問題が発生して、当たり判定がかなり厄介になっていました。SwiftUIの座標系が左上原点なのか左下原点なのか、使う要素のメソッドによりブレているのが辛かったです。

この問題は、愚直にViewの高さを取得してY座標を反転したり、Pathの上下を一時的に反転して当たり判定を行ったりして解決しました。

テクニック

Viewの四隅を丸くしたい場合は、cornerRadius()を使えばいいのですが、四隅の一部だけを丸くしたい場合には簡単に実装できる方法がありません。そこで、カスタムのShapeを実装して好きな形のViewを描画するようにしました。Pathを用いて特殊な形状のViewへの当たり判定も実装できるので今回は一石二鳥でした。

struct CustomShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        // ↓ここら辺のメソッドを使ってPathを描く
        path.move(to: CGPoint)
        path.addArc()
        path.addLine(to: CGPoint)
        path.closeSubpath()
        return path
    }
}

基本的にはUIBezierPathやNSBezierPathと同じ扱い方です。座標系には気をつけましょう。

学び(Combine編)

今回のピアノ実装の最大の難点は、 SwiftUIのViewにonTapGestureonLongPressGestureのようなイベントハンドラとしてタッチの開始や終了を起点に発火するやつがなかったことでした。自前でイベントハンドラを作らねばなりません。そのため、当たり判定が更新された際に、音の再生停止をするために命令を伝達するルートをCombineで作りました。必要なことは以下の3つでした。

当たり判定の更新時に音の再生停止を伝えるSubjectを用意してsend()する仕組みを用意

class CustomModel: ObservableObject {
    let subject = PassthroughSubject<Bool, Never>()
    var isHit: Bool = false {
        didSet {
            if oldValue != isHit {
                if isHit {
                    play()
                } else {
                    stop()
                }
            }
        }
    }

    func play() {
        subject.send(true)
    }

    func stop() {
        subject.send(false)
    }
}

subjectのsend()を受け取ってイベントハンドラを発火する仕組みを用意

struct CustomView: View {
    let model: CustomModel

    init(model: CustomModel) {
        self.model = model
    }

    // 中略

    func onEvent(handler: @escaping ((Bool) -> Void)) -> some View {
        return self.onReceive(model.subject, perform: { (flag) in
            handler(flag)
        })
    }
}

イベントハンドラの登録

struct HogeView: View {
    var body: some View {
        CustomView(model: CustomModel())
            .onEvent(handler: { (flag) in
                // flagにしたがって音を鳴らす処理
                print("ド♪")
            })
    }
}

学び(AVAudioUnitSampler編)

ピアノの音を鳴らすには、AVAudioUnitSamplerを用いました。基本的な方法はこちらの記事にまとめてあります。今回は単音ではなく和音も鳴らせるようにしたかったので、AVAudioUnitSamplerを複数用意してAVAudioMixerNodeでミキシングする必要があるのかなと予想していたのですが、一つのAVAudioUnitSamplerで複数のノートを同時に鳴らすことができました。

ただ、かなり実装でてこずった点がありまして、普通にAVAudioUnitSamplerを使ってノートを再生するだけでは、どうしてもノートを停止した後にブツッというノイズが入るのです。AVAudio関連でフェード・イン/アウトする方法をかなり調べましたが、AVAudioPCMBufferを使って実際にフェード・イン/アウトするような音源をその場で生成するしかありませんでした(これはかなり計算コストを要するしノートごとに音源を作らねばならないのでかなり厄介です)。いろいろ試行錯誤したところAVAudioUnitSamplerにSoundFontを読み込ませれば、ノートの停止時にノイズが入らないことが判明しました。

ちなみに、音源ファイル(mp3とかcafとか)を読み込んで鳴らす手法を取らなかった理由ですが、ピアノの鍵盤を押している間は途切れることなく音が再生されるようにしたかったため、固定長の音源ファイルを使いたくなかったからです。

所感

SwiftUIを使ったViewのレイアウトはStoryboardを使うよりもわかりやすく、コードで全て完結できるのでメンテナンスがしやすそうだと思いました。一方で、少し複雑なViewを定義すると、型推論が解決できずにタイムアウトしてしまい、どこが不具合の原因なのかもわからないということに陥りやすく、開発に慣れるまでは修正が難しかったです。基本的にはViewは小さな単位で切り分けて定義するようにすると良さそうですね。

Combineについては、どこからがCombineの機能で、どこからがSwiftUIの機能なのかまだ曖昧ではっきりわかっていないので、勉強していきたいですね。おそらく代表的な情報の伝達パターンをいくつか覚えてしまえば、綺麗にイベントを捌けるようになりそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コードでSwiftのautolayoutを書く

Storyboardで画面のレイアウトやUI部品をグラフィカルに配置できますが、チームで協同作業する時や細かいレイアウトの調整が発生する場合はStoryboardだとどうしても支障が出ます。

autolayoutの標準APIについて、AppleがNSLayoutConstraintNSLayoutAnchor(ios9~)、Visual Format Language(VFL)を提供しています。NSLayoutAnchorNSLayoutConstraintよりコード量が少なくなり、制約・声明もはっきりするようになっています。VFLはアスキーアートのように制約を定義することができる記法ですが、学習コストが高くかつリテラルな箇所が多いため、実行時にエラーが起こる恐れがあります(本文ではVFLパターンを紹介しません)。

1、NSLayoutConstraint

NSLayoutConstraintを使用するには、各々初期化する必要があります。
制約する属性ごとにNSLayoutConstraintを作る必要があるため、コード量が増えます。
初期化メソッド:NSLayoutConstraint.init()

//UIImageViewを作成し、viewに追加する
let layoutIcon = "layoutIcon"
let layoutImg = UIImage(named: layoutIcon)
let layoutView = UIImageView(image: layoutImg)
layoutView.backgroundColor = .green
layoutView.layer.cornerRadius = 10
//AutoresizingMaskをAutoLayoutの制約に置き換えるかどうか指定する値(必ずfalse)
layoutView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(layoutView)

//制約を追加 top:50 
//パラメーター説明 item:制約オブジェクト attribute:制約する属性 relatedBy:制約タイプ toItem:制約の相手 attribute:制約相手に使用する属性 multiplier:乗数値 constant:定数値
let topConstraint = NSLayoutConstraint.init(item: layoutView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 50.0)
//制約は定義した後、activateする必要あり
topConstraint.isActive = true

//制約を追加 left:50
let leftConstraint = NSLayoutConstraint.init(item: layoutView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1.0, constant: 50.0)
leftConstraint.isActive = true

//制約を追加 width:70
let widthConstraint = NSLayoutConstraint.init(item: layoutView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 70.0)
widthConstraint.isActive = true

//制約を追加 height:70
let heightConstraint = NSLayoutConstraint.init(item: layoutView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 70.0)
heightConstraint.isActive = true

2、NSLayoutAnchor

iOS9からはNSLayoutAnchorを使いNSLayoutConstraintオブジェクトを作ることができました。ただ直接にNSLayoutConstraintオブジェクトを作ることではなく、UIViewあるいはその子クラスやUILayoutGuideのあるanchorプロパティー値を利用します。これらのプロパティーはautolayout中の主要NSLayoutAttributeに相当します。
そのため、NSLayoutAnchorの子クラスでNSLayoutAttributeを設定してもいいです。
制約メソッド:xxxView.xxxAnchor.constraint( )

//紫色のUIImageViewを作成し、viewに追加する
let purLayoutIcon = "layoutIcon"
let purLayoutImg = UIImage(named: purLayoutIcon)
let purLayoutView = UIImageView(image: purLayoutImg)
purLayoutView.backgroundColor = .purple
purLayoutView.layer.cornerRadius = 10
//AutoresizingMaskをAutoLayoutの制約に置き換えるかどうか指定する値(必ずfalse)
purLayoutView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(purLayoutView)

//制約を追加 top:150 left:50 width:70 height:70
purLayoutView.topAnchor.constraint(equalTo: view.topAnchor, constant: 150.0).isActive = true
purLayoutView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 50.0).isActive = true
purLayoutView.widthAnchor.constraint(equalToConstant: 70.0).isActive = true
purLayoutView.heightAnchor.constraint(equalToConstant: 70.0).isActive = true

NSLayoutConstraintよりNSLayoutAnchorのほうは簡潔です。

3、ScrollView中のautolayout

ScrollViewはセルフのcontentSizeがあるため、autolayout使用時に留意したほうがいいです。
ScrollViewの子viewのサイズは即ちcontentSizeです。
制約はScrollViewの完全なレイアウトを決める必要があり、上下左右ともに必要です。また、ScrollViewの子viewの最大サイズも必ず制約条件を通じて確定値を得られます。

//scrollViewを作成し、viewに追加する
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.backgroundColor = .cyan
//ページ単位のスクロールを可能する
scrollView.isPagingEnabled = true
view.addSubview(scrollView)
scrollView.topAnchor.constraint(equalTo: purLayoutView.bottomAnchor, constant: 30.0).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
//safeAreaのボトム距離を使う
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true

let bgColor = [UIColor.red,UIColor.green,UIColor.blue]

var leftView:UIView? = nil
    for i in 0..<3{
          let view = UIView()
          view.translatesAutoresizingMaskIntoConstraints = false
          view.backgroundColor = bgColor[i]
          scrollView.addSubview(view)
          if let left = leftView{
            view.leftAnchor.constraint(equalTo: left.rightAnchor, constant: 0).isActive = true
          }else{
            view.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 0).isActive = true
          }
          view.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0).isActive = true
          view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0).isActive = true
          view.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 1.0).isActive = true
          view.heightAnchor.constraint(equalTo: scrollView.heightAnchor, multiplier: 1.0).isActive = true
          leftView = view
        }
        //全ての子viewは上、下、左がscrollViewを相手として制約していたが、右のほうがまだ
        //右方向の制約を追加
        if let left = leftView{
          left.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: 0).isActive = true
        }

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Core Dataのtransformableな属性をセキュアに実装する

Core Dataのエンティティが持つ属性 (attribute) の型は整数、文字列、日付などいくつかの決まったものしかとることができませんが、Transformableを指定することで任意の型を NSData に変換して保存することができるようになっています。このとき保存したい型と NSData の変換を担うのがvalue transformerです。

Core Dataのtransformableを利用しているプロジェクトをXcode 12でビルドしたときに、以下のような警告が出るようになりました。

warning: Misconfigured Property: <NSManagedObjectのサブクラス>.<プロパティ> is using a nil or insecure value transformer. Please switch to NSSecureUnarchiveFromDataTransformerName or a custom NSValueTransformer subclass of NSSecureUnarchiveFromDataTransformer

これは、これまでのvalue transformerが NSCoding を使ったものであるため非推奨になっており、 NSSecureCoding をベースとした NSSecureUnarchiveFromDataTransformer に置き換える必要があるということです。

今回この警告の対応のために調べた、 NSSecureUnarchiveFromDataTransformer を使ってtransformableな属性をセキュアに実装する方法をまとめました。

カスタムクラスが不要なパターン

NSSecureUnarchiveFromDataTransformer が扱える型は allowedTopLevelClasses で規定されていて、以下の通りです。

NSArray, NSDictionary, NSSet, NSString, NSNumber, NSDate, NSData, NSURL, NSUUID, NSNull

これらの型の組み合わせで表現できるもの、例えば文字列の配列 [String] の属性であれば、モデルエディタでTransformerの欄に NSSecureUnarchiveFromDataTransformerName を指定するだけでOKです。

カスタムクラスが必要なパターン

上に挙げた型以外の NSSecureCoding に準拠した標準の型(UIColor など)や、NSSecureCoding に準拠した独自の型を使う場合は、 NSSecureUnarchiveFromDataTransformer を継承したカスタムクラスを実装する必要があります。

// Core Dataで利用するために @objc が必要です
@objc final class SecureValueTransformer: NSSecureUnarchiveFromDataTransformer {

    // クラス名をvalue transformerのnameとします
    static let name = NSValueTransformerName(rawValue: String(describing: SecureValueTransformer.self))

    // superが返すクラスの配列に追加する形で使用するクラスを指定します
    override static var allowedTopLevelClasses: [AnyClass] {
        super.allowedTopLevelClasses + [
            UIColor.self,
            NSTimeZone.self,
            CustomTypeA.self,
            CustomTypeB.self,
        ]
    }

    // value transformerを登録する処理です。Core Data Stackの初期化前に呼びます。
    public static func register() {
        let transformer = SecureValueTransformer()
        ValueTransformer.setValueTransformer(transformer, forName: name)
    }

}

モデルエディタではTransformerの欄に name で指定したクラス名を入力します。

最後にCore Data Stackの初期化前に以下を実行して、value transformerを登録します。

SecureValueTransformer.register()

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUI用のジョイスティックUIライブラリ OMJoystickの使い方

OMJoystickはSwiftUI用のジョイスティックUIライブラリです。CocoaPodsとSwift Package Managerからダウンロードできます。

動作イメージ

image.png

インストール方法

CocoaPodsでPodfileに下記のように記述します。

pod 'OMJoystick'

動かし方

以下のコードで、デフォルト設定のまま動かすことができます。

import SwiftUI
import OMJoystick

struct ContentView: View {
    var body: some View {
        OMJoystick(colorSetting: ColorSetting()) { (joyStickState, stickPosition) in
        }
    }
}

アイコンやサイズ、色などの見た目を変えたい場合は、下記のように設定します。

import SwiftUI
import OMJoystick
import SFSafeSymbols

struct ContentView: View {        
    let iconSetting = IconSetting(
        leftIcon: Image(systemSymbol: .arrowLeft),
        rightIcon: Image(systemSymbol: .arrowRight),
        upIcon: Image(systemSymbol:.arrowUp),
        downIcon: Image(systemSymbol: .arrowDown)
    )

    let colorSetting = ColorSetting(subRingColor: .red, bigRingNormalBackgroundColor: .green, bigRingDarkBackgroundColor: .blue, bigRingStrokeColor: .yellow)

    var body: some View {
        GeometryReader { geometry in
            VStack(alignment: .center, spacing: 5) {
                OMJoystick(isDebug: true, iconSetting: self.iconSetting,  colorSetting: ColorSetting(), smallRingRadius: 70, bigRingRadius: 120
                ) { (joyStickState, stickPosition)  in

                }.frame(width: geometry.size.width-40, height: geometry.size.width-40)
            }
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CGRectをすっきり記述できるExtension

とても便利

Swift 5.2.4
Xcode 11.6

Extension.swift
import UIKit

extension UIView {
    public var width : CGFloat {
        return self.frame.size.width
    }

    public var height : CGFloat {
        return self.frame.size.height
    }

    public var top : CGFloat {
        return self.frame.origin.y
    }

    public var bottom : CGFloat {
        return self.frame.size.height + self.frame.origin.y
    }

    public var left : CGFloat {
        return self.frame.origin.x
    }

    public var right : CGFloat {
        return self.frame.size.width + self.frame.origin.x
    }
}

これだと

view.frame.size.width → view.widthになり

例えばview1の右となりに別のパーツを配置したいときのCGRectのxは

view1.right

view1の下のCGRectのyが欲しいときは

view1.bottom

でいけちゃう。

参考

Youtube

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

超絶簡単手作りSwiftチェックボックスの作り方

チェックボックスを作る

現在業務で会社のAndroidアプリをiOSのアプリに作り替えることをしています。色々やり方はあるかと思いますが、下準備なしで、Swiftのデフォルトの機能だけでチェックボックス的なやつを超絶簡単に作ったのでご紹介します。

スクリーンショット 2020-09-12 11.48.13.png

Swift 5.2.4
Xcode 11.6

使うのはこれだけ

・Booleanをいれる変数
・UIButton
・SF Symbolsから"checkmark"くん

まずは変数を宣言します

ViewController.swift
private var checked = false

デフォルトではチェックされてない状態なのでfalseを代入。

次にUIButton

先ほどの変数の下に以下を記述。

ViewController.swift
    private let radioButton : UIButton = {
        let button = UIButton(frame: CGRect(x: 0,
                                            y: 0,
                                            width: 50,
                                            height: 50))
        button.layer.borderWidth = 2
        button.layer.borderColor = UIColor.gray.cgColor
        return button
    }() //縦横50、灰色の太さ2の枠があるボタン

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(radioButton)
        //ボタンをviewに追加
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        radioButton.center = view.center
        //ボタンの中心をviewの中心に合わせる
    }

ボタンを押すたびにcheckedの値を変える

ということで以下をreturn buttonの上あたりに追記

ViewController.swift
button.addTarget(self,
                 action: #selector(didTapRadioButton),
                 for: .touchUpInside)

そしてviewDidLayoutSubviewsの下あたりにこれを追記

ViewController.swift
@objc private func didTapRadioButton(){
        print("tap")
}

これで灰色の四角をタップするたびにdidTapRadioButton関数の中身が実行されるようになります。

何をどうしたいのか

・ボタンをタップするたびに変数checkedの値をtrueだったらfalse, falseだったらtrueに変えたい
・trueの状態であれば四角にチェックマークが欲しい、falseだったら空っぽにしたい

これらをすっきり記述できるのがSwitch文です!それでは以下をdidTapRadioButton関数の中に記述します。

ViewController.swift
switch checked {
        case false:
            radioButton.setImage(UIImage(systemName: "checkmark"), for: .normal)
            checked = true
        case true:
            radioButton.setImage(nil, for: .normal)
            checked = false

        }

これで完成!全体像。

ViewController.swift
mport UIKit

class ViewController: UIViewController {
    private var checked = false

    private let radioButton : UIButton = {
        let button = UIButton(frame: CGRect(x: 0,
                                            y: 0,
                                            width: 50,
                                            height: 50))
        button.layer.borderWidth = 2
        button.layer.borderColor = UIColor.gray.cgColor
        button.addTarget(self, action: #selector(didTapRadioButton), for: .touchUpInside)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(radioButton)
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        radioButton.center = view.center
    }

    @objc private func didTapRadioButton(){
        switch checked {
        case false:
            radioButton.setImage(UIImage(systemName: "checkmark"), for: .normal)
            checked = true
        case true:
            radioButton.setImage(nil, for: .normal)
            checked = false

        }
    }


}

さいごに

他にも背景色を変えたりアニメーションで遅延を起こしたりして色々アレンジできるので、もしよかったら試してみてください?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

超絶簡単手作りSwiftラジオボタンの作り方

ラジオボタンを作る

現在業務で会社のAndroidアプリをiOSのアプリに作り替えることをしています。その中でラジオボタンを使うということで色々ググってたんですが、どっかからラジオボタン用の画像拾ってきたり、どれも一手間かかる印象であんまり頭に入ってきませんでした。
ということで、そんな下準備なしで、Swiftのデフォルトの機能だけでラジオボタン的なやつを超絶簡単に作ったのでご紹介します。

スクリーンショット 2020-09-12 11.48.13.png

Swift 5.2.4
Xcode 11.6

使うのはこれだけ

・Booleanをいれる変数
・UIButton
・SF Symbolsから"checkmark"くん

まずは変数を宣言します

ViewController.swift
private var checked = false

デフォルトではチェックされてない状態なのでfalseを代入。

次にUIButton

先ほどの変数の下に以下を記述。

ViewController.swift
    private let radioButton : UIButton = {
        let button = UIButton(frame: CGRect(x: 0,
                                            y: 0,
                                            width: 50,
                                            height: 50))
        button.layer.borderWidth = 2
        button.layer.borderColor = UIColor.gray.cgColor
        return button
    }() //縦横50、灰色の太さ2の枠があるボタン

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(radioButton)
        //ボタンをviewに追加
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        radioButton.center = view.center
        //ボタンの中心をviewの中心に合わせる
    }

ボタンを押すたびにcheckedの値を変える

ということで以下をreturn buttonの上あたりに追記

ViewController.swift
button.addTarget(self,
                 action: #selector(didTapRadioButton),
                 for: .touchUpInside)

そしてviewDidLayoutSubviewsの下あたりにこれを追記

ViewController.swift
@objc private func didTapRadioButton(){
        print("tap")
}

これで灰色の四角をタップするたびにdidTapRadioButton関数が実行されるようになります。

何をどうしたいのか

・ボタンをタップするたびに変数checkedの値をtrueだったらfalse, falseだったらtrueに変えたい
・trueの状態であれば四角にチェックマークが欲しい、falseだったら空っぽにしたい

これらをすっきり記述できるのがSwitch文です!それでは以下をdidTapRadioButton関数の中に記述します。

ViewController.swift
switch checked {
        case false:
            radioButton.setImage(UIImage(systemName: "checkmark"), for: .normal)
            checked = true
        case true:
            radioButton.setImage(nil, for: .normal)
            checked = false

        }

これで完成!全体像。

ViewController.swift
mport UIKit

class ViewController: UIViewController {
    private var checked = false

    private let radioButton : UIButton = {
        let button = UIButton(frame: CGRect(x: 0,
                                            y: 0,
                                            width: 50,
                                            height: 50))
        button.layer.borderWidth = 2
        button.layer.borderColor = UIColor.gray.cgColor
        button.addTarget(self, action: #selector(didTapRadioButton), for: .touchUpInside)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(radioButton)
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        radioButton.center = view.center
    }

    @objc private func didTapRadioButton(){
        switch checked {
        case false:
            radioButton.setImage(UIImage(systemName: "checkmark"), for: .normal)
            checked = true
        case true:
            radioButton.setImage(nil, for: .normal)
            checked = false

        }
    }


}

さいごに

他にも背景色を変えたりアニメーションで遅延を起こしたりして色々アレンジできるので、もしよかったら試してみてください?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む