20201012のiOSに関する記事は8件です。

UITableViewのスクロール位置を戻したい!

Xcode-12.0 iOS-14.0 Swift-5.3

はじめに

UITableViewreloadData() をしたときにトップへスクロールしたいときがたまにあります。でもこれが結構めんどくさい。。。

色々方法を試したので備忘録として記載します。

やりたいことは「ボタン押下したときにリロード+スクロール位置をトップに戻したい」ということ!

こんな感じ(before が最下部表示で after が before の状態からリロード+トップへスクロールした状態です)

before after
before after

テーブルは前回の記事で書いたヘッダーが消えたり出たりする grouped スタイルのテーブルでやります!
(before はヘッダー非表示、after はヘッダー表示です)

方法

テーブルをトップにスクロールする方法として setContetnOffsetscrollToRow があり、 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 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
l_1 l_2 l_3 l_4

全パターンいい感じにいけてそう:tada:

実行結果3

layoutIfNeeded() を呼ぶことでそれぞれ思った通りの動作になってますがあまいです!セルの高さが固定なら問題ないのですがセルの高さが可変の場合はちょっと。。。:confounded:

下記のような2パターンのセルを用意します。

cell

実行結果2の処理を試してみると結果はこんな感じ(ちなみに layoutIfNeeded ないパターンだとパターン4以外は中途半端なスクロールになりました)

パターン1 パターン2 パターン3 パターン4
b_l_1 b_l_2 b_l_3 b_l_4

パターン3以外はいけてそうです:clap:おそらく tableView(_ :estimatedHeightForRowAt:) で適切な値を返してやるとパターン3でもうまくいくと思います。2パターンとかならいいですがここにオートレイアウトとか関与してくると色々高さ計算がめんどくさくなります。。。

セルの高さに関しては下記の記事に色々丁寧に記載されていたので参考に(私はあんまりわかってない。。。)

UITableViewのrowHeightやestimatedRowHeightに何を設定すると良いのか

結論

今回は計算とかいろいろめんどくさいのとヘッダーまでスクロールしたかったので下記を採用しました。(たぶんいけてそう:v:

// パターン1(setContetnOffset先スクロール)
tableView.setContentOffset(.zero, animated: false)
tableView.layoutIfNeeded()
tableView.reloadData()

動作はこんな感じ

scroll

おわりに

reloadData とトップへスクロールの組み合わせはわりとやることあるんですが結局どういう方法がいいのかな?といつも悩んでしまいます。。。

下記のようなやり方もあるみたいです。

iOS TableView reload and scroll top

他なにかいい方法ご存知であればぜひご教授ください:raised_hands:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

guard文について理解しよう!

今回は、guard文について学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承ください。

guard文とは?

guard文を一言でいうと、条件不成立のときに早期退出を行なうための条件分岐文。
基本的な書き方は以下の通りです。

qiita.rbvar
guard 条件式 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.rbvar
func 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.rbvar
func 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))//5

guard文の例

qiita.rbvar
func 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文では退出処理を書き忘れた場合にエラーになるため、単純ミスを未然に防げます!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS14でタブバーが消えたまま戻らなくなる現象に遭遇した

Overview

画面遷移の度に hidesBottomBarWhenPushed = true すると popToRootViewController() した時にtabBarが戻らなくなりました。

iOS13では発生しなかったため、iOS14のバグと思われます。
Developer Forumsにも上がっていました

雑に再現コード

長いので折りたたみ
FirstViewController.swift
final 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.swift
final 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.swift
final class LastViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Last"
    }
}

extension LastViewController {
    @IBAction func popToRoot(_ sender: Any?) {
        navigationController?.popToRootViewController(animated: true)
    }
}

対応策

↑のコードの場合であればSecondViewControllervc.hidesBottomBarWhenPushed = trueを消してしまえばLastViewControllerからのpopToRootViewControllerでタブバーが戻ってくるようになります。
ただし、複雑な画面遷移だと思わぬ副作用が発生するリスクもあるので、バグ修正されるのを待った方がいいのかも?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【フロント・ネイティブエンジニア必見】マイクロインタラクションの参考になるサイト7選

マイクロインタラクションの参考になる情報サイトまとめ

UX改善に欠かせないマイクロインタラクション
フロントエンド・ネイティブアプリエンジニアはこのような細部へのこだわりにワクワクすること間違いなし!
どんな実装があるのか、参考になる情報をまとめます。

まずは書籍紹介

picture978-4-87311-659-4.gif

O'Reilly Japan - マイクロインタラクション

これは結構読むのが大変でした!
書籍をいきなり買うのはちょっと。。。という方は以下のサイトを厳選しましたので、さらっとみてからの購入をお勧めします

サイト紹介

マイクロインタラクションを感覚的に理解できるサイト

マイクロインタラクションとは?注意すべきポイントや事例を徹底解説! | 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もぜひコメントで紹介してください!
よろしくおねがいします!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift] アプリのサポートがiOS11以降であればIntとInt64を使い分ける必要はなかった話

私は今までなんとなく、「値がデカくなる可能性がある整数にはInt64、そうでもない整数はInt」で変数宣言するように気を配ってきましたが、実行環境次第ではそんな考慮は不要、という話です。

本記事の前提環境:
・iOS 11以降
・Swift 5

冒頭のような考慮をしてきた背景としては、
Intの最大値・最小値は、『実行環境が32bitか64bitによって違う』という特性があって、32bitだと範囲が狭いためです。

最大値 最小値
Int 2147483647
9223372036854775807
-2147483648
-9223372036854775808
(実行環境が32bitか64bitによって違う)
Int64 9223372036854775807 -9223372036854775808

引用:意外と知られていないSwift数値型の細かい仕様

なので32bit環境のIntにおいては、例えば扱う数値が金額(円)であれば21億4千7百万ウン円が最大値なので、セレブの場合はアプリがクラッシュする恐れがあります。

ですが、よくよく考えたら私が今担当しているアプリはiOS 11以降のiPhoneでしか使えません。

調べたところ、iOS 11以降では32bitの実行環境はないようです。

参考:iOS端末 画面サイズ・対応OS早見表(iOS7〜12)

ってことは、iOS 11以降のみサポートのアプリであれば、IntとInt64を使い分ける意味はないので、冒頭のような考慮は不要でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリ開発のスキルロードマップ(紹介)

Developer Roadmapsという有名なサイトがありまして、Webフロントエンド、バックエンド、Androidなどのスキルロードマップを図として提案してくれています。

しかし、iOS版はないのですね。。。

ってことで探してみたところ、「Reddit」というアメリカの投稿SNSに割と良さげなモノが投稿されていました。

引用:2018 Roadmap to iOS Development

ix44k24k9ik01.png

数年前に書かれたモノであるためか、
「Objective-Cは必須にしなくても良いのでは…」など若干ツッコミたいところもありますが、、、

それ以外は、

「黄色背景のスキルを学習していくと、iOSエンジニアとして及第点に到達するよ」
という観点で、概ね同意できるかなと思いました。

ところで、最後のApp Storeの項にPick any(何かを選択)の区分で「Lucky Reviewer」という項目があるのは、Appleに対する皮肉ですかね(笑

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

macにケーブル接続したiPhoneの画面をリアルタイム取得する

macにケーブル接続したiPhoneの画面をリアルタイム取得するミニマム実装を作りました。

これまではQuickTime Playerを起動し「新規ムービー収録」からiPhoneを選択するなどのアプリ外での取り回しが必要だったのですが、これで自作プログラムで実現可能となります。

output.gif

GitHubにアップしています。
https://github.com/satoshi0212/DeviceCameraMonitorSample

この実装含め、仮想カメラ/AR/映像表現などの情報更新はTwitterで投稿しています。
https://twitter.com/shmdevelop

実装ポイント

プロジェクト設定

「Hardware」「Camera」 の選択が必要。

スクリーンショット 2020-10-11 23.41.04.png

plist

plistに Privacy - Camera Usage Description を追加してください。

スクリーンショット 2020-10-12 2.26.56.png

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を modelIDmanufacturer で適宜フィルタすると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
        })
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む