質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
iOS

iOSとは、Apple製のスマートフォンであるiPhoneやタブレット端末のiPadに搭載しているオペレーションシステム(OS)です。その他にもiPod touch・Apple TVにも搭載されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

Q&A

解決済

1回答

3330閲覧

UICollectionViewで作成したカレンダーをスワイプで月の変更をしたい

sgyeta

総合スコア23

iOS

iOSとは、Apple製のスマートフォンであるiPhoneやタブレット端末のiPadに搭載しているオペレーションシステム(OS)です。その他にもiPod touch・Apple TVにも搭載されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

0グッド

0クリップ

投稿2018/10/14 12:46

編集2018/10/16 08:27

前提・実現したいこと

こちらこちらを参考にUICollectionViewを使ってカレンダーを作成しました。

ボタンによる月の変更が実装できたので、今度はスワイプ操作での月の変更を行いたいのですが
調べても手立てがわからなかったので質問させて下さい。

具体的には、添付画像の青枠の部分をスワイプすることによって
UIScrollViewのような動きでカレンダーを先月・翌日に変更させたいです。

自分なりに調べたことを下記に記述します。
アプローチの仕方が全くわからなかったのでご教示頂けると幸いです。

該当のソースコード

CalendarViewController

1class CalendarViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout{ 2 3 @IBOutlet weak var navigationbar: UINavigationBar! 4 @IBOutlet weak var calendarView: UICollectionView! 5 6 let dateManager = DateManager() 7 let weekArray = ["日","月","火","水","木","金","土"] 8 9 override func viewDidLoad() { 10 super.viewDidLoad() 11 12 navigationbar.barTintColor = UIColor.themeColor() 13 navigationbar.isTranslucent = false 14 navigationbar.titleTextAttributes = [.foregroundColor: UIColor.white] 15 calendarView.delegate = self 16 calendarView.dataSource = self 17 18 //ナビゲーションタイトルの設定 19 let selectedDate = self.dateManager.selectedDate 20 self.navigationItem.title = self.dateManager.getFormattedDay(date: selectedDate) 21 } 22 23 override func didReceiveMemoryWarning() { 24 super.didReceiveMemoryWarning() 25 } 26 27 //カレンダーを前の月に戻す 28 @IBAction func movePrevMonth(_ sender: UIButton) { 29 let currentDate = dateManager.selectedDate 30 let prevDate = dateManager.getPrevDate(currentDate: currentDate) 31 calendarView.reloadData() 32 self.navigationItem.title = dateManager.getFormattedDay(date: prevDate) 33 } 34 35 //カレンダーを次の月に進める 36 @IBAction func moveNextMonth(_ sender: UIButton) { 37 let currentDate = dateManager.selectedDate 38 let nextDate = dateManager.getNextDate(currentDate: currentDate) 39 calendarView.reloadData() 40 self.navigationItem.title = dateManager.getFormattedDay(date: nextDate) 41 } 42 43 //Sectionの数 44 internal func numberOfSections(in collectionView: UICollectionView) -> Int { 45 return 2 46 } 47 48 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 49 switch(section){ 50 case 0: 51 return CGSize(width: self.view.frame.width, height: 0) 52 default: 53 return CGSize(width: 0, height: 0) 54 } 55 } 56 57 //Cellの総数を返す 58 func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 59 // Section毎にCellの総数を変える. 60 switch(section){ 61 case 0: 62 return 7 63 case 1: 64 return dateManager.getDayCellNumber() 65 default: 66 print("error") 67 return 0 68 } 69 } 70 71 //Cellに値を設定する 72 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 73 // Section毎にCellのプロパティを変える. 74 switch(indexPath.section){ 75 case 0: 76 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CalendarCellOne", for: indexPath as IndexPath) as! CalendarCellOne 77// cell.backgroundColor = UIColor.themeColor() 78// cell.dateLabel.font = UIFont.boldSystemFont(ofSize: 14) 79// cell.dateLabel.textColor = UIColor.white 80 cell.dateLabel.text = weekArray[indexPath.row] 81 cell.trashIconOne.image = nil 82 return cell 83 case 1: 84 let ordinalityOfFirstDay = Calendar.current.ordinality(of: .day, in: .weekOfMonth, for: dateManager.getFirstDateOfMonth()) 85 let dateRange = NSCalendar.current.range(of: .day, in: .month, for: dateManager.getFirstDateOfMonth()) 86 var dateText = self.dateManager.conversionDateFormat(indexPath: indexPath) 87 88 if (ordinalityOfFirstDay! - 1) > indexPath.row || ((ordinalityOfFirstDay! - 1) + dateRange!.count) - 1 < indexPath.row{ 89 dateText = "" 90 } 91 92 let kinds = dateManager.getTrashKind(indexPath: indexPath) 93 if let kinds = kinds{ 94 switch(kinds.count){ 95 case 0: 96 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CalendarCellOne", for: indexPath as IndexPath) as! CalendarCellOne 97 cell.layer.borderColor = UIColor.lightGray.cgColor 98 cell.layer.borderWidth = CGFloat(0.5) 99 cell.dateLabel.text = dateText 100 101 return cell 102 case 1: 103 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CalendarCellOne", for: indexPath as IndexPath) as! CalendarCellOne 104 cell.layer.borderColor = UIColor.lightGray.cgColor 105 cell.layer.borderWidth = CGFloat(0.5) 106 cell.dateLabel.text = dateText 107 108 if !(cell.dateLabel.text?.isEmpty)!{ 109 //cell.trashIconOne.image = TrashInfoManager.getTrashIcon(kind: kinds[0]) 110 } 111 return cell 112 case 2: 113 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CalendarCellTwo", for: indexPath as IndexPath) as! CalendarCellTwo 114 cell.layer.borderColor = UIColor.lightGray.cgColor 115 cell.layer.borderWidth = CGFloat(0.5) 116 cell.dateLabel.text = dateText 117 118 if !(cell.dateLabel.text?.isEmpty)!{ 119 //cell.trashIconOne.image = TrashInfoManager.getTrashIcon(kind: kinds[0]) 120 //cell.trashIconTwo.image = TrashInfoManager.getTrashIcon(kind: kinds[1]) 121 } 122 123 return cell 124 // case 3: 125 // case 4: 126 default: 127 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CalendarCellOne", for: indexPath as IndexPath) 128 cell.backgroundColor = UIColor.white 129 return cell 130 } 131 } 132 133 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CalendarCellOne", for: indexPath as IndexPath) 134 cell.backgroundColor = UIColor.white 135 return cell 136 default: 137 print("section error") 138 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CalendarCellOne", for: indexPath as IndexPath) 139 cell.backgroundColor = UIColor.white 140 return cell 141 } 142 } 143 144 //セルのサイズを設定 145 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 146 let width: CGFloat = collectionView.frame.size.width / CGFloat(7.0) 147 var height:CGFloat = 0 148 149 if indexPath.section == 0{ 150 height = width * CGFloat(0.5) 151 }else if indexPath.section == 1{ 152 height = width * CGFloat(1.5) 153 } 154 155 return CGSize(width:width, height:height) 156 } 157 158 //セルの垂直方向のマージンを設定 159 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 160 return 0 161 } 162 163 //セルの水平方向のマージンを設定 164 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 165 return 0 166 } 167}

調べたこと

【iOS】【swift】カレンダーを作ってみる
こちらが参考になりそうかと思いましたが、カレンダーの実装方法が自分のものと違うため
どのようにUIScrollViewと関連付ければいいのかわかりませんでした。

画像

イメージ説明

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

5つ思いつきました
1.スワイプというジェスチャーを取得して、CollectionViewの中身のcellを入れ替える
2.横向きのUICollectionViewを作って、セルの中に1月のUICollectionViewを入れてしまう
3.1月をUICollectioinViewの1つのsectionと捉えて、横ページングする
4.1月をUICollectioinViewの1つのsectionと捉えて、縦ページングする(Apple公式アプリと同様)
5.ライブラリを使う

1は簡単ですが、切り替わりが一瞬です
Swipe Gesture Recognizeを使います
2,3,4は初学者には高難度です
(ページングさえできれば3,4は可能ですが)
ライブラリは最も良い結果になります(学習のためなら使えませんが)

投稿2018/10/16 11:46

kosanai

総合スコア471

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sgyeta

2018/10/17 00:06

ご回答ありがとうございます。 1はご認識の通り切り替わりが一瞬であるため別の方法が良さそうです。 日付のセルをカスタムセルで作りたかった+横にページングさせたいので 2か3になるかと思いますが、具体的に実装の流れとしては 既存のUICollectionViewを動的に生成して、大枠のUICollectioinViewの sectionに入れる流れになりますでしょうか?
kosanai

2018/10/17 07:01

それぞれ、もう一段具体的に大筋を書いてみます (試しで実装していないので私も勘違いあると思います)   2.1ヶ月分のUICollectionViewを1つのCellに入れてしまう方法 既存のUICollectionViewを動的に生成するのはそのとおりです 年と月を与えたら自動で生成するようにします 月のcellをxibで定義して、その中にUICollectionViewを入れ、日のcellを別途xibで定義します このUICollectionViewは縦のスクロールをしてほしくないので、月cellの高さ>contentSizeの高さになるようにします。 多分カレンダーの高さが予め決まって、それによって中のcellのサイズが変わるでしょうから、それほど問題はないと思います(逆だと大変) 横方向のUICollectionViewを画面に定義して、Storyboard上でScrollDirectionをHorizontalに、PagingEnabledにチェックします cellを入れて、Classを先程定義した月のCell名にします。CellのIdentifierも定義します layoutを定義して、cellSizeを1cell分の大きさにします delegateとdatasourceとlayoutが2セットできてしまいますが、月の方をcell内に定義するのはおすすめしません。cellは使い回しされるものなので、OSの都合で生成されたり破棄されたりします。 両方ViewControllerに定義するか、delegateとdatasourceを管理するクラスを作り、ViewControllerで持つようにします UICollectionViewは左から右に流れるので、カレンダーとしては不都合です。 例えばcell数を100にすれば、100ヶ月しか存在しないことになります。 これは妥協が必要です。 ・無限スクロールを頑張って作る(難しいです) ・右から左に流れるようにする(難しいです) ・今の年から前後100ヶ月と置く(簡単です)   100ヶ月とおいた場合、今の月を例えば100番目のセルにして、初期表示cellを100番目にすれば今の月から表示できます。viewDidAppearでcollectionView.scrollToItemAtIndexPath()を呼びます。 3.1月をUICollectioinViewの1つのsectionと捉えて、横ページングする これがなぜ厄介といえば、横向きのUICollectionVIewを作ってもらえればわかるのですが 横向きの場合、順序が左上→左下→右上→右下の順に表示されるからです カレンダーの向きとは違いますよね。 しょうがないので幾つか考えられると思います ・曜日を1セルにする(6日分の日を縦に並べたもの) ・左上→左下→右上→右下の順に、日のcellを並べていく ・各cellのframeを計算して定義していく どれも厄介ですが、「左上→左下→右上→右下の順に、日のcellを並べていく」がまだましかな、と思います。 しかしこの場合、曜日の部分の見た目や、月の部分の見た目をどうするかというのが悩ましくなります。 SectioinHeaderはどうかとも思いますが、横UICollectionViewの場合はSectioinHeaderも横方向です。 アイディアとしては ・UICollectionViewとは別のViewにして、スクロール後に表示を切り替える ・UICollectionViewとは別のUICollectionViewにして、スクロール位置を一致させる cellのframeを手計算すれば全て解決しそうですが、これもちょっと大変です 参考 https://dev.classmethod.jp/smartphone/iphone/ios6-uicollectionview-customlayout/ ___ 2点備考 ・なぜAppleが4を採用しているか たぶんですが、トリッキーなことをせず1個のCollectionViewで成り立つからです CollectionVIewを左上から右下へ並べるということは、スクロールは縦方向ですから ・おそらく多くの有名ライブラリはもっと低レイヤーのコンポーネントをふんだんに使っていると思います。軽量化や自由度、アニメーションのため(この方法は私も苦手です) (完全に初学者向けではないですね。カレンダーは人気テーマですが、世にある参考コードはここまでやっていません。本気でやると本気で大変です。なので実際の開発では、UITableViewにしたり、UIDatePickerを用いたり、ライブラリを使うことが多いです)
sgyeta

2018/10/18 00:17

ご回答頂きありがとうございます。 具体的な大筋を書いて頂き非常に恐縮です。 2.1ヶ月分のUICollectionViewを1つのCellに入れてしまう方法 100ヶ月先も確認することは想定していないので、ここは妥協で大丈夫かと思います。 こちらで実装するのが良さそうかなと思いますが追加で質問させて下さい。 月をスワイプで切り替えている際に、月と月の間でスワイプをやめた時に 表示としては月中で止まってしまうのではないかと思ったのですが こちらは上記の実装方法で解消出来ますでしょうか? 意図としては、月中でスワイプをやめたら遷移先か遷移元の月のカレンダーを表示したいからです。 私の方で調べてみましたが解決出来なかったので、ご教示お願いしたいです。
kosanai

2018/10/18 01:15

PagingEnabledはONになっていますか? Storyboard>UICollectionView>右カラム>ShowTheAttributeInspector>Scrollingです これはUIScrollViewのパラメータです 1ページの長さはUICollectionViewの長さになっていると思います ずれていたらレイアウトを調整する必要があるかもしれません なおページの長さを調整する方法もあります UICollectionViewFlowLayoutの - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity; をオーバーライドします。
sgyeta

2018/10/20 08:37

ご回答頂きありがとうございます。 返信が遅くなり恐縮です。 PagingEnabledがONになっていなかったので 変更したら大枠のcollectionViewの横スワイプによるセルの変更部分は実装出来ました。 後はこのセルの中に、今まで作ったカレンダーのcollectionViewを 入れてあげれば良さそうかなと思うのですが1点質問させて下さい。 (xibを使ったことがないのでそこは今調べながらやってます) 自分で言っておきながら「既存のUICollectionViewを動的に生成して」 この部分の実装がいまいちピンと来ておりません・・・。 UICollectionViewを継承したクラスを作ることはわかるのですが 今は各月の日のラベルやセルの数なんかは全て上記の貼り付けたコードの CalendarViewControllerの方でやってますが、それを新しく作った UICollectionViewを継承したクラスのイニシャライザの方でやるってことでしょうか・・・?
kosanai

2018/10/20 09:36

継承するのはCellです   横UICollectionView(親)の中にCell(親)があり その中に月のUICollectionView(子)があり、その中にセクションがあり、その中に日のCell(子)がある、という状態は分かると思います 継承するのはCell(親)です もしここで分からなければUICollectionViewが理解できていないかもしれません。「UICollectionView カスタムセル」あたりを覚えましょう(必要知識1) カスタムセル(親)を作る際に、Xcodeの仕様では入れ子にできないので、カスタムセル(親)をxibで作成して、UICollectionView(親)に紐づけます(必要知識2) ちなみにカスタムセル(親)の中にはUICollectionView(子=月)があります カスタムセル(子=日)はまたxibで定義します (結果的に、storyboard、月セルxib、日セルxibができます) 「動的に生成」は、UICollectionViewやUITableViewの機能です。 「年と月が与えられたら正しい月のカレンダーがレイアウトされる」という状態を目指してください(参考コードに似たものがありますが) (「状態」と「レイアウト」を一緒に考えると混乱するかもしれません)
sgyeta

2018/10/22 06:03

ご回答頂きありがとうございます。 ちょっと整理出来てないので回答頂いた上記の部分を見ながら 自分で手を動かして試してみます。サンプルコードまでご提示頂き恐縮です。 またコードを見てもわからない部分がありましたら質問させて頂けると幸いです。 この度はありがとうございました!
sgyeta

2018/10/22 15:08

1箇所実装していてわからない部分があったので質問させて下さい。 githubの方を参考にさせて頂きながら実装したのですが カレンダーがものすごく小さくなってしまいました。 日のセルのwidthとheightを決めているところがいまいち理解出来てないのですが MonthCalendarCollectionViewCell.swiftのsetupLayout()の中でしょうか?
kosanai

2018/10/23 01:20

setupLayout()の中です 日のセルはUICollectionViewFlowLayoutのインスタンスでitemSizeを設定しています そのlayoutをsetCollectionViewLayout()で設定しています セルのサイズがどうなっているか、printデバッグしてみてください また、itemSize = CGSize(width:100, height:100)などと適当な値を試してください カレンダー本体のViewはViewControllerにAutoLayoutで設定してあります
sgyeta

2018/10/25 07:41 編集

ご回答頂きありがとうございます。 返信が遅くなり恐縮です。 githubのものを手元に落として色々といじってみたのですが わからない箇所があったので質問させて下さい。 日付のセルの大きさについてですが セルのサイズがどうなっているか、printデバッグしてみてください また、itemSize = CGSize(width:100, height:100)などと適当な値を試してください こちらについて試してみました。日付のセルのサイズは UICollectionViewFlowLayoutのインスタンスででtemSizeで決めていることはわかりました。 今やりたいこととしては、カレンダー本体のViewを 画面いっぱいに広がるようにAutoLayoutを設定し Monthのセルも親のカレンダー本体のViewと同じ大きさにして DayのセルはwidthがMonthのセルの1 / 7(7曜日あるから)で Dayのセル同士の余白を0にして敷き詰めたいのですがうまくいきませんでした。 試したことは貼っていただいたgithubのものの MonthCalendarCollectionViewCell.swiftのsetupLayout()の 実装を以下のように変更致しました。 func setupLayout() { let layout = UICollectionViewFlowLayout() layout.itemSize = CGSize(width:monthCalendarCollectionView.bounds.width/7.0 , height:monthCalendarCollectionView.bounds.height/7.0 ) layout.minimumInteritemSpacing = 0 layout.minimumLineSpacing = 0 layout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 0) monthCalendarCollectionView.setCollectionViewLayout(layout, animated: false) } calendarCollectionView.bounds = monthCalendarCollectionView.boundsだと思うので それを7当分して子のDayのセルの大きさに設定しているので期待通りの レイアウトになるかと思うのですが、どこが違うのでしょうか・・・。 ご教示頂けたら幸いです。
kosanai

2018/10/25 07:25 編集

AutoLayoutで最もハマるポイントの一つです ViewControllerやViewの子Viewがきちんとレイアウトが終わっているかどうか検知しなければなりません でないと変な値が返ってきます 今ほしいのは、MonthCalendarCollectionViewCellの子Viewのレイアウト情報なので UIView内でレイアウトがされたのを検知する必要があります ```swift override func layoutSubviews() { super.layoutSubviews() setupLayout() } ``` をMonthCalendarCollectionViewCellに入れてください なお詳しくは「UIVIewのライフサイクル」や「UIViewControllerのライフサイクル」を調べてください AutoLayoutが完了するのはそれぞれlayoutSubviews()と、viewDidLayoutSubviews()です
sgyeta

2018/10/25 07:41

ご回答ありがとうございます。 確かに変な値が返ってきていました。 実装自体は上記のコードを追加したら出来ました! ライフサイクル調べてみます! 非常に勉強になりましたありがとうございました!
sgyeta

2018/11/12 13:22

>kosanai様 お世話になっております。 こちらでお聞きした実装方法で作成したカレンダーについて お伺いしたいことがありコメントさせて頂きました。 DayCollectionViewCellのタップ時の処理を実装したく タップ時にアラートを表示するようにしたいのですが Use of unresolved identifier 'present'とエラーが出てしまいます。 MonthCalendarSourceクラス内にタップ時の処理を記述しており MonthCalendarSourceはUIViewControllerを継承していないので 上記のエラーが出る原因は理解出来るのですが 解決方法がわからなかったので追加でお伺いした次第です。 本来であれば別スレを立てなければいけないかと思うのですが 上記の一連の流れを説明するのが難しいと思いこちらに記載してしまいました。 恐らくマナーとして宜しくないため、お気付きになり次第 別スレでご教示頂ければと思います。宜しくお願い致します。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問