2022年度 入部
霽月
Apple信者
せいげつです。プログラミングが趣味です。情電数理の2回生。言語は Python Swift HTML JavaScript Java C# などができます。Unity齧ったりReact齧ったりしてました。最近はXcodeのiOSアプリ開発をやってます。Twitter:@Se1getsu
この人が書いた記事
プログラミング言語を自作してみた
内容を見る
内容霽月です。自作UIの紹介記事です。 UIButtonっていうのはSwiftのボタンのことなんですけど、UIButtonって標準だと押したとき何のアニメーションもないんですよね。ということで今回は、色んな場面で使えそうなUIコンポーネントを3つ制作したので、その紹介をしようと思います。Apple純正アプリのUIを真似たのでユーザーに分かりやすく、UX向上が図れます。3つのUIは独立していて、依存関係はありません。PushableButton:押してる間ちょっと暗くなるボタンです。HoldableButton:長押しが可能なボタンです。HorizontalButtonGroup:横一列にボタンを並べます。実際にこれらを使用したサンプルソースを書いていきます。先に完成形をお見せします。 なお、3つのUIのソースと、今回作るサンプルソースはGitHubにて公開しています。 ボタングループを画面に配置まずはボタングループと、3つのボタンを宣言します。 左右の2つはHoldableButton、中央のはPushableButtonにします。 let buttonGroup: HorizontalButtonGroup = { let buttonGroup = HorizontalButtonGroup() buttonGroup.backgroundColor = .systemCyan.withAlphaComponent(0.2) buttonGroup.separatorColor = .systemCyan buttonGroup.layer.cornerRadius = 10 return buttonGroup }() let leftButton: HoldableButton = { let button = HoldableButton() button.setTitle("←", for: .normal) button.setTitleColor(.black, for: .normal) return button }() let centerButton: PushableButton = { let button = PushableButton() button.setTitle("⚪︎", for: .normal) button.setTitleColor(.black, for: .normal) return button }() let rightButton: HoldableButton = { let button = HoldableButton() button.setTitle("→", for: .normal) button.setTitleColor(.black, for: .normal) return button }()viewDidLoadで配置します。ボタングループのaddSubButtonというメソッドで、ボタンを順番に配置することができます。 buttonGroup.translatesAutoresizingMaskIntoConstraints = false buttonGroup.addSubButton(leftButton) buttonGroup.addSubButton(centerButton) buttonGroup.addSubButton(rightButton) view.addSubview(buttonGroup) let safeAreaGuide = view.safeAreaLayoutGuide NSLayoutConstraint.activate([ buttonGroup.centerXAnchor.constraint(equalTo: safeAreaGuide.centerXAnchor), buttonGroup.topAnchor.constraint(equalTo: safeAreaGuide.topAnchor, constant: 30), buttonGroup.widthAnchor.constraint(equalToConstant: 180), buttonGroup.heightAnchor.constraint(equalToConstant: 30) ])オートレイアウト制約でセーフエリアの上の方にボタングループを表示するようにしました。実行するとこんな見栄えで表示されます。 数字をカウントするラベルを配置画面中央にcountというメンバの値を表示するラベルを作りましょう。 ここではcountを初期値0、負にはならない整数としておきます。 let numberLabel: UILabel = { let label = UILabel() label.font = UIFont(name: "Menlo-Regular", size: 46) label.textColor = .black label.text = "0" return label }() private var _count = 0 private var count: Int { get { return _count } set { _count = max(0, newValue) numberLabel.text = "\(count)" } }オートレイアウト制約を使います。 numberLabel.translatesAutoresizingMaskIntoConstraints = falseaddSubviewして、 view.addSubview(numberLabel)NSLayoutConstraint.activate内に制約を書きます。 numberLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), numberLabel.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor)そうすれば、画面中央にMenloフォントで0と表示されます。 ボタンのイベントを登録右のボタンでcountを増加、左のボタンで減少、中央のボタンで10〜99の範囲のランダムな整数に設定します。 @objc func rightButtonTapped() { count += 1 } @objc func leftButtonTapped() { count -= 1 } @objc func centerButtonPressed() { count = Int.random(in: 10...99) }HoldableButtonにイベントを登録する時は、addTargetではなくaddEventを使います。 rightButton.addEvent(self, action: #selector(rightButtonTapped)) leftButton.addEvent(self, action: #selector(leftButtonTapped)) centerButton.addTarget(self, action: #selector(centerButtonPressed), for: .touchUpInside)これで冒頭に見せたサンプルが完成します。
内容霽月です。作品紹介記事です。 2023年の新歓からTwitterで「岡大電算研質問箱」が公開されましたね。おはようございます!(激おそ挨拶) 電子計算機研究会の質問箱を作成したので、私たちのことで質問がある人はなんでも送ってください!#春から岡大 #春から岡大生へhttps://t.co/ITbhX15Ig8— 岡山大学電子計算機研究会 (@oucrc) April 2, 2023 これは去年僕がReactを触った時に練習で作ったやつです。 コードを使ったWeb制作は未経験だったのでやってみようと思い、鮭さんにNext.js教えてもらったのがきっかけです。 部活の質問箱作るぞーとなって勉強を始めました。具体的に何をやったかというと、ReactとNext.jsの公式チュートリアルを読んだ。データベースの作り方(Firebase)と操作方法を調べた。JavaScriptは習得済みということもあり、勉強は2日で終わらして作り始めました。 正味の制作期間は1ヶ月くらい。Web初心者すぎて色々苦戦したりしましたが、あんまりかかってないですね。 質問画面質問画面が、僕の1番のこだわりポイントです(ˊᗜˋ*) Peingとかマシュマロとか、ほとんどの質問箱サイトって、真っ白な四角い入力ボックスに質問を入力しますよね。でもこの質問箱では質問カードに直接入力できるので、投稿後の見栄えを常に確認しながら書けるんです!この方が絶対楽しいですよね。 そして注目すべきは送信時のアニメーション!質問カードが画面奥に向かって飛んでいきます!これぞ「遊び心」ってやつです。これが見たくて2個目の質問書きたくなる人もいそう。 質問一覧質問カードを押したら回答が書いてある個別ページに飛ぶようになってます。 ここは特にこだわりないので、配色の話をしましょうか。 質問カードの色は、電算研の毛筆みたいな文字に合わせて、木をイメージした茶色にしました。淡い色が僕は好みなので、淡めの茶色です。 背景はサイバー感を出そうと思って緑にしましたが、背景が主張し過ぎると見づらくなるので、落ち着いたチェック柄にしました。サイバーというよりピクニック気分です。この部のゆる〜い雰囲気が伝わればいいなと思いました。 画面の広い端末では左右に緑のグラデーションが見えますね。よくよく観察すると分かるんですけど、このグラデーション、実は動いてます。気付いた人いるかな? ログイン画面上の「回答する」を押したらログイン画面が出てきます。ログインすると回答用のページが開けるようになります。 僕が作ったのはパスワード認証画面だったんですが、あんどうさんが幹部Googleアカウント認証に改造したので跡形もありません。確かに、これ専用のパスワード覚えて入力するよりGoogleログインの方が圧倒的に楽ですよね。名案としか言いようがありません。 サムネ画像Twitterでの利用を想定して、質問ごとにサムネ画像が自動で生成されるようになってます。 僕が書いたら何故か動かなかったので、あんどうさんの力を借りています。 現時点、たまに表示されなかったりバグったりしてますが、対応中です。 ('23.4.17) 改善しました! Slack通知Slackのbotと連携して、質問が送信されると部のSlackに通知するようにしています。 何か言いたいことがある部員は、返信に書けば回答者が読んでくれます。 作った感想今回は体験というノリでWebやってみましたが、結構難しいですね。僕は2日しか勉強してませんが、Webエンジニア目指す人向けの超長いロードマップとか見ると、そこまで頑張れるほどWebに興味はないなあって思いました。最近Macを買ったので、iOS専門の人になろうかなと思ってます。 でもまあ、自分の作ったサービスがこんな風に実際に使われて、自分の端末のブラウザからもアクセスできるのを見ると、達成感はありますし、楽しいです。 この部活は部活動フェスティバルのHPを任されてたり、このサイトの運営もあったり、Webの人はやること色々あるので、少しでも興味ある人は僕みたいに試しに齧ってみるといいですよ!
内容霽月です。Swift初心者向けの記事です。 今回は少し難しめの画面遷移に挑戦してみました。 iOSにはPopover(ポップオーバー)と呼ばれるUIがあり、例えばボタンを押した際に、そのボタンから出る吹き出しのような形でビューを表示することができます。画面の大きいiPadのアプリではよく使われるUIですが、基本的にiPhoneではモーダルで表示されるので、普段iPhoneしか使わない人は見たことがないかもしれません。 今回はそのPopoverの吹き出しの中でプッシュ遷移をやってみようと思います。普通のプッシュ・モーダル遷移などに比べれば割とマイナーな遷移ですが、Apple純正の「時計」「Pages」「Numbers」など、プリインストールのアプリで結構使われてたりします。完成形を先にお見せします。 環境macOS Ventura 13.2.1シミュレータ: iPhone 14 Pro(iOS16.2)Xcode14.2Swift5.7.2 Storyboardを開くまずはStoryboardを開いて、ViewControllerを選択した状態で「Editor」→「Embed In」→「Navigation Controller」を押す、というお決まりの流れでナビゲーションバーを出します。Storyboardはもうこれ以上使わないので、ViewController.swiftに戻ります。さようなら、Storyboard君。 ポップオーバーを表示とりあえず、ポップオーバーを表示するボタンを作ってみます。 先ほどナビゲーションバーを用意したのは、UIBarButtonItemを置くためです。ポップオーバーはUIButtonから出るよりUIBarButtonItemから出た方が見慣れてる感じがしたので。まあどっちでも良いですけど。 ということで、できたのがこちらです。import UIKit class ViewController: UIViewController, UIPopoverPresentationControllerDelegate { override func viewDidLoad() { super.viewDidLoad() navigationItem.rightBarButtonItem = UIBarButtonItem( barButtonSystemItem: .add, target: self, action: #selector(didTapBarButton) ) } @objc func didTapBarButton(_ sender: UIBarButtonItem) { let vc = PopoverViewController() vc.preferredContentSize = CGSize(width: 300, height: 240) vc.modalPresentationStyle = .popover let presentationController = vc.popoverPresentationController if let sourceView = sender.value(forKey: "view") as? UIView { presentationController?.sourceView = sourceView presentationController?.sourceRect = sourceView.bounds } presentationController?.permittedArrowDirections = .up presentationController?.delegate = self present(vc, animated: true) } func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { return .none } } class PopoverViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } }「+」ボタンを押したらdidTapButtonが呼び出されて、PopoverViewControllerをポップアップで表示します。 ポップアップを出すコードは長いですが、やってることはシンプルです。ポップオーバーのサイズを設定して、ビューのプレゼンテーションスタイルをポップオーバーに設定して、ポップオーバーの吹き出し口を向ける場所と、向きを設定して、最後にpresentで表示します。 記事の冒頭で話したように、本来ポップオーバーはiPhoneではモーダルとして表示されるのですが、presentationControllerのdelegateをselfにしてadaptivePresentationStyle関数で.noneを返すことで、iPhoneでもポップオーバーとして表示されます。 ナビゲーションバーを表示さて、個人的にここが一番の難関でした。 まず、プッシュ遷移やモーダル遷移を行うにはナビゲーションバーが必須です。 プッシュ遷移を行うコードはこんな感じになっていて、navigationController?.pushViewController(vc, animated: true)navigationControllerがnilだったら、プッシュ遷移は不可能なわけです。 だから、PopoverViewControllerにナビゲーションバーを追加する方法を考えねばなりません。 ナビゲーションバーといえばStoryboardで出すものという認識だったので、Storyboardを弄ってPopoverViewControllerにナビゲーションバーをEmbed Inしてみるも、反映されず……。 PopoverViewControllerのviewDidLoadの中で、navigationControllerに新しく生成したUINavigationControllerを代入できないかと考えるも、navigationControllerはゲッターしか定義されていないので代入不可……。 ダメ元でChatGPT君に助けを求めるも、UIViewControllerの、実在しないプロパティを教えられ……w いや、どうすんのよこれ。 となったところで、一旦考えるのをやめてナビゲーションバーについて情報収集することにしました。 すると、モーダルの画面にナビゲーションバーを追加している動画(リンク)が見つかりました。 動画ではこのようにしてモーダル遷移することで、それを可能にしていました。 let rootVC = SecondViewController() let navVC = UINavigationController(rootViewController: rootVC) navVC.modalPresentationStyle = .fullScreen present(navVC, animated: true)なるほど、rootVCではなくnavVCの方をpresentするのか。興味深い。 ということで、こちらも同じやり方でやってみます。 まずはdidTapButton関数の中身を変えていきます。 let vc = PopoverViewController() vc.preferredContentSize = CGSize(width: 300, height: 240) let navVC = UINavigationController(rootViewController: vc) navVC.modalPresentationStyle = .popover let presentationController = navVC.popoverPresentationController if let sourceView = sender.value(forKey: "view") as? UIView { presentationController?.sourceView = sourceView presentationController?.sourceRect = sourceView.bounds } presentationController?.permittedArrowDirections = .up presentationController?.delegate = self present(navVC, animated: true)navVCを宣言して、vcのところをいくつかnavVCに変えただけです。 PopoverViewControllerのviewDidLoadに以下を追加して、ナビゲーションバーに「画面1」と表示します。 title = "画面1"実行してみます。 ちゃんとタイトルが表示されました!これで問題は解決です。 遷移ボタンを追加それでは、PopoverViewControllerにボタンを追加しましょう。 ボタンを宣言して、 private let button: UIButton = { let button = UIButton() button.setTitle("遷移する", for: .normal) button.backgroundColor = .systemRed button.setTitleColor(.white, for: .normal) return button }()ボタンを配置するコードをViewDidLoadに書きます。 button.frame = CGRect(x: 100, y: 100, width: preferredContentSize.width - 200, height: 40) view.addSubview(button)ボタンのイベントを登録して、 button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)何もないただのUIViewControllerに遷移するコードを書きます。 @objc func didTapButton() { let vc = UIViewController() vc.view.backgroundColor = .white vc.title = "画面2" navigationController?.pushViewController(vc, animated: true) }ビューの背景色を設定しないと遷移の時にアニメーションがバグったので、白に設定しています。 これでようやく完成です。対戦ありがとうございました。 最終的なコードの全文import UIKit class ViewController: UIViewController, UIPopoverPresentationControllerDelegate { override func viewDidLoad() { super.viewDidLoad() navigationItem.rightBarButtonItem = UIBarButtonItem( barButtonSystemItem: .add, target: self, action: #selector(didTapBarButton) ) } @objc func didTapBarButton(_ sender: UIBarButtonItem) { let vc = PopoverViewController() vc.preferredContentSize = CGSize(width: 300, height: 240) let navVC = UINavigationController(rootViewController: vc) navVC.modalPresentationStyle = .popover let presentationController = navVC.popoverPresentationController if let sourceView = sender.value(forKey: "view") as? UIView { presentationController?.sourceView = sourceView presentationController?.sourceRect = sourceView.bounds } presentationController?.permittedArrowDirections = .up presentationController?.delegate = self present(navVC, animated: true) } func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { return .none } } class PopoverViewController: UIViewController { private let button: UIButton = { let button = UIButton() button.setTitle("遷移する", for: .normal) button.backgroundColor = .systemRed button.setTitleColor(.white, for: .normal) return button }() override func viewDidLoad() { super.viewDidLoad() title = "画面1" button.frame = CGRect(x: 100, y: 100, width: preferredContentSize.width - 200, height: 40) view.addSubview(button) button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) } @objc func didTapButton() { let vc = UIViewController() vc.view.backgroundColor = .white vc.title = "画面2" navigationController?.pushViewController(vc, animated: true) } }
内容霽月です。Swiftちょっと分かる人向け(≠チョットデキル)の記事です。 Appleのメモ帳アプリなどでは、改行しまくってキーボードに迫ったとしても、キーボードの下にカーソルが隠れることなく、自動的にスクロールされることで、カーソルが常に見える位置に存在するようになっていますよね。 その機能を再現する方法についてネットで少し調べてみましたが、あまり良い解決策は見つかりませんでした。ということで、この記事では、単純で分かりやすくその機能を実現する方法について紹介します。 環境macOS Ventura 13.2.1シミュレータ: iPhone 14 Pro(iOS16.2)Xcode14.2Swift5.7.2 UITextViewを配置まずはUITextViewを画面に配置します。import UIKit class ViewController: UIViewController { private let textView = UITextView() override func viewDidLoad() { super.viewDidLoad() view.addSubview(textView) textView.frame = view.bounds textView.text = String(repeating: "これは UITextView を使ったテストです。\n", count: 100) } }実行すると適当な文章が100行表示されます。 キーボードの高さを取得次に、キーボードが開かれた時に、キーボードの高さを取得するようにします。 保存先の変数を作って、 private var keyboardHeight: Double = 0.0以下をviewDidLoadの中に書いて、キーボードが表示された時にメソッドが呼び出されるように登録し、 NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)キーボードの高さを取得する処理を書きます。 @objc func keyboardWillShow(_ notification: Notification) { guard let userInfo = notification.userInfo else { return } guard let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } keyboardHeight = keyboardFrame.height }実行してエラーがないことを確認して、次の段階に進みます。 textViewDidChange関数textViewに変更が加えられた時に呼び出される関数を作ります。 textViewのdelegateをselfにしておいて、 textView.delegate = selftextViewDidChange関数の中に書いていきます。extension ViewController: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { } } 目的の機能を作成必要な情報は「現在のスクロール具合」「カーソルがある矩形領域」「キーボードが開いてても見える矩形領域」です。この3つの情報を取得します。 var contentOffset = textView.contentOffset let cursorPosition = textView.selectedTextRange?.start ?? textView.beginningOfDocument var cursorRect = textView.caretRect(for: cursorPosition) let visibleRect = CGRect(x: 0, y: 0, width: textView.bounds.width, height: textView.bounds.height - keyboardHeight) cursorRect.origin.y -= contentOffset.y順に「contentOffset」「cursorRect」「visibleRect」という名前で取得しました。 カーソルの下端がvisibleRectの下端より下に行かないようにします。 if cursorRect.maxY > visibleRect.maxY { contentOffset.y += cursorRect.maxY - visibleRect.maxY }最後にcontentOffsetの変更を保存して終わりです。 textView.setContentOffset(contentOffset, animated: true) 実行して確認問題なく実行できました。カーソルが見えない場所に行ってしまう現象は解決され、とても便利なUITextViewの完成です! と、思いきや? textVIewのコンテンツの下端に余白がないために、下の方を編集しようとすると物凄くやりにくいことになってしまいました。 キーボードが開いている時は下端にキーボードの高さ分の余白を開けるようにしてみましょう。 keyboardWillShowに以下を追加します。 textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0) textViewDidChange(textView)キーボードが閉じた時の通知も登録して、 NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)キーボードが閉じた時に余白を元に戻す処理を書きます。 @objc func keyboardWillHide(_ notification: Notification) { textView.textContainerInset = UIEdgeInsets.zero }これで完璧です。 最終的なコードの全文import UIKit class ViewController: UIViewController { private let textView = UITextView() private var keyboardHeight: Double = 0.0 override func viewDidLoad() { super.viewDidLoad() view.addSubview(textView) textView.frame = view.bounds textView.delegate = self textView.text = String(repeating: "これは UITextView を使ったテストです。\n", count: 100) NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) } @objc func keyboardWillShow(_ notification: Notification) { guard let userInfo = notification.userInfo else { return } guard let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } keyboardHeight = keyboardFrame.height textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0) textViewDidChange(textView) } @objc func keyboardWillHide(_ notification: Notification) { textView.textContainerInset = UIEdgeInsets.zero } } extension ViewController: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { var contentOffset = textView.contentOffset let cursorPosition = textView.selectedTextRange?.start ?? textView.beginningOfDocument var cursorRect = textView.caretRect(for: cursorPosition) let visibleRect = CGRect(x: 0, y: 0, width: textView.bounds.width, height: textView.bounds.height - keyboardHeight) cursorRect.origin.y -= contentOffset.y if cursorRect.maxY > visibleRect.maxY { contentOffset.y += cursorRect.maxY - visibleRect.maxY } textView.setContentOffset(contentOffset, animated: true) } }