- 投稿日:2021-03-01T23:48:24+09:00
UIが出すNotificationを複数ウインドウ間で処理する
概要
ドキュメントベースのアプリケーションを作っていて
NSScrollView
のdocumentVisibleRect
を継続的に取得しようとしたら突っ掛かった所があって他のUIに応用ができると思ったので書きます。解決方法
NSUserInterfaceItemIdentifier
にuuid
を含めた状態でNotification
を受け取る。やろうとした事
普通に
Notification
を使う。まず、
NSScrollView
がスクロールされている時に出されるNSScrollView
のNSNotification.Name
型のdidEndLiveScrollNotification
を使ってclass ViewController: NSViewController { //...中略 override func viewDidLoad() { super.viewDidLoad() //... let scrollView = NSScrollView(frame: NSMakeRect(0, 0, 100, 100)) scrollView.identifier = NSUserInterfaceItemIdentifier("ScrollView") //... NotificationCenter.default.addObserver(self, selector: #selector(ScrollViewLiveScroll(_:)), name: NSScrollView.didEndLiveScrollNotification, object: nil) } @objc private func ScrollViewLiveScroll(_ notification: Notification){ guard let scrollView = notification.object as? NSScrollView else { return } if scrollView.identifier?.rawValue == "ScrollView" { print(scrollView.documentVisibleRect) } } }しかし、ドキュメントベースのアプリで、
scrollView
のidentifier
が同じなので、ユーザーが作った他のウインドウでのイベントも受け取ってしまう。
KVO (Key-Value-Observing)
を使ってscrollView.documentVisibleRect
の変更を監視する。これもしかし、値が
=
で代入された時は通知されるが、UI内部で変化するdocumentVisibleRect
の変化は受け取ってくれないらしい?ので無理。
KVO
で出来る方法を知っている方がいたら教えてください。
NotificationCenter
を新たに宣言してNotification
クラス内で
NotificationCenter
を新たに宣言することによって重複した通知を受け取らないようにする。しかし、UIが
NotificationCenter.default
でNotification
を出すことは変えられないので、受け取る側のNotificationCenter
を変えてしまっては受け取れない。解決した方法
scrollView
のidentifier
が同じなら変えればいいということで、uuid
で一意なIDを生成して、そのクラスがインスタンス化された時毎に違う Identifier を生成すればできる!ということでclass ViewController: NSViewController { let uuid = UUID()//ここで //...中略 override func viewDidLoad() { super.viewDidLoad() //... let scrollView = NSScrollView(frame: NSMakeRect(0, 0, 100, 100)) scrollView.identifier = NSUserInterfaceItemIdentifier("ScrollView\(\(uuid.uuidString))") //... NotificationCenter.default.addObserver(self, selector: #selector(ScrollViewLiveScroll(_:)), name: NSScrollView.didEndLiveScrollNotification, object: nil) } @objc private func ScrollViewLiveScroll(_ notification: Notification){ guard let scrollView = notification.object as? NSScrollView else { return } if scrollView.identifier?.rawValue == "ScrollView\(\(uuid.uuidString))" { print(scrollView.documentVisibleRect) } } }
identifier
は"UserInterface"には使えるので、NSScrollView
以外にも応用できます余談ですが、この時の
identifier
は、The slash (/), backslash (\), or colon (:) characters are reserved and must not be used in your custom identifiers. Similarly, Apple reserves all identifiers beginning with an underscore (_) character.
(/),(),(:)は使えず、また、(_)はappleが先頭を予約しているので使えません。
参考
uuid
の参考
- 投稿日:2021-03-01T23:48:24+09:00
UIが出すNotificationを複数ウインドウ間で処理する : Swift
概要
ドキュメントベースのアプリケーションを作っていて
NSScrollView
のdocumentVisibleRect
を継続的に取得しようとしたら突っ掛かった所があって他のUIに応用ができると思ったので書きます。解決方法
NSUserInterfaceItemIdentifier
にuuid
を含めた状態でNotification
を受け取る。やろうとした事
普通に
Notification
を使う。まず、
NSScrollView
がスクロールされている時に出されるNSScrollView
のNSNotification.Name
型のdidLiveScrollNotification
を使ってclass ViewController: NSViewController { //...中略 override func viewDidLoad() { super.viewDidLoad() //... let scrollView = NSScrollView(frame: NSMakeRect(0, 0, 100, 100)) scrollView.identifier = NSUserInterfaceItemIdentifier("ScrollView") //... NotificationCenter.default.addObserver(self, selector: #selector(ScrollViewLiveScroll(_:)), name: NSScrollView.didLiveScrollNotification, object: nil) } @objc private func ScrollViewLiveScroll(_ notification: Notification){ guard let scrollView = notification.object as? NSScrollView else { return } if scrollView.identifier?.rawValue == "ScrollView" { print(scrollView.documentVisibleRect) } } }しかし、ドキュメントベースのアプリで、
scrollView
のidentifier
が同じなので、ユーザーが作った他のウインドウでのイベントも受け取ってしまう。
KVO (Key-Value-Observing)
を使ってscrollView.documentVisibleRect
の変更を監視する。これもしかし、値が
=
で代入された時は通知されるが、UI内部で変化するdocumentVisibleRect
の変化は受け取ってくれないらしい?ので無理。
KVO
で出来る方法を知っている方がいたら教えてください。
NotificationCenter
を新たに宣言してNotification
クラス内で
NotificationCenter
を新たに宣言することによって重複した通知を受け取らないようにする。しかし、UIが
NotificationCenter.default
でNotification
を出すことは変えられないので、受け取る側のNotificationCenter
を変えてしまっては受け取れない。解決した方法
scrollView
のidentifier
が同じなら変えればいいということで、uuid
で一意なIDを生成して、そのクラスがインスタンス化された時毎に違う Identifier を生成すればできる!ということでclass ViewController: NSViewController { let uuid = UUID()//ここで //...中略 override func viewDidLoad() { super.viewDidLoad() //... let scrollView = NSScrollView(frame: NSMakeRect(0, 0, 100, 100)) scrollView.identifier = NSUserInterfaceItemIdentifier("ScrollView\(\(uuid.uuidString))") //... NotificationCenter.default.addObserver(self, selector: #selector(ScrollViewLiveScroll(_:)), name: NSScrollView.didLiveScrollNotification, object: nil) } @objc private func ScrollViewLiveScroll(_ notification: Notification){ guard let scrollView = notification.object as? NSScrollView else { return } if scrollView.identifier?.rawValue == "ScrollView\(\(uuid.uuidString))" { print(scrollView.documentVisibleRect) } } }
identifier
は"UserInterface"には使えるので、NSScrollView
以外にも応用できます余談ですが、この時の
identifier
は、The slash (/), backslash (\), or colon (:) characters are reserved and must not be used in your custom identifiers. Similarly, Apple reserves all identifiers beginning with an underscore (_) character.
(/),(),(:)は使えず、また、(_)はappleが先頭を予約しているので使えません。
参考
uuid
の参考
- 投稿日:2021-03-01T23:39:40+09:00
【Swift5】UIScrollView上のUIButton上でスクロールできない
事象
個人開発で、スクロールビュー上にUIButtonを配置して
ドロワーを実装した時、UIButton上でスクロールできない問題に
ぶつかったのでその時の解決策をメモ原因
まず、UIControlに準拠するUIButton上では、
スクロールイベントが発火しない。[UIScrollView touchesShouldCancelInContentView:] The default returned value is YES if view is not a UIControl object; otherwise, it returns NO.
これはどういうことかというと、タッチ対象がUIControlオブジェクトの時、
UIButtonの時は、touchesShouldCancelがfalseで返ってくる。
つまり、タップイベントがキャンセルされない。解決
この問題を解決するには、UIScrollView上のUIButton上では
touchesShouldCancelInContentViewでtrueを返してあげれば良いカスタムクラスを作成
class CustomScrollView: UIScrollView { /// ScrollView上のボタン上でドラッグした時、ドラッグを優先する(tagを1にしておく override func touchesShouldCancel(in view: UIView) -> Bool { if view.tag == 1 { return true } return super.touchesShouldCancel(in: view) } }tagを設定しておく
カスタムクラスの
view.tag == 1を見ると、"tagが1の時"という条件がついている。
storyboard、もしくはコード上で対象のUIButtonオブジェクトのtagを1に設定しておくことで、
この条件式が使える。終わりに
壁にぶつかっている時はスムーズに開発できずに苦しいけど、
できることをやっている時は成長していない時だと思えば
壁に立ち向かいやすくなるかもしれない。
- 投稿日:2021-03-01T22:31:45+09:00
【iOS】コードから端末のSizeClassを判断する(Swift)
SizeClassとは
SizeClassとは、端末の縦横のサイズをそれぞれ
Compact
またはRegular
として指定し、その組み合わせによってレイアウトの変更ができるようにするための機能。
Compact
が小さいサイズのときに使われ、Regular
が大きいサイズとして使われます。
組み合わせは全部で4通りあります。
SizeClass(Full) SizeClass(略) 主な機種 Compact/Compact CC iPhone8の横画面、iPhone12の横画面 Compact/Regular CR すべてのiPhoneの縦画面 Regular/Compact RC iPhone8Plusの横画面、iPhone11の横画面など Regular/Regular RR iPad(フルスクリーン)の縦画面、横画面 より詳しいパターンについては、公式ドキュメントをご参照ください。
https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/どうやって使うの?
StoryBoardから使う
簡単に説明すると、
SizeClassによって、制約を有効にしたり、Viewの表示非表示を切り替えることができます。
例1)ImageViewの高さを、SizeClassが「RR」のときは100px、「CR」のときは、60pxにするといったようなことができる。
例2)LabelをSizeClassが「RR」のときは非表示、「CR」のときは表示するといったようなことができる。詳しい使い方については、下記の記事がわかりやすいと思いましたので、リンクを貼らせていただきます。
https://qiita.com/wnstjd0333/items/1c2ed3cec565f69cbb90コードで使う
「UITraitEnvironment」プロトコルを実装しているクラスにおいて、
traitCollection
プロパティが利用可能です。
代表例として、UIViewControllerは「UITraitEnvironment」プロトコルを採用しているため、traitCollection
が利用できます。また、端末の回転やマルチタスキングへの切替によって、端末のSizeClassに変化があった場合、
traitCollectionDidChange(_:)
というメソッドが呼び出されます。(viewWillTransition(to:with:)
も呼ばれます。)
実際に使用したい場合は、どちらかのメソッドをオーバーライドして個々のViewControllerで利用していく感じです。それらを活用して、現在の画面が、4パターンあるSizeClassのうちどれに当てはまるかをコード上から検知することができます。
この記事は、タイトルにもあるように、SizeClassの判断をコードで行うことが目的ですので、以下に書いていきます。
SizeClassパターンの検知
おそらく、マルチデバイス対応、マルチタスキング対応を実施しようとするとすべてのパターンが必要になると思いますので、4パターンすべてを判定するロジックを載せておきます。
ViewController.swiftenum SizeClass: String { // width/height case CC = "Compact/Compact" case CR = "Compact/Regular" case RC = "Regular/Compact" case RR = "Regular/Regular" } class ViewController: UIViewController { // 色々と省略 // viewWillTransitionでも代用可能です。previousTraitCollectionを使いたければ、traitCollectionDidChangeのが最適 override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { // SizeClassが変わった時に呼ばれるので毎回チェックしたい場合は、ここでチェックするメソッドを呼び出す。 let sizeClass: String = returnSizeClass().rawValue print(sizeClass) // SizeClassが変わった時に、処理を書きたい場合はここに書く } func returnSizeClass() -> SizeClass { if traitCollection.horizontalSizeClass == .regular && traitCollection.verticalSizeClass == .regular { return .RR } else if traitCollection.horizontalSizeClass == .regular { return .RC } else if traitCollection.verticalSizeClass == .regular { return .CR } else { return .CC } } }enumで4パターンを定義しておいて、画面の状態によって当てはまるSizeClassをリターンするという方法で実装しました。
おそらく、ご自身のViewControllerにこちらをコピペしていただくだけでも動くと思います。
読んでいただいた方のお役に立てれば幸いです。
- 投稿日:2021-03-01T22:03:13+09:00
[Swift5]スクロールでNavigationBarを隠す方法(シンプル)
やりたいこと
YouTubeなどでよく目にする、スクロールでタイムラインの下部に移動する際にNavigationbarも同時に画面上部に隠す動作を実装したいと思います。
今回紹介する方法は非常にシンプルです。
他にも最適解のような方法があるとは思いますが、とにかくNavigationBarを隠せれば問題ないという方は是非参考にしてみてください。実装方法
// スクロールでナビゲーションバーを隠す func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView.panGestureRecognizer.translation(in: scrollView).y < 0 { navigationController?.setNavigationBarHidden(true, animated: true) } else { navigationController?.setNavigationBarHidden(false, animated: true) } }以上です。
- 投稿日:2021-03-01T20:15:50+09:00
【Swift】ライブラリ "Material Components" の MDCOutlinedTextField を使ったTextField実装
これはなに
マテリアルデザインのUIが簡単に実装できる Material Components を使ってこんな感じのTextFieldを作りたい。
Material Componentsのインストール方法は省く。
実装してみる
StryboardのTextFieldを選択して
MDCOutlinedTextField
を設定する。
そしたらControllerにつなぐ。LoginViewController.swiftclass LoginViewController: UIViewController { // ユーザ名入力欄 @IBOutlet weak var userNameTextField: MDCOutlinedTextField! // 中略TextFieldのプロパティを設定していく。
LoginViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() userIdTextField.label.text = "ユーザーなまえ" userIdTextField.placeholder = "なまえをいれなさい" // TextFieldが選択されていない状態の枠と文字の色 userIdTextField.setOutlineColor(.gray, for: .normal) userIdTextField.setFloatingLabelColor(.gray, for: .normal) // 編集中の枠と文字の色 userIdTextField.setOutlineColor(.blue, for: .editing) userIdTextField.setFloatingLabelColor(.blue, for: .editing) }これで上記の動画のようなTextField実装できる。
問題点
――が、ここまではドキュメントを読めば書いてあったんだけど、
userIdTextField.label.text
の部分、つまりここでいう『ユーザーなまえ』の部分の色を変える方法がなかなか見つからなかった(userIdTextField.label.textColor
ではダメだった)。解決策
userIdTextField.setNormalLabelColor(.gray, for: .normal)ハー、できてよかった( ;∀;)
- 投稿日:2021-03-01T19:48:52+09:00
【Swift5】Timerを変数に保存しないでinvalidateする
結論から言えばそんなに大したことない話なんですけど、意外と使うので備忘録としてアウトプット。
とっても便利なTimer
アプリを作るときには、何かしらの処理でほぼ必ず使うであろうTimer。
Swiftにも使いやすいTimer関数が用意されています。ViewController.swiftclass ViewController: UIViewController { var timer: Timer = Timer() var count: Int = 0 override func viewDidLoad() { super.viewDidLoad() // 1秒毎にup()を実行 timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.up), userInfo: nil, repeats: false) } @objc func up() { // 60秒経ったらタイマーを止める if count > 60 { timer.invalidate() } else { count += 1 } } }こんな感じで変数にタイマーを保存して、後から
.invalidate()
してタイマーをストップ&破棄するのが基本的な使い方です。毎回変数に入れるのだるくない?
ストップウォッチ的な奴は別ですが、上のサンプルみたいに「x秒後に一回処理を実行する」みたいなパターンではすぐにタイマーを破棄することになるのでいちいち変数に入れるのはめんどくさいですよね。
しかもタイマーの数が増えていく場合には変数を追加していかなければならない。そこでタイマーを変数に入れなくても止める(invalidate()する)ことにしました。
引数に自分自身を入れちゃおう
ViewController.swiftclass ViewController: UIViewController { // 30秒後に止まるタイマー func setTimer() { Timer.scheduledTimer(timeInterval: 30, // x秒後 target: self, selector: #selector(self.stopTimer(_:)), // 引数にTimer自身を指定する userInfo: nil, repeats: false) print("Timer started") } @objc func stopTimer(_ timer: Timer) { timer.invalidate() print("Timer stopped") } }このように
selector
で関数を指定するときに引数を自分自身にすることで、変数にTimerのインスタンスを保持しなくてもあとで.invalidate()
することができます。おしまい
Timerはメモリを圧迫するので、不要になったら必ず
.invalidate()
しよう。
- 投稿日:2021-03-01T18:20:11+09:00
【SwiftUI】alertメソッドでアラートを出す
先日リリースした私のアプリに使用した技術をひとつずつ解説しています。
私のアプリはこちら。
alertとは
下のようなアラートを出すメソッド。iPhoneを使っているといろんな場面で見かけるかと思います。
実際の動作
このように、ボタンを押すとアラートが出るサンプルアプリを作りました。こちらについて解説します。
基本的な書き方
まず、alertメソッドの基本的な書き方を説明します。
alertメソッド.alert(isPresented: ブール値, content: { Alert(アラートの内容) })引数の
isPresented
がtrueになったときにアラートが出現します。
アラートの内容はcontent
に書き、Alert構造体
を使います。サンプルアプリのコードについて解説します。
ソースコード
ContentView.swiftimport SwiftUI struct ContentView: View { @State var goodAlert = false //いいね!ボタンの方のアラートを出すためのブール値 @State var isGood = false //サムズアップに色をつけるかどうかのブール値 @State var noGoodAlert = false //よくないね!ボタンの方のアラートを出すためのブール値 var body: some View { VStack{ // サムズアップのマーク。isGoodがtrueになると色がつく。 Image(systemName: isGood ? "hand.thumbsup.fill":"hand.thumbsup") .font(.title) .foregroundColor(isGood ? .pink:.none) .padding() // いいね!ボタン Button(action: { goodAlert = true // このアクションでアラートを呼び出す }, label: { buttonLabel(text: "いいね!", fieldColor: Color.pink) }) .padding() // goodAlertがtrueになると呼び出されるアラート .alert(isPresented: $goodAlert, content: { Alert( title: Text("いいね!しますか?"), message: Text("いいね!すると色がつきます。"), primaryButton: .default(Text("はい"), action: {isGood = true}), secondaryButton: .destructive(Text("いいえ"), action: {isGood = false}) ) }) // よくないね!ボタン Button(action: { noGoodAlert = true // このアクションでアラートを呼び出す }, label: { buttonLabel(text: "よくないね!", fieldColor: Color.blue) }) .padding() // noGoodAlertがtrueになると呼び出されるアラート .alert(isPresented: $noGoodAlert, content: { Alert(title: Text("そんなこと言わないで")) }) } } } struct buttonLabel: View { let text: String let fieldColor: Color var body: some View { Text(text) .font(.title) .foregroundColor(.white) .frame(width: 250, height: 100) .background(fieldColor) .cornerRadius(30) } }全体のソースコードはこのようになっています。
少し複雑なので、いいね!ボタンの方に焦点を当てて説明したいと思います。いいね!ボタンとアラートの仕組み
まず、ContentViewのプロパティとしてブール型の変数を用意しています。
@State var goodAlert = false //いいね!ボタンの方のアラートを出すためのブール値 @State var isGood = false //サムズアップに色をつけるかどうかのブール値いいね!ボタンのアラートを出すための変数
goodAlert
は初期値としてfalseを入れています。初期値がtrueの場合、アプリ起動時にアラートが表示されることになります。次に、
goodAlert
がtrueに反転するためのトリガーを書きます。Button(action: { goodAlert = true // このアクションでアラートを呼び出す }, label: { buttonLabel(text: "いいね!", fieldColor: Color.pink) })今回はボタンのアクションによってアラートを呼び出したいので、このようになります。
そして、呼び出すアラートがこちら。
.alert(isPresented: $goodAlert, content: { Alert( title: Text("いいね!しますか?"), message: Text("いいね!すると色がつきます。"), primaryButton: .default(Text("はい"), action: {isGood = true}), secondaryButton: .destructive(Text("いいえ"), action: {isGood = false}) ) })
isPresented
にgoodAlertを入れているので、goodAlertがtrueに反転したときにcontent
に書かれているアラートが出現します。このように書くことで出現するアラートがこちらです。
こうして見ると、Alert構造体のどの引数がどの部分に表れているかわかりやすいかと思います。このアラートの「はい」ボタンを押すことで
isGood
にtrueが、「いいえ」ボタンを押すことでfalseが入ります。そうすることで結果的にサムズアップのマークに色がついたり消えたりしているのです。よくないね!ボタンとアラートの仕組み
次に、よくないね!ボタンの方の説明をしていきます。
こちらもまずはブール型の変数を用意しています。@State var noGoodAlert = false //よくないね!ボタンの方のアラートを出すためのブール値こちらの変数を反転させるトリガーがこちら。
Button(action: { noGoodAlert = true // このアクションでアラートを呼び出す }, label: { buttonLabel(text: "よくないね!", fieldColor: Color.blue) })呼び出すアラートはこちら。
.alert(isPresented: $noGoodAlert, content: { Alert(title: Text("そんなこと言わないで")) })いいね!ボタンの方のアラートに比べるとすっきりしています。実は、Alert構造体はtitleさえあればアラートとして機能します。
実際に出現するアラートがこちら。
Alert構造体にはタイトルしか書いていませんが、自動的にOKボタンをつけてくれます。このボタンを押すと
isPresented
がfalseになり、アラートが消えます。(いいね!ボタンの方も同様の仕組みでアラートが消えています)アラートのボタンについて
先ほど述べたように、Alert構造体はtitleがあれば機能し、アラートを消すボタンを自動的につけてくれますが、どのようなボタンにするかを指定することができます。
アラートのボタンはひとつ、もしくはふたつ付けることができます。それぞれ使用する引数が違うので、別々に解説したいと思います。
ボタンがひとつの場合
先ほどのよくないね!アラートを次のように書きかえます。
.alert(isPresented: $noGoodAlert, content: { Alert(title: Text("そんなこと言わないで"), dismissButton: .default(Text("了解です"))) })
dismissButton
という引数を追加しました。これによりボタンのテキストが変わります。「了解です」ボタンを押すとアラートが消えてくれます。このボタンに機能を追加したい場合、
.default
の引数のaction
に書けば追加できます。ボタンがふたつの場合
いいね!のアラートをもう一度見てみましょう。
.alert(isPresented: $goodAlert, content: { Alert( title: Text("いいね!しますか?"), message: Text("いいね!すると色がつきます。"), primaryButton: .default(Text("はい"), action: {isGood = true}), secondaryButton: .destructive(Text("いいえ"), action: {isGood = false}) ) })
primaryButton
がボタンひとつめ、secondaryButton
がふたつめです(そのままですね)。
primaryButtonでは.default
、secondaryButtonでは.destructive
というメソッドが使われています。実はこれらがボタンの文字の色を決めています。「はい」ボタンの方はdefault、つまりデフォルトの色の青になります。destructiveとは「破壊的な」という意味があり、これを使った「いいえ」ボタンは文字が赤色になっています。これらふたつを使い分けることで、ユーザーの押し間違いを減らす効果が期待できます。ぜひ使い分けましょう。
まとめ
alertメソッドはおそらく、iPhoneを触っていれば見ない日はないほど頻繁に見る機能かと思います。使うシーンもさまざまなので、ぜひこの機能を応用してアプリに取り入れてみてください。
- 投稿日:2021-03-01T15:58:04+09:00
【SwiftUI】ListでRowの開閉アニメーション
SwiftUIのListで、Rowタップ時にRowの高さをアニメーションさせつつ変化させます。
環境
- Swift:5.3.2
- Xcode:12.4 (12D4e)
コード
ContentView.swiftimport SwiftUI struct ContentView: View { @State var selected: UUID? @State var scrollTop = false var items = (0..<100).map { ItemData(value: $0 * 2 + 10001) } var body: some View { ScrollViewReader { proxy in NavigationView { List(items) { item in ItemView(item: Binding.constant(item), parent: Binding.constant(self)) } .listStyle(PlainListStyle()) .navigationBarTitle(Text("List01"), displayMode: .automatic) .navigationBarItems(trailing: Button(scrollTop ? "Top" : "Bottom") { withAnimation { proxy.scrollTo((scrollTop ? items.first : items.last)!.id, anchor: .top) scrollTop = !scrollTop } } ) } } } } struct ItemData: Identifiable { var value = 0 let id = UUID() var idString: String { get { return String(id.uuidString.prefix(5)) } } } struct ItemView: View { @Binding var item: ItemData @Binding var parent: ContentView @State var openCell = false var body: some View { ZStack { Color.blue.opacity(0.3) VStack { Text("Item \(item.idString) => \(item.value)") .frame(maxWidth: .infinity, alignment: .leading) Text("--") .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity, alignment: .trailing) Spacer() } .opacity(openCell ? 0 : 1) VStack { Text((0..<(openCell ? 5 : 1)).map { "Detail \($0)" }.joined(separator: "\n")) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity, alignment: .trailing) } .opacity(openCell ? 1 : 0) } .fixedSize(horizontal: false, vertical: true) .modifier(AnimatingCellHeight(height: openCell ? 120 : 60)) .animation(.linear) .contentShape(Rectangle()) .onTapGesture { openCell.toggle() } } init(item: Binding<ItemData>, parent: Binding<ContentView>) { _item = item _parent = parent print("init: \(item.wrappedValue.idString)") } } struct AnimatingCellHeight: AnimatableModifier { var height: CGFloat = 0 var animatableData: CGFloat { get { height } set { height = newValue } } func body(content: Content) -> some View { content.frame(height: height) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }リポジトリ
問題点
- Rowの高さが固定値で入っている(GeometryReaderを使うとうまくいかなかった)
- アニメーションがぎこちない(modifierか何かがおかしい?)
- 投稿日:2021-03-01T15:38:30+09:00
UITableView とTextField備忘録 Outlet cannot be connected to repeating content
StoryboardからUITableVIewのCellの中でTextFiledなどを配置して
ViewControllerへ連携したら、ビルドする時にエラーになりました:
Outlet cannot be connected to repeating contentCellは繰り返し使用されるオブジェクトであるため、直接Outlet接続できないらしい、
解決方法としてサブクラスで実装、つまりXibファイルで実装になります。
こちらの記事を参考しました。TableViewとTexFieldの組み合わせ実装の備忘録としてメモしておきます。
1.セールの中身を一括で宣言
enum Section: CaseIterable { case a case t var text:[String]? { switch self { case .a: return ["A","a","あ"] default: return nil } } var identifiers:[String]{ switch self { case .a: return ["acell","acell","acell"] case .t: return ["tcell","tcell","tcell"] } } var header:String{ switch self { case .a: return "aHeader" case .t: return "tHeader" } } var indicator:Bool{ switch self { case .a: return true case .t: return false } } }2.ViewControllerでデータを反映
extension ViewController: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { return Section.allCases.count } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return Section.allCases[section].identifiers.count } func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return Section.allCases[section].header } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let section = Section.allCases[indexPath.section] let cell = tableView.dequeueReusableCell(withIdentifier: section.identifiers[indexPath.row], for: indexPath) return cell } } extension ViewController: UITableViewDelegate{ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 45.0 } func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { let section = Section.allCases[indexPath.section] if section.indicator { cell.accessoryType = .disclosureIndicator } if section.identifiers[indexPath.row] == "acell"{ cell.textLabel?.text = section.text![indexPath.row] } } }3.二種類のCellを作る
1)Cellの中にLableを配置してTextを表示する”acell”
2)Cellの中にTextFieldを配置して入力できるようにする”tcell”4.Xibファイルで作ったCellを登録
class ViewController: UIViewController { @IBOutlet weak var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() let nib = UINib.init(nibName: "TextFieldCell", bundle: nil) tableView.register(nib, forCellReuseIdentifier: "tcell") } }5.StoryBoardでDataSourceとDelegateの連携も忘れずに
この段階ではこのようなTableViewになっています。
6.Keyboardを閉じる処理
TextFieldで入力中、TableViewをスクロールすると、Keyboardを閉じる処理func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.view.endEditing(true) }7.入力箇所が常に画面の真ん中に表示するように処理
例えば、画面の下側のCellのTextFieldをタップしたら、TableView全体を上へスクロール、
入力中のCellが常に真ん中に表示するようには、2ステップです。
1)UITextFieldDelegateのBeginEditingが入力始めると(Keyboardが表示すると)呼ばれるDelegateで
TableView.ScrollToRowを実装
2)Keyboardの高さ分を画面全体を上へ持ち上げる実装1)はUITextFieldのXibクラスでDelegateを作り、使用します。delegate = selfを忘れずに
protocol TestTextFieldDelegate: class { func beginEditing(_ cell:TextFieldCell) } extension TextFieldCell: UITextFieldDelegate { func textFieldDidBeginEditing(_ textField: UITextField) { self.textField!.becomeFirstResponder() self.delegate?.beginEditing(self) } }extension ViewController: TestTextFieldDelegate { func beginEditing(_ cell: TextFieldCell) { let index = tableView.indexPath(for: cell) tableView.scrollToRow(at: index!, at: .middle, animated: true) } }2)はVieControllerの方で実装
class ViewController: UIViewController { @IBOutlet weak var tableView: UITableView! @IBOutlet weak var tableBottomLayout: NSLayoutConstraint! override func viewDidLoad() { super.viewDidLoad() let nib = UINib.init(nibName: "TextFieldCell", bundle: nil) tableView.register(nib, forCellReuseIdentifier: "tcell") } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) regsiterKeyboardNotification() } private func regsiterKeyboardNotification(){ let notification = NotificationCenter.default notification.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) notification.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil) } @objc func keyboardWillShow(_ notification:Notification?){ guard let userInfo = notification?.userInfo, let keyboradInfo = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else{ return } let keyboardHieght = keyboradInfo.cgRectValue.size.height self.tableBottomLayout.constant = keyboardHieght UIView.animate(withDuration: duration){ self.view.layoutIfNeeded() } } @objc func keyboardWillHide(_ notification:Notification?){ guard let userInfo = notification?.userInfo, let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else{ return } self.tableBottomLayout.constant = 0 UIView.animate(withDuration: duration){ self.view.layoutIfNeeded() } } }tableBottomLayoutですが、SotryBoardはこのようになっています。
FirstItemとSecondItemが逆だとうまくいかないみたい。
8.入力内容制限
1)xx文字以上入力できない
StoryBoardからTextFieldのAction:EditingChangedをXibファイルへ接続して、実装します。@IBAction func textChanged(_ sender: UITextField) { checkMaxLength() } private func checkMaxLength(){ var text = textField.text guard text != nil else{ return } if text!.count > 5 { text = String(text!.prefix(5)) } if self.textField.text != text{ self.textField.text = text } }2)特殊の文字を入れないようにする
例えば、英数字記号以外を入力できないようにするには以下で実装しますextension String { var ns: NSString{ return self as NSString } func deleteMatched(parttern:String?)-> String{ guard let parttern = parttern else{ return self } guard let regex = try? NSRegularExpression(pattern: parttern, options: NSRegularExpression.Options.caseInsensitive) else { return self } let range = NSRange(location: 0, length: self.ns.length) let modStirng = regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "") return modStirng } }入力できない文字列を宣言
let NONAlphanumericAndSymbols = "[^a-zA-Z0-9!#&'()*+,/:;=?@\\[\\]\\-\\._~]+"private func checkMaxLength(){ var text = textField.text guard text != nil else{ return } ///こちらを追加 text = text?.deleteMatched(parttern: self.NONAlphanumericAndSymbols) if text!.count > 5 { text = String(text!.prefix(5)) } if self.textField.text != text{ self.textField.text = text } }
- 投稿日:2021-03-01T15:01:26+09:00
Could not instantiate class named WKWebView because no class named WKWebView was found; the class needs to be defined in source code or linked in from a library (ensure the class is part of the correct target)"
フレームワークにWebKit.frameworkを追加すればOKです。
参考
- 投稿日:2021-03-01T14:26:35+09:00
Swift:メモ RSSを使ったテーブルアプリ
import UIKit class ListViewController: UITableViewController,XMLParserDelegate{//uitableviewcontrollerはdatasourceやdelegateも批准している var parser:XMLParser!//rssデータを解析するためのXMLParserクラスのインスタンスを格納するためのプロパティ var items = [Item]()//複数の記事を格納するための配列 var item:Item?//Itemクラス型のプロパティ Itemクラスはタイトルと本文のURLの2つのプロパティを持つ var currentString = "" //セルの個数 override func tableView(_ tableView:UITableView,numberOfRowsInSection section:Int) -> Int { return items.count } //tableView:cellForRowAtはセルの内容をiPhoneに知らせるメソッド セルの内容を戻り値に設定 //indexPathにはtableView:cellForRowAtメソッドが設定を行っているセルの行番号が保持されている //IndexPathにはsectionとrowのプロパティが含まれている override func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) -> UITableViewCell{ let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) cell.textLabel?.text = items[indexPath.row].title //瞬時に表示するために大量のセルを作成しなくて済むようにセルの再利用を設定322 //dequeueReusableCell(withIdentifier:for)メソッドを使う場合、引数にどのセルを再利用するか指定する→セルを選択した状態でのIdentifier項目の名前Cellが第一引数 return cell } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) startDownload() } //データのダウンロード startDownload()はオリジナルのメソッド func startDownload(){ self.items = []//itemsを空にする、古いデータが残ったままダウンロードすると重複する記事が生まれてしまう if let url = URL( string: "https://aaaaaa.com/rssfeeder/"){//ニュース記事のあるwebサイトのURLを指定 if let parser = XMLParser(contentsOf: url)//parserのインスタンスを作成 前の行で指定したURLを引数に 不正なURLが検出された時nilを返すことがあるのでメソッドの戻り値はオプショナル型 { self.parser = parser self.parser.delegate = self self.parser.parse() //parserのデリゲートにselfを指定して最期にparserメソッドを呼び出すことでデータの解析を開始 } } } func parser(_ parser: XMLParser,didStartElement elementName: String,namespaceURI:String?,qualifiedName qName: String?,attributes attributeDict:[String : String]) //要素感の開始タグが見つかるごとに呼び出される ダウンロードされた記事から必要なデータだけ取り出す処理を行うメソッド XMLParserDelegateで宣言されているメソッド { self.currentString=""//要素を一時的に保存する変数currentStringを空に 古い文字列が入らないように if elementName == "item"{//要素名がitemの場合のみニュース記事を入れる箱itemを作る self.item = Item() } } //タグで囲まれた要素を取り出す 内容が見つかると自動的にこのメソッドが呼び出される メソッドの中に文字列を取り出す処理を書く func parser(_ parser: XMLParser, foundCharacters string: String) { self.currentString += string//引数stringには見つかった記事の内容が格納されている 引数の中身を変数currentStringに+=演算子を使って追加 この段階ではcurrentStringにどの要素名の内容が入っているかは不明 } //要素名が終わる</items>が見つかると以下のメソッドが自動的に呼び出される func parser(_ parser: XMLParser, didEndElement elementName:String,//elementNameに要素名を格納 namespaceURI:String?, qualifiedName qName:String?){ switch elementName{//switch文の条件にelementNameを指定することで要素名ごとに処理が行われる //要素名がtitleの時はitem.titleに内容を格納 linkも同じ case "title":self.item?.title = currentString case "link":self.item?.link = currentString case "item":self.items.append(self.item!)//要素がitemの場合はニュース記事の終わりを意味するのでこれまで取得したデータをitemsの中に格納 append=追加する default : break } } //テーブルビューの内容を更新することで取得した記事を画面に表示 func parserDidEndDocument(_ parser:XMLParser){ self.tableView.reloadData() } override func prepare(for segue:UIStoryboardSegue, sender:Any?){ if let indexPath = self.tableView.indexPathForSelectedRow{//ユーザーがタップしたセルのindexPathを取得 let item = items[indexPath.row]//取得した値を用いてitem配列から該当する記事を取得 let controller = segue.destination as! DetailViewController//controller定数に遷移先のビューコントローラーを格納 controller.title = item.title//titleプロパティに記事タイトルを格納 controller.link = item.link } } }
- 投稿日:2021-03-01T14:15:03+09:00
【Swift】条件に当てはまるFirebaseのドキュメントを一括削除
条件に該当するドキュメントを削除するコード
Firestore.firestore().collection("users").whereField("user_name", isEqualTo : "田中").getDocuments() { (querySnapshot, err) in if let err = err { print("Error getting documents: \(err)") } else { for document in querySnapshot!.documents { document.reference.delete() } } }このコードの場合user_nameが田中と書かれたすべてのドキュメントが削除されます
- 投稿日:2021-03-01T14:09:03+09:00
【Swift】Firebaseで認証済みユーザーかどうか振り分ける処理
最初の画面を認証済みユーザーと新規ユーザーとで切り替えたい時の処理
import UIKit import Firebase import FirebaseUI class LaunchViewController : UIViewController{ override func viewDidLoad() { super.viewDidLoad() //ロードされるまでの初期画面(ロゴ画像、ナビゲーションバーなど) } } override func viewDidAppear(_ animated: Bool) { if Auth.auth().currentUser != nil { self.performSegue(withIdentifier: "sign", sender: nil) print(認証済みユーザー) } else { self.performSegue(withIdentifier: "nosign", sender: nil) print(新規ユーザー) } } }ポイントはAuth.auth().currentUser != nilのif文
- 投稿日:2021-03-01T14:00:45+09:00
【Swift】Eurekaでボタンの表示・非表示の切り替え
まずはフォームを作成
ここでは簡略化のため表示・非表示を切り替えたいボタンのみ実装していますoverride func viewDidLoad() { super.viewDidLoad() form <<< ButtonRow("Button") {row in row.tag = "delete_row" row.title = "商品を削除する" row.onCellSelection{[unowned self] ButtonCellOf, row in self.delete() } } }次に表示・非表示を切り替えるコード
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if let buttonRow = self.form.rowBy(tag: "delete_row") as? ButtonRow{ if delete_state == true { buttonRow.hidden = true buttonRow.evaluateHidden() } } }変数delete_stateは自分で条件に合わせて設定しましょう
- 投稿日:2021-03-01T13:54:14+09:00
【Swift】Eurekaのボタンの色を変更する方法
SwiftのライブラリEurekaでボタンの色を変更する方法に苦戦したので残しておきます。
<<< ButtonRow("Button3") {row in row.tag = "delete_row" row.title = "商品を削除する" row.onCellSelection{[unowned self] ButtonCellOf, row in self.delete() } }.cellSetup() {cell, row in cell.backgroundColor = UIColor.white cell.tintColor = UIColor.red }他にもalpha、window、backgroundviewなどがあります。
- 投稿日:2021-03-01T13:46:02+09:00
【Swift】WKWebViewをコードだけで使い回す
SwiftでUITableViewのセルごとに異なるウェブサイトに遷移させたい時に便利な使い回しコード。
まずは使い回すclassを指定
import UIKit import WebKit class WebView : UIViewController, WKUIDelegate{ var webView : WKWebView! var request_url : URL? override func loadView(){ let webConfiguration = WKWebViewConfiguration() webView = WKWebView(frame: .zero, configuration: webConfiguration) webView.uiDelegate = self view = webView } override func viewDidLoad() { super.viewDidLoad() let url = self.request_url let request = URLRequest(url: url!) webView.load(request) } }後はtableviewのcellごとにURLを指定するだけ。
if indexPath.section == 1 { if indexPath.row == 0 { //チュートリアル let vc = WebView() vc.title = "チュートリアル" vc.request_url = URL(string: "https://sampe.com/tutorial/") self.navigationController?.pushViewController(vc, animated: true) } if indexPath.row == 1 { //アプリの使い方 let vc = WebView() vc.title = "アプリの使い方" vc.request_url = URL(string: "https://sampe.com//use/") self.navigationController?.pushViewController(vc, animated: true) } if indexPath.row == 2 { //よくある質問 let vc = WebView() vc.title = "よくある質問" vc.request_url = URL(string: "https://sampe.com/question/") self.navigationController?.pushViewController(vc, animated: true) } 以下略
- 投稿日:2021-03-01T13:35:14+09:00
Firebaseのタイムスタンプ型をSwiftでDate型にキャスト
まずはFirebaseのデータを配列?リスト?として取得するクラスを作成
もともとFirebaseにはdateという名前のタイムスタンプ型のフィールドが登録されている前提import Foundation import CodableFirebase import Firebase struct Info : Decodable { //Firebaseから取得するフィールドを格納する変数を定義 var id : String? var messageId : String? var roomid : String? var senderid : String? var created_date : Date? static func decode(_ data : [String:Any]) -> Info? { do { var item = try FirestoreDecoder().decode(Info.self, from: data) if let timestamp : Timestamp = data["date"] as? Timestamp { item.created_date = timestamp.dateValue() } return item }catch { return nil } } }呼び出す時はこんな感じ。
import Foundation import UIKit import Firebase var Sample_Array: [Info] = [] var firestore = Firestore.firestore() override func viewDidLoad() { super.viewDidLoad() self.firestore.collection("info_data").getDocuments { (snaps, error) in guard let documents = snaps?.documents else { return } self.Sample_Array = [] for document in documents { guard let item = Info.decode(document.data()) else{ continue } self.Sample_Array.append(item) } print(self.Sample_Array) } }
- 投稿日:2021-03-01T13:17:08+09:00
【Swift】1画面に2種類のCollectionViewを実装する方法
まずlazy varで2種類のCollectionViewを定義
lazy var collectionView1 : UICollectionView = { let layout = UICollectionViewFlowLayout() let collectionView = UICollectionView( frame: self.view.bounds, collectionViewLayout: layout) collectionView.backgroundColor = UIColor.hex(string: "#eeeeee", alpha: 1) collectionView.delegate = self collectionView.dataSource = self collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "cellId") self.cellidentifier = "cellId1" return collectionView }() lazy var collectionView2 : UICollectionView = { let layout = UICollectionViewFlowLayout() let collectionView = UICollectionView( frame: self.view.bounds, collectionViewLayout: layout) collectionView.backgroundColor = UIColor.hex(string: "#eeeeee", alpha: 1) collectionView.delegate = self collectionView.dataSource = self collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "cellId2") self.cellidentifier = "cellId2" return collectionView2 }()次にアイテムの個数や表示内容、タッチ時の処理collectionViewの設定を設定します。
// 表示するアイテムの数 func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { if collectionView == self.collectionView2 { //collectionView2で設定したいアイテム数 self.array_count = array2.count } else{ //collectionView1で設定したいアイテム数 self.array_count = array1.count } return self.array_count } // アイテムの大きさ func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let size = self.view.frame.width / 3.5 return CGSize(width: size, height: size) } // 上下左右の余白設定 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { let inset = (self.view.frame.width / 4) / 8 return UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset) } // 余白の最小値を設定 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return (self.view.frame.width / 4) / 6 } // アイテムの表示内容(UICollectionViewDataSource が必要) func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { if collectionView == self.collectionView2 { let cellA = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId2", for: indexPath) as! CollectionViewCell cellA.backgroundColor = UIColor.lightGray let item = array1[indexPath.row] cellA.setUpContents(item) //他のファイルで設定したカスタムセルを呼び出す return cellA } else { let cellB = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId1", for: indexPath) as! CollectionViewCell cellB.backgroundColor = UIColor.lightGray let item = array2[indexPath.row] cellB.setUpContents(item) //他のファイルで設定したカスタムセルを呼び出す return cellB } } // タッチ時の処理 func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if collectionView == self.collectionView2 { //collectionView2で設定したいタッチ処理 print("collectionView2 is tapped!") } else{ //collectionView1で設定したいタッチ処理 print("collectionView1 is tapped!") } }関数内の処理をif文で分けるのがポイント
参考記事:How can I add multiple collection views in a UIViewController in Swift?
- 投稿日:2021-03-01T10:36:18+09:00
enumの使い方について簡単にまとめる
スキル不足だった僕はそれぞれの場所で宣言すればいいじゃんなんて思っていたわけなのだが、企業とかで使われるアプリを案件を通して行ったときにいろんな場所で宣言しまくるのはあまりよくないななんて思った。
確かに楽なんだけど、あれ、、、、○○○○って何番だっけみたいな感じであまり使わないやつとかだとマジでわからなくなる
そんな時にenumの使い方と実際の使われ方を見てこっちの方が間違いなく便利なんて思ったので自分が見返せるようにするためにも残しておく
例えばenumの使い方はこんな感じ
商品の状態を数字で管理している場合
0が販売
1が在庫
2がなんちゃらみたいな
そんな感じの場合
enum 商品: Int {
case 販売 = 0
case 在庫 = 1
case なんちゃら = 2
}こんな感じで配置しておけば関数とか作成した場合でも販売を呼び出したり在庫を呼び出したりすればいいだけなので、ある程度スキルが身に付けばこっちの方が楽だななんて思えるようになる
これからプログラミングを始めるとかそういう方は「なんだこれ、、、」みたいな感じになるだろうけど、理解して実際に現場で経験すればこっちの方が楽かもみたいになると思います。
enumの使い方は未経験とか個人で小さくアプリを作成している方からするとあまり使う機会は多くないかもですが、割と便利なので頑張って覚えてください。
当時の僕に向けたメッセージでした。
- 投稿日:2021-03-01T10:33:22+09:00
[iOS] Storyboard Referenceを活用しStoryboardを分割する
はじめに
storyboardを使用したiOSアプリ開発において、1つのStoryboardに対したくさんのViewControllerが詰め込まれているのは良くないということなので、Storyboardを分割する方法を記載します。
1つのStoryboardにViewControllerを複数突っ込んだ良くない例 StoryboardReferenceを活用し、Storyboardを適切に分割している例 概要
環境
Xcode : Version 12.4
内容
ViewControllerを用意し、それに対応するStoryboardファイルを作成する。
(1画面につき1つのViewControllerである前提です)実装手順
1) Storyboardファイルを作成する
main.storyboardと同ディレクトリにて
commond + N
で新規ファイルを作成します。
ファイルの種類をStoryboardと選択し、決定します。
その後にファイル名を入力するのですが、今回は"second"とします。
(この時、初期値がStoryboard
と大文字Sで入力されているのですが、これを残した状態でsecond.Storyboard
などとしてしまうと、拡張子エラーでファイルが開けなくなるので注意してください)2) ViewControllerファイルを作成する
次に、新しく作成された上記のsecond.storyboardに対応するViewControllerファイルを作ります。
先ほどと同じ要領で作成していきます。
Cocoa Touch Classを選択し、ファイル名はsecondViewController
としました。このように2つのファイルが出来ていればOKです。
3) Storyboard Referenceを設置する
ではいよいよStoryboard Referenceを設置していきます。
main.storyboardのLibraryからObjectを検索します。ref
と入れれば出てくるので空いているところにドラッグ&ドロップしていきます。
設置できたらStoryboard ReferenceのInspectorsで、対応するStoryboardを選びます。今回は先ほど作成した
second
となります。4) segueの設定を行う
画面遷移のためにsegueの設定をします。
ViewControllerからStoryboard Referenceにsegueを繋ぎます。
segueを繋いだらsegue Identifierも設定しておきましょう。今回はgoSecond
とします。segueについての解説は本記事では省略しますが、以下の記事がとても分かりやすく参考にさせて頂きました。ありがとうございます。
- 【Swift5/Xcode】画面遷移のチートシート。Segueを画面遷移とSegueを使わない画面遷移を徹底解説5) 作成したstoryboardにViewControllerを設置
1)で作成した何もない状態のsecond.storyboardにViewControllerを設置します。
設置後はViewControllerの
Custom Class
の設定、Is Initial View Controller
のチェックをします。
6) 画面遷移を試す
Storyboardを切り離す作業・設定は完了したので、下記2点を追記してちゃんと動作するかどうかを画面遷移させてみて確認します。
- main.storyboardにButtonを設置
- ViewController.swiftに画面遷移のメソッドを記述
- (遷移先のViewに色をつけて分かりやすくする)
// ViewController.swift @IBAction func goSecondButtonTapped(_ sender: UIButton) { performSegue(withIdentifier: "goSecond", sender: nil) }後語り
以上が
Storyboard Reference
活用による、Storyboardを分割して1つのStoryboardに対して1つのViewController
という配置をする方法となります。
このようにStoryboardを分割して管理することで、コンフリクトも起こりにくくなります。
私自身もしっかりと理解を深め活用していきたいと思います。
- 投稿日:2021-03-01T03:59:36+09:00
【Xcode/Swift豆知識 #1】 「Color Literal」で直感的にカラー指定
はじめに
Xcode標準搭載のカラーパレット「Color Literal」を使うことで、直感的にUIColorを指定できます。
本記事は、
- コードで色を指定するのが面倒と感じる人
- NavigationBarやTabBarなどの色を直感的に変更したい人
これらに当てはまる人におすすめです!
開発環境
- Swift5
- Xcode 12.4
実際のコード
「color...」と打つと、UIColorの候補に「Color Literal」が出てきます。
RGBから色を決めたり、Opacity(透明度)なんかもいじれちゃいます!便利!
最後に
今日はXcode / Swift豆知識その1として、「Color Literal」をご紹介しました。
NavigationBarやTabBarの色を変更したいときは特に便利ですので
ぜひ使ってみてくださいね!参考記事:iPhoneアプリのヘッダー(ナビゲーションバー)の色を変更する
ここまで読んでいただいて、ありがとうございました。
- 投稿日:2021-03-01T03:42:39+09:00
【Swift】UIPickerViewについて コードで表示/複数列
はじめに
私がSwiftを勉強し始めて一番最初に苦戦したのがPickerViewでした。なので個人でのQiitaの最初の投稿もPickerViewについてです!
私だけでなく、よくわかんないって人が意外と多いと感じたので簡単にまとめてみました。
参考までにどうぞ。storyboardの作成
今回は複数の列を選択できるPickerViewを作成するので、わかりやすくするためにTextFieldはstoryboardで設置した。
手順
①デリゲートメソッドを追加
②変数宣言
③PickerViewの設定
④toolbarの設定①デリゲートメソッドを追加
UIPickerViewDelegate, UIPickerViewDataSource
これらを追加します。
(最終的なコードは最後にまとめて表示します。)②変数宣言
ViewController.swift@IBOutlet weak var year:UITextField! @IBOutlet weak var month:UITextField! @IBOutlet weak var day:UITextField! var PickerView = UIPickerView()storyboardに配置したTextFieldと今回使うPickerViewの宣言を行います。
ViewController.swiftvar yearArray:[String] = ["2021","2022","2023","2024","2025","2026","2027","2028","2029","2030"] var monthArray:[String] = ["1","2","3","4","5","6","7","8","9","10","11","12"] var dayArray:[String] = ["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31"]また今回表示させる配列も宣言しておきます。
ViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() PickerView.delegate = self PickerView.dataSource = self }またViewDidLoadの中に、この画面でPickerViewのデリゲートメソッドを使用することを言っておきます。
ここは忘れやすいので注意!③PickerViewの設置
さて準備も終わったところなのでいよいよ本題のPickerViewの設定です。
ViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() //さっき書き込んだところ PickerView.delegate = self PickerView.dataSource = self //追記 year.inputView = PickerView month.inputView = PickerView day.inputView = PickerView }まずViewDidLoadの中で、先ほど宣言したTextFieldにPickerViewを当てはめます。
これを行わないとtextFieldを選択してもPickerViewが下から出てきません。ここからはPickerViewの中身について具体的に設定していきます
列の設定
ViewController.swiftfunc numberOfComponents(in pickerView: UIPickerView) -> Int { return 3 }まず列の個数を選択します。西暦、年、日の3つを同時に記入したいので、列の個数は3です。
3を返すように設定しましょう。行の設定
ViewController.swiftfunc pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { switch component { case 0: return yearArray.count case 1: return monthArray.count case 2: return dayArray.count default: return 0 } }それぞれのTextFieldで選択したい値の数が違うのでswich文を用いて条件分岐を行います。
デリゲートメソッドが記入されている状態で"numb"ぐらいまで打つと選択肢として出てきますよ!!表示名の設定
ViewController.swiftfunc pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { switch component { case 0: return yearArray[row] case 1: return monthArray[row] case 2: return dayArray[row] default: return "error" } }表示名って何?って思う人もいるかもしれませんが、簡単にいうとPickerViewの内容を記入するものになります。
このコードがないとPickerViewの内容が"?"となって選択されているものがどれかわからないので、必ず書きましょう。以上でPickerViewの設定は終了となります。
次はPickerViewで選択したものをTextFieldに表示させます
ViewController.swiftfunc pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { switch component { case 0: year.text = yearArray[row] case 1: month.text = monthArray[row] case 2: day.text = dayArray[row] default: break } }簡単ですね。同じようにして表示させるだけです。
ここまででPickerViewの実装は完了しました!
次はtoolbarを実装してちょっとだけおしゃれにしていきましょう
そもそもtoolbarって?
そうなんです。上にちょっぴり生えてきたものありますよね?それが今回表示させたいtoolbarです。
今から解説していきますね!
ViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() //さっき書き込んだところ PickerView.delegate = self PickerView.dataSource = self //さっき書き込んだところ year.inputView = PickerView month.inputView = PickerView day.inputView = PickerView //追記 let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 50)) let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil) let done = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(ViewController.donePressed)) let cancel= UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(ViewController.cancelPressed)) toolbar.setItems([cancel , space, done ], animated: true) year.inputAccessoryView = toolbar month.inputAccessoryView = toolbar day.inputAccessoryView = toolbar }最初にtoolbarを作らないといけませんね。なのでViewDidLoadの中で上の追記のようにします。
まず、toolbarをどのようなものするかを決め、そしてそこに表示させるアイテム(ここではcancelとdoneとspace)を設定します。次ににtoolbarの中に順番に表示させて見やすいようになるまで工夫します。
そして最後に、toolbarを表示させるコードを書いたら完了です。ここで気づいた方もいるかもしれませんが、
「"ViewController.donePressed" , "ViewController.cancelPressed"」 ってなんだよ!って思いませんでしたか?
ごめんなさい...順番が難しくて...今から解説していきます!まず何かと簡単にいうと、cancelボタンとdoneボタンが押された時にどうするかというものですね!
それを決めないとtoolbarを置いた意味がありません。今回はcancelボタンとdoneボタン両方ともPickerViewが画面から消えるということにします!
簡略化してすいません?♂️
コードは以下の通りになります。ViewController.swift@objc func donePressed() { view.endEditing(true) } @objc func cancelPressed() { view.endEditing(true) }これをclass内のViewDidLoadの外に記入してください。
はい!これで以上になります!
最終的には以下のようになります。
import UIKit class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource { @IBOutlet weak var year:UITextField! @IBOutlet weak var month:UITextField! @IBOutlet weak var day:UITextField! var PickerView = UIPickerView() var yearArray:[String] = ["2021","2022","2023","2024","2025","2026","2027","2028","2029","2030"] var monthArray:[String] = ["1","2","3","4","5","6","7","8","9","10","11","12"] var dayArray:[String] = ["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31"] override func viewDidLoad() { super.viewDidLoad() PickerView.delegate = self PickerView.dataSource = self year.inputView = PickerView month.inputView = PickerView day.inputView = PickerView let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height:50)) let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil) let done = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(ViewController.donePressed)) let cancel = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(ViewController.cancelPressed)) toolbar.setItems([cancel , space, done ], animated: true) year.inputAccessoryView = toolbar month.inputAccessoryView = toolbar day.inputAccessoryView = toolbar } func numberOfComponents(in pickerView: UIPickerView) -> Int { return 3 } func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { switch component { case 0: return yearArray.count case 1: return monthArray.count case 2: return dayArray.count default: return 0 } } func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { switch component { case 0: return yearArray[row] case 1: return monthArray[row] case 2: return dayArray[row] default: return "error" } } func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { switch component { case 0: year.text = yearArray[row] case 1: month.text = monthArray[row] case 2: day.text = dayArray[row] default: break } } @objc func donePressed() { view.endEditing(true) } @objc func cancelPressed() { view.endEditing(true) } }さいごに
今回はswich文を用いて条件分岐しましたが、列が1つの場合などはこのように分岐しなくて大丈夫です。
また、もう少し綺麗な書き方がある!とか少し違う!とかツッコミたくなるかもしれませんがご放念ください。
- 投稿日:2021-03-01T00:10:37+09:00
UIKit だけでタブブラウザを作ってオープンソースにした話
2020年末、こよなく愛用していた Smooz という iOS 向けのタブブラウザが突然使えなくなってしまいました。で、代わりのブラウザをいろいろ試してみたものの、Smooz みたいに手に馴染む物が見つからず... ならばと思い、試しに作ってみたところ、意外にも標準的な UI の組み合わせで作れてしまったので、一通り実装してApp Store でリリースしてみました。
また、日頃から Qiita やインターネットでいろいろな方の記事を参考にさせてもらっているので、何か還元できないかと思い、オープンソースにしてみました。独学なのであまり自身はないですが、参考にしていただければ幸いです。
ということで、このアプリをどう作ったかを解説します。
コンセプト
安心して使えるブラウザ
Smooz では個人情報の扱いが問題となったので、一切情報を収集、送信しないブラウザを目指しました1。
- 3rd パーティーライブラリ不使用
- 外部 API 不使用
- アナリティクス不使用
- 広告不使用
- WKWebKit 経由でコンテンツを操作しない
その結果、ケアすることも少なくなり、素早く実装できた気がします。
OS が提供するライブラリだけで作る
3rd パーティーのライブラリを使わないので、UI は UIKit だけで実装。データベースは Core Data を使い、データバインディングに Combine を使用しました。なので、もちろん CocoaPods / Carthage / SPM などは不要で、ビルドもめちゃくちゃ速い!
オープンソース
今回、オープンソースにしてみました。「安心して使えるブラウザ」にするためには、コードの中身も見てもらったほうがいい2 かなというのが元々のアイデアでしたが、自分のコードを参考にしてもらったり、逆に指摘してもらって自分ももっと学べることにメリットがあると思いました。なので、プルリク Welcome です。
画面構成と UIKit の使い方
メイン画面
UINavigationController -> UIViewController
の構成にして、UIViewController
の View にUIPageViewController
とUICollectionView
を配置しています。コンテンツ部分(
UIPageViewController
)
UIPageViewController
は、ビューコントローラをページとして、複数のページを管理できるコントローラです。Web コンテンツを表示する場合には
WKWebView
を表示し、検索ビュー(虫眼鏡を押した時に表示されるビュー)では、検索履歴リストを表示するためにUICollectionView
を表示しています。
Smooz では、左右にスワイプしてタブを切り替えることができ、これがとても便利でした。UIPageViewController
は標準でスワイプでのページ切り替えがサポートされていますが、今回は諦めました。というのも、Web ページがカルーセルなどのスワイプさせるコンテンツを含む場合に、操作が競合してしまうためです。これ解決するの相当大変そう。タブ部分(
UICollectionView
)このエリアには、水平方向にタブが並びスクロールができるようになっています。ここには
UICollectionView
を利用しています。ちなみにこのアプリでは、iOS13 から導入されたモダンなUICollectionView
を使っています。
NSDiffableDataSourceSectionSnapshot
を使うことで、ダイナミックなタブの追加/挿入/削除をうまく処理してくれたのが便利でした。 コードはこの辺り。コンテクストメニュー
タブを長押しすると、コンテクストメニューが表示されて、タブに対する操作ができます。
これは、UICollectionViewDelegate
のcollectionView(_:contextMenuConfigurationForItemAt:point:)
で簡単に設定できます。コードはこの辺り。検索フィールド部分(
UISearchBar
)
navigationItem.titleView
にUISearchBar
を埋めて、検索フィールドを実現しています。コードはここ。操作ボタン部分(
UIToolbar
)Storyboard で View Controller を選択すると、Attributes inspector で
Bottom Bar
というプルダウンメニューがあり、~ Toolbar
という項目を選択すると、Toolbar が表示されます。そこにUIBarButtonItem
を配置しています。最近のアプリではあまり使われていない気がしますが、Storyboard でお手軽にボタンを配置できます。メニュー(
UIMenu
)iOS14 から、ボタンに対してメニューを追加できるようになりました。今回「閉じる」ボタンにこれを使って、長押しして「すべてを閉じる」を実行できるようにしています。コードはこの辺り。
前述のコンテクストメニューに似ていますが、こちらは
UIBarButtonItem
のmenu
プロパティにUIMenu
を渡して設定します(Human Interface Guidelinesでも、別のものとして定義されています)。ブックマーク画面
ブックマークボタンを押すと、ブックマーク画面がモーダルビューで表示されます。その内側の構成は下記のようになってます。
一覧部分(UICollectionView)
ブックマーク/閲覧履歴/検索履歴一覧には、モダン
UICollectionView
を使っていて、検索ビューと共通化をしています。コードはこちら。検索フィールド部分(
UISearchController
)ナビゲーションバーに、項目を絞り込む検索フィールドと、一覧の種類を切り替える Segmented Controls が表示されていますが、これは
navigationItem.searchController
にUISearchController
を設定しています。これはずいぶん前から提供されている仕組みで、簡単に一覧を絞り込む UI を構築できます。コードはこの辺り。ちなみに、前述のメイン画面の検索フィールドとは違う実装をしています(こちらの方がむしろ標準的な実装)。また、
UISearchController
をインスタンスする際に、検索結果を表示するビューコントローラーをsearchResultsController
に指定できますが、今回は使用していません。操作ボタン部分(
UIToolbar
)こちらは、メイン画面と同様の、標準的な
UIToolbar
の実装となっています。設定画面
検索フィールドの右側のボタンを押すと、設定画面がモーダルビューで表示されます。こちらは極力 Storyboard で実装しています。
設定一覧(
UITableView
)設定一覧は、
UITableView
の Static Cells で実装しています。選択された検索エンジン名を表示したり、Safari ビューでコンテンツを表示したり、バージョン番号を動的に取得して表示する部分は、コードで実装しています。検索エンジン選択(
UITableViewController
)いくつかの項目から1つ選択する一覧画面ですが、モダン
UICollectionView
だと多少 too much な感じなので、UITableViewController
で実装しています。Safari ビュー(
SFSafariViewController
)フィードバックフォーム(Goole フォーム)や、プライバシーポリシー(Web ページ)は Safari View を使ってモーダル表示しています。Safari View は Storyboard では実現できないので、コードで表示しています。
オンボーディング画面(
UIPageViewController
)初回起動時のオンボーディング画面は、ページをめくる構成なので、
UIPageViewController
を利用しています。また、コンテンツ部分は Storyboard で作りました。
各ページはUIViewController
(下段の5つ)となっていて、Storyboard ID をキーにして下記のようにコードに読み込んでいます。pages = (1...5).map { storyboard.instantiateViewController(withIdentifier: "View\($0)") }その他
アイコン
各所でアイコンを使っていますが、これらはすべて SF Symbols を使っています。コードでは下記のように指定します。
UIImage(systemName: "magnifyingglass"),
systemName
に指定する文字列は SF Symbols 2 という macOS アプリで調べると便利です。アプリアイコン
唯一、ビジュアルデザインが必要なのがアプリのアイコンです。シンプルを売りにするアプリなので、アイコンもシンプルにしました(というかこの程度しかデザインできない)。ちなみに、Google Slide を使って作成しました。
データハンドリングまわり
Combine
アプリ全体の状態管理は
Browsers
というシングルトンのクラスで行い、各 View Model がそれを Subscribe してビューに必要な情報に変換して、ビューがそれを Subscribe して表示する、という構成になってます。この状態の監視、データの変換に Combine を利用しています。
Combine は SwiftUI と同時期にリリースされた Apple 提供のライブラリで、以前作った SwiftUI ベースのアプリで習得したのですが、UIKit ベースのアプリでも問題なく使えました。
実際の使い方は、説明できるほどちゃんと理解できていないので、ソースコードを見ていただければと思います。なお、アーキテクチャは MVVM っぽい感じになっていると思いますが、ちゃんと学んだことがないのでこれで良いのか自信がないです。
Core Data
タブの状態やブックマーク、履歴などの保存に Core Data を使いました。今のところ問題はなさそうですが、Core Data もあまり自信を持って使っているわけではないので、コードを整理したり、最適化できる余地があるかと思います。
ちなみに、途中で Core Data を使うことにしたので、こちらの記事を参考にさせていただきました。
[Xcode9] 既存プロジェクトにCoreDataを追加する方法 - Qiita
課題
iPad 対応が適当。
iPad はただ iPhone を拡大しただけになっているので、使いやすさを考慮して UI を最適化したり、
UISplitViewController
を使ってブックマーク等へのアクセス性を向上できればと思っています。参考:UISplitViewController 公式ドキュメントの和訳(iOS14 対応)
スクロールした時に上下のバーを隠す処理が適当。
使用感がいまいちなので、もう少しスムーズかつ適切な動きにしたいと思っています。
タブを削除した時に、タブの挙動がおかしくなる。
おそらく
UICollectionView
の扱い方がおかしいのだと思うけど、タブ(UICollectionViewCell
)が1つずれたり、横スクロールがおかしくなってしまう。うーむ。Basic 認証など、いくつかのブラウザ機能やエラーハンドリングがちゃんと実装できていない。
ブラウザとして必要な機能を実装するのに、この記事を参考にさせていただきました。ただ、まだ Basic 認証の入力ダイアログが実装できていなかったり、エラー処理も適当なので、今後ちゃんと実装していきたいと思います。
WKWebViewで必要十分な機能を持ったアプリ内ブラウザを作る - Qiita
Combine のイベントが余計に発行されている気がする。
UI がちらついたり、閲覧履歴が何度も更新されているっぽいので、一度 Combine の流れを見直して、不要なイベントを捨てる必要がありそう。
非同期処理を使いこなせていなさそう。
基本 UI スレッドで動かしているので、Core Data の処理とかを別スレッドで動かせる気がしますが、この辺りも詳しくないので勉強が必要。
メモリ管理もちゃんと検討・実装できていない。
大量にタブを開いた時などの考慮ができていないので、表示するまで読み込まないなどの対応が必要。また、メモリリークもちゃんと検討できていないので、もっと勉強して対応できればと思っています。
まとめ
iOS も 14 となり、既存の UI のアーキテクチャが使いやすく進化していたり、新しい UI が追加されていたり、Combine のような新しいフレームワークも出てきていて、初期の頃に比べたら、より安定したモダンな実装が可能になってきている気がします。逆にいうと、常に学んでいく必要があって追いつくのが大変ではありますが...
それでも、多くのエンジニアの方が Qiita などで新しい技術を素早くわかりやすく共有してくれているので、こうやってアプリを開発することができています。この場を借りて感謝申し上げます。お返しになるかわかりませんが、今回オープンソースにしたので、実際のアプリでどう新しい技術が使われているかの参考になればいいなと思っています。ぜひ、フィードバックもお待ちしております!
もちろん、WKWebView 内で表示している Web コンテンツが、広告を表示したりアクセス解析することを防ぐことはできないです。あ、Content Blockerを実装すれば良いのか? ↩
公開しているソースコードがそのままビルドされて App Store でリリースされている、ということは証明できないので、厳密には安心を提供できているわけではないです。もちろん、ビルド時に変なコードを入れたりはしてないですが。 ↩