- 投稿日:2020-09-12T23:42:10+09:00
本日学んだ、オプショナル型の基礎。【 ! の混同】
本日学んだ、オプショナル型の基礎をまとめます。
自分用の記録用の皮算用。! を混同していた。
"!"はオプショナルではない、nilを許さないというのは間違いです。
このことも勘違いしている人が多いかもしれません。勘違いしていました。
変数の宣言で型に
!
をつける場合、その変数は「オプショナル型」なので、強制的アンラップの
!
とは別物らしい。// 変数の宣言で型に ! をつける場合、その変数は「オプショナル型」 // (= 強制的アンラップの ! とは別物) var hoge: String! // nil暗黙的アンラップ型 (= IUO)
Implicitly Unwrapped Optional
オプショナル型
の1つ (= nliを許容)- 使用するとき、必ず強制的アンラップ
!
をする。- つまり、自動でアンラップをしてくれる
但し、nilでアンラップすると、アプリが落ちる?ため、
100%絶対に値がある(= nilでない) 場合のみ、使用可能。最初は nil で宣言したいが、使うときには絶対に値が入っている、という場合に使用。
他のアンラップ方法
debugPrint(error?.localizedDescription ?? "")
guard let
やif let
とかの「オプショナル バインディング」の他に、
- オプショナル チェイニング(
?
)- nil結合演算子 (
??
)があるみたいなので、以下にメモっておく。
オプショナル チェイニング? (= Optional Chaining)
- 値がある場合
アンラップする。
但し、それに続くプロパティやメソッドの戻り値は「
オプショナル型
」になる。
(-> nilの場合 nil を返すため)- nilの場合
nil を返す。それに続く処理をすべてキャンセル (-> なので安全)
nil結合演算子? (nil-coalescing)
A ?? B
... Aに値があるとAをアンラップ。 Aが nil だとBを返す。おしまい。
参考サイト
どこよりも分かりやすいSwiftの"?"と"!"
-> LGTMの数が異常。
- 投稿日:2020-09-12T22:32:05+09:00
SwiftUIとCombineの練習がてらmacOS向けのピアノアプリ作ってみた
はじめに
SwiftUIとCombine、Swiftコーダーのみなさまは勉強なさっているでしょうか?私は今まで近いうちに勉強しよう明日やろうと言いながら、アプリのメンテを優先してずっと後回しにしてきていました。しかし、ついに会社でも使う機会が訪れ、ようやく重い腰を上げ勉強し始めました。ただ、文法や仕組みを学ぶだけでは全く身につかないなぁと思ったので、作りたいものを決めて実装しながら学ぶことにしました。
今回は、ある程度複雑な形状のViewを複数組み合わせる必要があり、Viewに対するアクションに基づいてリアルタイムに動作が変わるものが良さそうだったので、ピアノのシミュレーターアプリを作る事にしました。
成果物 PianoSample
⬇️デモ動画リンク
ウィンドウにピアノの鍵盤が表示され、クリックやドラッグをする事で実際に音を鳴らして弾くことができるアプリを作りました。開発にかかった時間は10時間くらいだったと思います。コードはGitHubに置いてあります。
https://github.com/Kyome22/PianoSample学び(SwiftUI編)
「ピアノの鍵盤をドラッグしている時にカーソルが鍵盤の上にある時はViewが押されているよう描画し、カーソルが鍵盤の上から外れた場合にはViewが押されていないよう描画する」という機能を実装する際にかなりハマりました。
関門1
- Viewに対するアクションのイベントハンドラには、
onTapGesture
やonLongPressGesture
はあるが、タッチの開始と終了を検知して処理を実行する術がデフォルトで容易されていないこの問題に対処するため、
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) } } }
isHit
をhitTest
のローカル変数にすればいいのでは?と思うかもしれませんが、後でこの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に
onTapGesture
やonLongPressGesture
のようなイベントハンドラとしてタッチの開始や終了を起点に発火するやつがなかったことでした。自前でイベントハンドラを作らねばなりません。そのため、当たり判定が更新された際に、音の再生停止をするために命令を伝達するルートを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の機能なのかまだ曖昧ではっきりわかっていないので、勉強していきたいですね。おそらく代表的な情報の伝達パターンをいくつか覚えてしまえば、綺麗にイベントを捌けるようになりそうです。
- 投稿日:2020-09-12T15:34:54+09:00
コードでSwiftのautolayoutを書く
Storyboardで画面のレイアウトやUI部品をグラフィカルに配置できますが、チームで協同作業する時や細かいレイアウトの調整が発生する場合はStoryboardだとどうしても支障が出ます。
autolayoutの標準APIについて、Appleが
NSLayoutConstraint
とNSLayoutAnchor
(ios9~)、Visual Format Language(VFL)
を提供しています。NSLayoutAnchor
はNSLayoutConstraint
よりコード量が少なくなり、制約・声明もはっきりするようになっています。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 }
- 投稿日:2020-09-12T14:36:25+09:00
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()参考リンク
- 投稿日:2020-09-12T12:54:42+09:00
SwiftUI用のジョイスティックUIライブラリ OMJoystickの使い方
OMJoystickはSwiftUI用のジョイスティックUIライブラリです。CocoaPodsとSwift Package Managerからダウンロードできます。
動作イメージ
インストール方法
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) } } } }
- 投稿日:2020-09-12T12:20:51+09:00
CGRectをすっきり記述できるExtension
とても便利
Swift 5.2.4
Xcode 11.6Extension.swiftimport 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
でいけちゃう。
参考
- 投稿日:2020-09-12T11:52:21+09:00
超絶簡単手作りSwiftチェックボックスの作り方
チェックボックスを作る
現在業務で会社のAndroidアプリをiOSのアプリに作り替えることをしています。色々やり方はあるかと思いますが、下準備なしで、Swiftのデフォルトの機能だけでチェックボックス的なやつを超絶簡単に作ったのでご紹介します。
Swift 5.2.4
Xcode 11.6使うのはこれだけ
・Booleanをいれる変数
・UIButton
・SF Symbolsから"checkmark"くんまずは変数を宣言します
ViewController.swiftprivate var checked = falseデフォルトではチェックされてない状態なのでfalseを代入。
次にUIButton
先ほどの変数の下に以下を記述。
ViewController.swiftprivate 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.swiftbutton.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.swiftswitch checked { case false: radioButton.setImage(UIImage(systemName: "checkmark"), for: .normal) checked = true case true: radioButton.setImage(nil, for: .normal) checked = false }これで完成!全体像。
ViewController.swiftmport 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 } } }さいごに
他にも背景色を変えたりアニメーションで遅延を起こしたりして色々アレンジできるので、もしよかったら試してみてください?
- 投稿日:2020-09-12T11:52:21+09:00
超絶簡単手作りSwiftラジオボタンの作り方
ラジオボタンを作る
現在業務で会社のAndroidアプリをiOSのアプリに作り替えることをしています。その中でラジオボタンを使うということで色々ググってたんですが、どっかからラジオボタン用の画像拾ってきたり、どれも一手間かかる印象であんまり頭に入ってきませんでした。
ということで、そんな下準備なしで、Swiftのデフォルトの機能だけでラジオボタン的なやつを超絶簡単に作ったのでご紹介します。Swift 5.2.4
Xcode 11.6使うのはこれだけ
・Booleanをいれる変数
・UIButton
・SF Symbolsから"checkmark"くんまずは変数を宣言します
ViewController.swiftprivate var checked = falseデフォルトではチェックされてない状態なのでfalseを代入。
次にUIButton
先ほどの変数の下に以下を記述。
ViewController.swiftprivate 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.swiftbutton.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.swiftswitch checked { case false: radioButton.setImage(UIImage(systemName: "checkmark"), for: .normal) checked = true case true: radioButton.setImage(nil, for: .normal) checked = false }これで完成!全体像。
ViewController.swiftmport 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 } } }さいごに
他にも背景色を変えたりアニメーションで遅延を起こしたりして色々アレンジできるので、もしよかったら試してみてください?