- 投稿日:2020-10-12T19:47:54+09:00
UITableViewのスクロール位置を戻したい!
はじめに
UITableView
でreloadData()
をしたときにトップへスクロールしたいときがたまにあります。でもこれが結構めんどくさい。。。
色々方法を試したので備忘録として記載します。やりたいことは「ボタン押下したときにリロード+スクロール位置をトップに戻したい」ということ!
こんな感じ(before が最下部表示で after が before の状態からリロード+トップへスクロールした状態です)
before after テーブルは前回の記事で書いたヘッダーが消えたり出たりする
grouped
スタイルのテーブルでやります!
(before はヘッダー非表示、after はヘッダー表示です)方法
テーブルをトップにスクロールする方法として
setContetnOffset
とscrollToRow
があり、reloadData
の前にスクロールさせるのか後にスクロールさせるのかで4パターンできると思います。実行結果1
実装はこんな感じ
// パターン1(setContetnOffset先スクロール) tableView.setContentOffset(.zero, animated: false) tableView.reloadData() // パターン2(scrollToRow先スクロール) tableView.scrollToRow(at: .init(row: 0, section: 0), at: .top, animated: false) tableView.reloadData() // パターン3(setContetnOffset後スクロール) tableView.reloadData() tableView.setContentOffset(.zero, animated: false) // パターン4(scrollToRow後スクロール) tableView.reloadData() tableView.scrollToRow(at: .init(row: 0, section: 0), at: .top, animated: false)結果はこんな感じ
パターン1 パターン2 パターン3 パターン4 パターン1とパターン4(でも row 設定なのでヘッダーまではスクロールしてない)はいけてそうだけどパターン2、3が中途半端な位置になっている。。。
実行結果2
なんかわからんけどとりあえずコンテンツサイズが確定してない=レイアウトが中途半端な状態だから上のような結果になるんだろうと思い
layoutIfNeeded()
呼べばいいんじゃね?ということで下記のように実装してみた。// パターン1(setContetnOffset先スクロール) tableView.setContentOffset(.zero, animated: false) tableView.layoutIfNeeded() tableView.reloadData() // パターン2(scrollToRow先スクロール) tableView.scrollToRow(at: .init(row: 0, section: 0), at: .top, animated: false) tableView.layoutIfNeeded() tableView.reloadData() // パターン3(setContetnOffset後スクロール) tableView.reloadData() tableView.layoutIfNeeded() tableView.setContentOffset(.zero, animated: false) // パターン4(scrollToRow後スクロール) tableView.reloadData() tableView.layoutIfNeeded() tableView.scrollToRow(at: .init(row: 0, section: 0), at: .top, animated: false)結果はこんな感じ
パターン1 パターン2 パターン3 パターン4 全パターンいい感じにいけてそう
実行結果3
layoutIfNeeded()
を呼ぶことでそれぞれ思った通りの動作になってますがあまいです!セルの高さが固定なら問題ないのですがセルの高さが可変の場合はちょっと。。。下記のような2パターンのセルを用意します。
実行結果2の処理を試してみると結果はこんな感じ(ちなみに
layoutIfNeeded
ないパターンだとパターン4以外は中途半端なスクロールになりました)
パターン1 パターン2 パターン3 パターン4 パターン3以外はいけてそうですおそらく
tableView(_ :estimatedHeightForRowAt:)
で適切な値を返してやるとパターン3でもうまくいくと思います。2パターンとかならいいですがここにオートレイアウトとか関与してくると色々高さ計算がめんどくさくなります。。。セルの高さに関しては下記の記事に色々丁寧に記載されていたので参考に(私はあんまりわかってない。。。)
UITableViewのrowHeightやestimatedRowHeightに何を設定すると良いのか結論
今回は計算とかいろいろめんどくさいのとヘッダーまでスクロールしたかったので下記を採用しました。(たぶんいけてそう)
// パターン1(setContetnOffset先スクロール) tableView.setContentOffset(.zero, animated: false) tableView.layoutIfNeeded() tableView.reloadData()動作はこんな感じ
おわりに
reloadData
とトップへスクロールの組み合わせはわりとやることあるんですが結局どういう方法がいいのかな?といつも悩んでしまいます。。。下記のようなやり方もあるみたいです。
iOS TableView reload and scroll top
他なにかいい方法ご存知であればぜひご教授ください
- 投稿日:2020-10-12T19:33:49+09:00
guard文について理解しよう!
今回は、guard文について学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承ください。guard文とは?
guard文を一言でいうと、条件不成立のときに早期退出を行なうための条件分岐文。
基本的な書き方は以下の通りです。qiita.rbvarguard 条件式 else{ 条件式がfalseの場合に実行される文 guard文が記述されているスコープ外に退出する必要がある (つまり、returnを記述する必要がある) }※{}←スコープ
では、基本的な例を見ていきましょう!qiita.rbvarでは、基本的な例を見ていきましょう! func someFunction(){ let value = 99 guard value >= 100 else{ print("100未満の値です")//値が100未満だったので実行される return } someFunction() 実行結果:100未満の値ですguard文で宣言された変数や定数へのアクセス
guard文は、if文と同様にguard-let文が利用できます。
if文を復習したい人は下記のURLをチェック!!
if文とは?("https://qiita.com/syunta061689/items/65d54a58936a5849a67a")if-let文との違いは、guard-let文で宣言された変数や定数はguard-let文以降でも利用可能ということです。
次の例ではguard-let文で宣言した定数intにアクセスしています。
qiita.rbvarfunc someFunction(){ let a: Any = 1 //Any型 guard let int= a as? Int //aをInt型にダウンキャストできますか? else{//そうでないなら以下を実行してください print("aはInt型ではありません") return } print("値はInt型の\(int)です")//intはguard文以降でも使用可能! } someFunction() 実行結果:値はInt型の1です。if文との使い分け
では、今度は具体的な例を用いて、if文との使い分けを深堀りしていきましょう.
次の例では、if文とguard文で、2つのInt型を受け取り、両方の値を持っていればその和を返し、どちらかが値を持っていなければnilを返すという処理をしていきます。if文の例
qiita.rbvarfunc add(_ optionalA: Int?,_ optionalB: Int?)-> Int?{ let a: Int if let unwrappedA = optionalA{ a = unwrappedA }else{ print("第一引数に値が入っていません") return nil } let b: Int if let unwrappedB = optionalB{ b = unwrappedB }else{ print("第引数に値が入っていません") return nil } return a+b } add(optional(3)optional(2))//5guard文の例
qiita.rbvarfunc add(_ optionalA: Int?, _ optionalB: Int?)-> Int?{ guard let a = optionalA else{ print("第1引数に値が入っていません") return nil } guard let b = optionalB else{ print("第2引数に値が入っていません") return nil } return a+b } add(Optional(3),Optional(2))//5このように、条件に応じて早期退出するコードは、 guard文を使用して実装した方がシンプルとなります
また、guard文では退出処理を書き忘れた場合にエラーになるため、単純ミスを未然に防げます!
- 投稿日:2020-10-12T17:04:12+09:00
iOS14でタブバーが消えたまま戻らなくなる現象に遭遇した
Overview
画面遷移の度に
hidesBottomBarWhenPushed = true
するとpopToRootViewController()
した時にtabBarが戻らなくなりました。iOS13では発生しなかったため、iOS14のバグと思われます。
※ Developer Forumsにも上がっていました雑に再現コード
長いので折りたたみ
FirstViewController.swiftfinal class FirstViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() title = "First" } } extension FirstViewController { @IBAction func next(_ sender: Any?) { guard let vc = storyboard?.instantiateViewController(identifier: "second") else { return } vc.hidesBottomBarWhenPushed = true navigationController?.pushViewController(vc, animated: true) } }SecondViewController.swiftfinal class SecondViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() title = "Second" } } extension SecondViewController { @IBAction func next(_ sender: Any?) { guard let vc = storyboard?.instantiateViewController(identifier: "last") else { return } vc.hidesBottomBarWhenPushed = true navigationController?.pushViewController(vc, animated: true) } }LastViewController.swiftfinal class LastViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() title = "Last" } } extension LastViewController { @IBAction func popToRoot(_ sender: Any?) { navigationController?.popToRootViewController(animated: true) } }対応策
↑のコードの場合であれば
SecondViewController
のvc.hidesBottomBarWhenPushed = true
を消してしまえばLastViewController
からのpopToRootViewController
でタブバーが戻ってくるようになります。
ただし、複雑な画面遷移だと思わぬ副作用が発生するリスクもあるので、バグ修正されるのを待った方がいいのかも?
- 投稿日:2020-10-12T16:52:20+09:00
【フロント・ネイティブエンジニア必見】マイクロインタラクションの参考になるサイト7選
マイクロインタラクションの参考になる情報サイトまとめ
UX改善に欠かせないマイクロインタラクション
フロントエンド・ネイティブアプリエンジニアはこのような細部へのこだわりにワクワクすること間違いなし!
どんな実装があるのか、参考になる情報をまとめます。まずは書籍紹介
これは結構読むのが大変でした!
書籍をいきなり買うのはちょっと。。。という方は以下のサイトを厳選しましたので、さらっとみてからの購入をお勧めしますサイト紹介
マイクロインタラクションを感覚的に理解できるサイト
マイクロインタラクションとは?注意すべきポイントや事例を徹底解説! | Web Design Trends
マイクロインタラクションを考慮すべき4つの理由 | UX MILK
UIにマイクロインタラクションを! より良いUXのための7つの秘訣 | UX MILK
より良いUIのためのマイクロインタラクション入門|Blog|Goodpatch グッドパッチ
小さな動きで大きな効果!マイクロインタラクションを使って優れたUXを実現する | Web Design Trends
これらのサイトをざっと眺めれば、15分程度でどのようなものがマイクロインタラクションかわかります。
見てるだけで楽しいのでぜひ眺めてみましょう!効果検証はどうするのか!
マイクロインタラクションの効果を数値化する|鈴木慎吾 / TSUMIKI INC.
ただ綺麗で楽しいだけじゃビジネス上意味はありません。
数字で効果を把握して、ビジネスに貢献するにはどうすればいいのか参考になります!デザイナーはどうやってつくるのか!
マイクロインタラクションを活かしたコンセプトデザインの作成 @Adobe MAX Japan 2019|鈴木慎吾 / TSUMIKI INC.
デザイナーの方がどのように設計するのか、動画で多くのサンプルが見れます。
工程を把握することでデザイナーの方とのコミュニケーションがスムーズになること間違いなし!最後に
読んでくださったみなさんが感動したUX・UIもぜひコメントで紹介してください!
よろしくおねがいします!!
- 投稿日:2020-10-12T15:36:08+09:00
iOS safariのinputイベント+focus()の謎挙動について
下のような文字を一文字を打つと自動で次の入力値に行くようなjqueryをかきました
※実際のものより色々省いたので下記で動くかは未確認<html> <input id="number1" type="number" name="number1" value="" > <input id="number2" type="number" name="number2" value="" > <input id="number3" type="number" name="number3" value="" > <input id="number4" type="number" name="number4" value=""> </html> <script type="text/javascript"> $(function(){ $('input[type=number]').on('input', function(ev){ var $me = $(this); var $list = $('input[type=number]'); $list.each(function(index){ if ($(this).is($me)) { $list.eq(index+1).focus(); ev.preventDefault(); } }); }); }); </script>PC系ブラウザでは問題なかったですがiPhoneのsafariでは下記のようなよくわからない現象がおきました
number1に1と入力
↓
何故かnumber1とnumber2に1と入力されnumber3にフォーカスが移動
あれこれ調べてみたものの解決策らしきものは見当たらず
safariではinputイベントの発火のタイミングが早すぎて(遅すぎて?)focusの直後にも発火しているのでは?
と思い下記のようにすることで一応想定した動きを得ることはできましたsetTimeout( function(){ $list.eq(index+1).focus(); } ,100)
- 投稿日:2020-10-12T12:42:37+09:00
[Swift] アプリのサポートがiOS11以降であればIntとInt64を使い分ける必要はなかった話
私は今までなんとなく、「値がデカくなる可能性がある整数にはInt64、そうでもない整数はInt」で変数宣言するように気を配ってきましたが、実行環境次第ではそんな考慮は不要、という話です。
本記事の前提環境:
・iOS 11以降
・Swift 5冒頭のような考慮をしてきた背景としては、
Intの最大値・最小値は、『実行環境が32bitか64bitによって違う』という特性があって、32bitだと範囲が狭いためです。
型 最大値 最小値 Int 2147483647
9223372036854775807-2147483648
-9223372036854775808
(実行環境が32bitか64bitによって違う)Int64 9223372036854775807 -9223372036854775808 なので32bit環境のIntにおいては、例えば扱う数値が金額(円)であれば21億4千7百万ウン円が最大値なので、セレブの場合はアプリがクラッシュする恐れがあります。
ですが、よくよく考えたら私が今担当しているアプリはiOS 11以降のiPhoneでしか使えません。
調べたところ、iOS 11以降では32bitの実行環境はないようです。
参考:iOS端末 画面サイズ・対応OS早見表(iOS7〜12)
ってことは、iOS 11以降のみサポートのアプリであれば、IntとInt64を使い分ける意味はないので、冒頭のような考慮は不要でした。
- 投稿日:2020-10-12T12:41:55+09:00
iOSアプリ開発のスキルロードマップ(紹介)
Developer Roadmapsという有名なサイトがありまして、Webフロントエンド、バックエンド、Androidなどのスキルロードマップを図として提案してくれています。
しかし、iOS版はないのですね。。。
ってことで探してみたところ、「Reddit」というアメリカの投稿SNSに割と良さげなモノが投稿されていました。
引用:2018 Roadmap to iOS Development
数年前に書かれたモノであるためか、
「Objective-Cは必須にしなくても良いのでは…」など若干ツッコミたいところもありますが、、、それ以外は、
「黄色背景のスキルを学習していくと、iOSエンジニアとして及第点に到達するよ」
という観点で、概ね同意できるかなと思いました。ところで、最後のApp Storeの項にPick any(何かを選択)の区分で「Lucky Reviewer」という項目があるのは、Appleに対する皮肉ですかね(笑
- 投稿日:2020-10-12T03:16:40+09:00
macにケーブル接続したiPhoneの画面をリアルタイム取得する
macにケーブル接続したiPhoneの画面をリアルタイム取得するミニマム実装を作りました。
これまではQuickTime Playerを起動し「新規ムービー収録」からiPhoneを選択するなどのアプリ外での取り回しが必要だったのですが、これで自作プログラムで実現可能となります。
GitHubにアップしています。
https://github.com/satoshi0212/DeviceCameraMonitorSampleこの実装含め、仮想カメラ/AR/映像表現などの情報更新はTwitterで投稿しています。
https://twitter.com/shmdevelop実装ポイント
プロジェクト設定
「Hardware」「Camera」 の選択が必要。
plist
plistに
Privacy - Camera Usage Description
を追加してください。Device探索時設定
AVCaptureDevice.DiscoverySession
実行前に以下を指定することでオプトインで外部デバイスが表示されるようになります。var prop = CMIOObjectPropertyAddress( mSelector: CMIOObjectPropertySelector(kCMIOHardwarePropertyAllowScreenCaptureDevices), mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeGlobal), mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementMaster)) var allow: UInt32 = 1; CMIOObjectSetPropertyData(CMIOObjectID(kCMIOObjectSystemObject), &prop, 0, nil, UInt32(MemoryLayout.size(ofValue: allow)), &allow)そして以下のパラメータで探索するとdevicesにiPhoneが含まれています。
見つかったdevicesをmodelID
、manufacturer
で適宜フィルタするとiPhoneデバイスが特定できます。let devices = AVCaptureDevice.DiscoverySession(deviceTypes: [.externalUnknown], mediaType: nil, position: .unspecified).devices if let device = devices.filter({ $0.modelID == "iOS Device" && $0.manufacturer == "Apple Inc." }).first { ... }ただし起動直後や探索直後はiPhoneが見つからない場合があるため
AVCaptureDeviceWasConnectedNotification
のnotificationをobserveする必要もありました。let nc = NotificationCenter.default nc.addObserver(forName: NSNotification.Name(rawValue: "AVCaptureDeviceWasConnectedNotification"), object: nil, queue: .main) { (notification) in print(notification) guard let device = notification.object as? AVCaptureDevice else { return } ... }余談: 表示用リサイズ
アップした実装では画面表示用にリサイズしました。
高さを固定値として比率を計算し幅を算出しimageViewのサイズ指定。
画像の方がCGAffineTransform
でサイズ変換しています。private func resizeIfNeeded(w: CGFloat, h: CGFloat) { guard targetRect == nil else { return } let aspect = h / fixedHeight let rect = CGRect(x: 0, y: 0, width: floor(w / aspect), height: fixedHeight) imageView.frame = rect targetRect = rect } ... func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { connection.videoOrientation = .portrait DispatchQueue.main.async(execute: { let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)! let ciImage = CIImage(cvPixelBuffer: pixelBuffer) let w = CGFloat(CVPixelBufferGetWidth(pixelBuffer)) let h = CGFloat(CVPixelBufferGetHeight(pixelBuffer)) self.resizeIfNeeded(w: w, h: h) guard let targetRect = self.targetRect else { return } let m = CGAffineTransform(scaleX: targetRect.width / w, y: targetRect.height / h) let resizedImage = ciImage.transformed(by: m) let cgimage = self.context.createCGImage(resizedImage, from: targetRect)! let image = NSImage(cgImage: cgimage, size: targetRect.size) self.imageView.image = image }) }