20200205のSwiftに関する記事は10件です。

UI ImageViewがわからない

小銭を減らすアプリを作っている途中です。

現在の所持金を設定する画面でそれぞれの紙幣、硬貨のイラストを設置したところ、Xcodeのストーリーボードではうまく設置できたのですがシュミレーターでは表示されませんでした。(以下画像:1枚目がストリーボード、2枚目がシュミレーター)
スクリーンショット 2020-02-05 23.14.39.png
スクリーンショット 2020-02-05 23.14.39.png

調べても出てこなかったので色々やってみました。
するとAdd New Constraintsでwidth:161,height:65にしていた設定を外すと画像は表示されるようになりました。

しかし、画像のサイズがかなり大きくなってしまいました。
(1万円のUIImageViewのwidthとheightの指定を外したシュミレーター)
スクリーンショット 2020-02-05 23.28.03.png

仕方ないのでコードで書いてサイズを指定しようと考えています。
もし、コードに書かないでできる方法があったら教えて欲しいです。

追記
調べたらこうなってました。
スクリーンショット 2020-02-05 23.35.38.png
反省してます。

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

#23 Viewの背景を画像にする方法

はじめに

個人のメモ程度の出来なのであまり参考にしないで下さい.

環境

Xcode:11.2.1
Swift:5.1.2
2019/11

part1

Assets.cassestsに画像をドロップする.

スクリーンショット 2020-02-05 午後10.39.49.png

part2

UIImageViewと検索する.

スクリーンショット 2020-02-05 午後10.40.32.png

part3

UIImageViewViewに全て覆うように置く.

スクリーンショット 2020-02-05 午後10.40.53.png

part4

UIImageViewに以下の4つの制約を付ける.

スクリーンショット 2020-02-05 午後10.41.08.png

part5

するとUIImageViewがズレるので,もう一度Viewに全て覆うようにする.

スクリーンショット 2020-02-05 午後10.41.28.png

part6

その状態で,Selected ViewsUpdate Constraint Constantsを選択する.

スクリーンショット 2020-02-05 午後10.41.37.png

part7

そのままUIImageViewを選択した状態で,Attributes inspectorImageから,先程追加した画像を選択する.

スクリーンショット 2020-02-05 午後10.41.59.png

part8

UIImageViewを選択した状態で,Attributes inspectorContent ModeScale To Fillを選択する.

スクリーンショット 2020-02-05 午後10.42.14.png

part9

ラベルやボタンよりも後にUIImageViewを配置すると,ラベルやボタンが隠れてしまうので,下の写真のように,UIImageViewSafe Areaの直下にドラッグ・ドロップする.

スクリーンショット 2020-02-05 午後11.04.53.png

スクリーンショット 2020-02-05 午後10.42.51.png

part10

以下のように,UIImageViewが再背面に移動している事を確認する.

スクリーンショット 2020-02-05 午後10.43.35.png

part11

Label Labelを選択して,Attributes inspectorShadowSystem Background Colorにする.

スクリーンショット 2020-02-05 午後10.44.04.png

part12

Label Labelを選択した状態で,Attributes inspectorShadow OffsetWidthHeight-1にする.

スクリーンショット 2020-02-05 午後10.44.38.png

part13

Button!を選択して,Attributes inspectorFontStyleBoldにする.

スクリーンショット 2020-02-05 午後10.45.07.png

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

RxSwiftでVoidをonNextした時、subscribeが処理されなかった

とある処理

RxSwiftを正しく使ってないとかそういうことは置いておいて、下記のような記述があった。

class HogeView: UIView {
    private subject: PublishSubject<Void> = PublishSubject()
    var observer: Observable<Void> { return subject }

    @IBOutlet func onTap(_ sender: UIButton) {
        subject.onNext(())
    }
}

class HogeViewController: UIViewController {
    @IBOutlet weak var hogeView: HogeView!

    override func viewDidLoad() {
        super.viewDidLoad()
        hogeView.observer.subscribe {
            // 処理
        }
    }
}

このhogeView.observer.subscribeの処理部分がボタンをタップしても動かず、なんでだろうと悩んだ。

結論

subscribeのevent引数を省略していたことで、今回の問題が起きていた。
event.elementはどうせVoidだしボタンのタップだけ検知できればいいから、eventいらないと記述を忘れていた結果、

public func subscribe(onCompleted: (() -> Void)? = nil, onError: ((Error) -> Void)? = nil) -> RxSwift.Disposable

が推論として呼ばれるようになっていた。

下記記述にしたことで、意図通り動いた。

class HogeView: UIView {
    private subject: PublishSubject<Void> = PublishSubject()
    var observer: Observable<Void> { return subject }

    @IBOutlet func onTap(_ sender: UIButton) {
        subject.onNext(())
    }
}

class HogeViewController: UIViewController {
    @IBOutlet weak var hogeView: HogeView!

    override func viewDidLoad() {
        super.viewDidLoad()
        hogeView.observer.subscribe { (_) in // event引数を省略しない
            // 処理
        }
    }
}

備考

debug オペレータを使ってデバッグしろとアドバイス貰って、onNextが正常に飛んできているのを確認できたので、subscribeに問題あることがわかり解決できた。

初歩的なミスだった。
備忘録がてら記述しておく。

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

[Swift] UserDefaults#synchronize()について

使うなって言ってるだろ!!!

synchronize()はLegacyで不要なメソッドのため使用しないよう求められています。
Appleのドキュメントを参照してください。

今すぐ消せ!

以上です

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

【Swift】Color指定の方法まとめ

SwiftにおけるColor Chooser

色の指定を行う方法としていくつか考えられるので,メモがわりに残しておきます.

開発環境

Swift 4
Xcode 11.1

UIColorのプロパティ指定

Swift初心者用の参考書などにもよく載っている方法がこちらです.

// 背景を赤色に変更
view.backgroundColor = UIColor.red
view.backgroundColor = .red

UIColorの後ろにredやblueなどを指定することでそれに対応する色を指定することができます.
UIColorを省略することもでき,その場合には2行目の書き方になります.

しかし,この書き方では,プロパティに設定されているデフォルトの色を指定することが基本になるため,開発者が意図するような色を指定できない場合もあります.

UIColorでRGB指定

やはり,開発者として嬉しいのはRGBで色を指定できることでしょう.
UIColorでも次のようにRGBを指定できます.

// RGBを指定
view.backgroundColor = UIColor(red: 0.1, green: 0.5, blue: 1.0, alpha: 1.0)

しかし,この方法では,RGBとalpha値は全てCGFloat型で指定しなければならず,色の指定が難しくなります.

そこで,次のように分数表記にし,分子に任意のRGB値を指定することで,RGBの設定値をわかりやすくすることができます.

// 分数表記にすることでRGBを指定
view.backgroundColor = UIColor(red: 30/255, green: 144/255, blue: 255/255, alpha: 1.0)

この指定の方が,メンテナンスなどもしやすいと思います.

Color Literal

Color Literalを使用することで,コードで色を指定する際にColor Pickerで指定することができます.
この指定方法はXcode8で追加されましたが,Xcode11でも使用することができます.

使い方としてはまず,カラー指定の際にColorと入力することでColor Literalがsuggestされるのでこちらを選択します.

image.png

すると,以下のように指定した色が可視化されるようになります.
(以下は白を指定している状態)

image.png

この白くなっている部分をダブルクリックすることでColor Pickerをコード内に表示することができます.
image.png

このまま色を指定することもできますが,Other...をクリックすると,見慣れたColor Pickerを開くことができます.

image.png

この方法が見やすいと思う人も,そうでない人もいると思います.
自分はRGBの方が扱いやすいと思ってしまいますが,Color Literalの方が子供とかは喜びそうだなとか思ったりもしました.

まとめ

今回は色の指定の仕方をまとめてみましたが,実際にはデザイナーから指定された画像をペタッと貼る場合も多いかと思いますのでどれほど参考になるかわかりませんが,Swiftでの色指定の際に少しでも力に慣れれば幸いです.

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

[Swift5]ニコニコ動画やLINEにあるようなスクロールによって閉じたり開いたりするヘッダー

はじめに

TableViewのスライドに同期して出てきたり閉じたりするヘッダーみたいなバーは、主に検索やビューの切り替えなどに使われ非常に便利なツールです。

簡単に実装できないかなぁとネットを探しまわったんですが、思い通りの情報が得られず結局自分で実装したので共有します。

少し複雑な内容になっていますが、初心者の人にも出来るだけ分かりやすく解説したいと思います。

意外と重宝しそうなツールほどネットに転がっていなかったりしますよね。
この記事が皆さんの助けになれば幸いです。

!初投稿かつSwift初心者なので至らない点があるかもしれません。
(この記事を気に入った人は"いいね"して頂けるとモチベーション上がります)

目次

  • 環境
  • 実行例
  • 考え方
    • 必要な値を取得
    • 適切なヘッダーの状態を判断する
    • ヘッダーの状態を元にヘッダーに対してスクロール処理をする
    • 必要な値を更新する
  • ソースコード
  • コードを紐解く
  • おわりに

環境

  • Xcode 11.2.1
  • Swift5

実行例

testheader.gif

考え方

はじめにTableViewのスクロールに連動して動くツールバーみたいなものをこの記事ではヘッダーとして呼びます。

基本的な考え方としては、TableViewにおけるスクロールの向きに基づいてヘッダーの座標を動かすことになります。

実際に行われる処理は以下の表のような流れになります。

順番 処理内容
1 必要な値を取得する
2 1で得た値をもとに適切なヘッダーの状態を判断する
3 2で得たヘッダーの状態をもとにヘッダーに対してスクロール処理を実行する
4 必要な値を更新する

この流れに沿って説明したいと思います。

また、今回説明するビュー(初期状態)を示した図を下に載せておきます。
ヘッダーはヘッダービューの一部であることが分かりますね。
余談ですが、ヘッダーの上に余白を大きく持たせているのはローディングの際にアニメーションを見せるためです。
(要望があればこちらも記事にします。)

ScrollViewの上の余白をヘッダーの高さ分設けていることに注意してください。

ThirdDiagram (11).png

1.必要な値を取得する

以下が実際に呼び出されるイベント関数となっています。
ここで私たちがこれから定義していく関数を呼び出すことになります。

func scrollViewDidScroll(_ scrollView: UIScrollView) {
        var subSet: [String:CGFloat] = ["up": 0, "down": 0]
        // MARK: -- 以下のinitialHeaderFrameに適切な値を入力するだけで、このプログラムは動作します。
        let initialHeaderFrame: [String:CGFloat] = ["Y": -260, "height": 60]
        let headerFrame: [String:CGFloat] = ["minY": initialHeaderFrame["Y"]! , "maxY": initialHeaderFrame["Y"]! + initialHeaderFrame["height"]!, "height": initialHeaderFrame["height"]!]

        subSet["up"]! += (lastContentOffset - scrollView.contentOffset.y)*0.1
        subSet["down"]! += (lastContentOffset - scrollView.contentOffset.y)*0.1

        let status = self.getHeaderViewStatus(&subSet, scrollView.contentOffset.y, myHeaderView.frame, lastContentOffset, headerFrame)
        self.scrolling(status: status)
        lastContentOffset = scrollView.contentOffset.y

    }

必要な値について述べたいと思います。

  • スクロールビューにおける現在のy座標 (scrollView.contentOffset.y)
  • スクロールビューにおける過去(更新前)のy座標 (lastContentOffset)
  • ヘッダービューのx, y, width, height情報 (myHeaderView.frame)
  • 過去と現在のスクロールビューにおけるy座標の差(スクロール量) (subset:[String:CGFloat])
  • ヘッダービューの初期座標と高さ (initialHeaderFrame: [String:CGFloat])

注意点としては、Swiftは左上が原点となっている座標系であることです。
コード上では過去と現在のスクロールビューにおけるy座標の差
(lastContentOffset - scrollView.contentOffset.y)
のように計算されます。
特筆すべき点としては、subset[String:CGFloat]には同じ値を"up""down"をキーにして二つ保存しています。
これは、上下のスクロールごとに値を用意するためです(理由は後述)。
少し冗長に感じますが便利です。

また、よく使う値としてヘッダービューのデフォルトy座標をヘッダーの高さ分下げたy座標があります。
そちらも、この関数内であらかじめ用意しておくことになります。
(コード内のheaderFrame["maxY"]のことです)

2.得た値をもとに適切なヘッダーの状態を判断する

まずヘッダーの状態について説明します。
今回はプログラムを分かりやすくするために5つの状態を用意しました。

状態 説明
start 初期状態、ビューのトップにヘッダーが存在している状態
move_up 下向きにスクロールしていて、ヘッダーが全て見えきっていない状態
stop_up 下向きにスクロールしていて、ヘッダーが全て見えきっている状態
move_down 上向きにスクロールしていて、ヘッダーが全て隠れきっていない状態
stop_down 上向きにスクロールしていて、ヘッダーが全て隠れきっている状態

これらは全て(関数)列挙型で表現されています。

enum headerViewStatus {
        case start(_ headerViewFrame: CGRect, _ initHeaderFrameMinY: CGFloat)
        case move_up(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect)
        case stop_up(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMaxY: CGFloat)
        case move_down(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect)
        case stop_down(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMinY: CGFloat)
}

さて、次にヘッダーの状態を判断する関数(getHeaderViewStatus)を見ていきましょう。
上で述べたとおり5つの状態を判断する式を持っています。

5つの状態を判断する条件式をA, B, C, Dとおくと以下のフローチャートのように整理できます。

条件式 簡易的な意味
A 初期状態かどうか
B 下向きにスクロールをしたかどうか
C ヘッダーが全て表示される位置に移動しているかどうか
D ヘッダーを全て隠れる位置に移動しているかどうか

Untitled Diagram (1).png

以下はヘッダーの状態を判断する関数のコードです。

    private func getHeaderViewStatus(_ sub: inout[String:CGFloat], _ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ lastScrollViewY: CGFloat, _ initHeaderFrame: [String:CGFloat]) -> headerViewStatus {
        if (scrollViewY <= (0 - initHeaderFrame["height"]!)) {
            return headerViewStatus.start(headerViewFrame, initHeaderFrame["minY"]!)
        } else if (lastScrollViewY > scrollViewY) {
            sub["down"] = 0
            if(headerViewFrame.origin.y >= scrollViewY + initHeaderFrame["maxY"]!) {
                return headerViewStatus.stop_up(scrollViewY, headerViewFrame, initHeaderFrame["maxY"]!)}
            else { return headerViewStatus.move_up(sub["up"]!, scrollViewY, headerViewFrame)}
        } else {
            sub["up"] = 0
            if(headerViewFrame.origin.y <= scrollViewY + initHeaderFrame["minY"]!) { return headerViewStatus.stop_down(scrollViewY, headerViewFrame, initHeaderFrame["minY"]!)}
            else { return headerViewStatus.move_down(sub["down"]!, scrollViewY, headerViewFrame)}
        }
    }

条件Aは以下のコードで表現されています。

if (scrollViewY <= (0 - initHeaderFrame["height"]!)) 

スクロールビューのy座標が、ヘッダーの高さ分だけ小さい位置に配置されているかについての条件式です。
初期状態ではスクロールビューはヘッダーの高さ分だけ上に配置されていますよね。
なので初期状態、もしくはそれより上に画面を進めた(下向きへスクロールした)場合ではTrueとなり、
それ以外ではFalseとなり次の条件式に進んでいきます。

参考にビューの初期状態を再掲します。

ThirdDiagram (11).png


条件Bは以下のコードで表現されています。

if(lastScrollViewY > scrollViewY)

前回スクロールした際のスクロールビューのy座標の方が現在のy座標よりも下にあるか(大きいか)どうかについての条件式ですね。
この条件式がTrueになるのは、ビューを下にスクロールして画面を上に進めた場合でしょう。

つまりこの条件式はビューを下にスクロールしたかどうかについての条件式となります。

例として上にスクロールした場合のビューの様子を以下の図で示しました。

Untitled Diagram (7).png


条件Cは以下のコードで表現されています。

if(headerViewFrame.origin.y >= scrollViewY + initHeaderFrame["maxY"]!)

ビューを下にスクロールした場合この条件Cを実行することになります。

ビューを下にスクロールした場合はヘッダーを出す必要があります。
しかし、出し過ぎる(ヘッダーを下げ過ぎる)のも問題ですよね。
画面の一番上にくっついているように表示したいです。

条件Cは画面の一番上にくっついているかどうかについて判断します。
画面のトップの位置、それより下にあればTrueで、
まだ完全に表示されきっていないならFalseとなります。

以下の図は条件式がTrue(等しくなる)場合のビューの状態を示しています。

Untitled Diagram (8).png

まずは変数の説明をしましょう。
headerViewFrame.origin.yヘッダービューの現在の座標を示しています。
scrollViewYスクロールビューの現在の座標を示しています。
initHeaderFrame["maxY"]ヘッダービューのデフォルト座標をヘッダーの高さ分下にずらした座標を示しています。

ここで、図における
header_height30
HeaderView_height230
画面の座標を(0, 100)とすると

赤点の座標(initHeaderFrame["maxY"])=(0, -200)
青点の座標(scrollViewY)=(0, 100)
橙点の座標(headerViewFrame.origin.y)=(0, -100)
となります。

そして条件式は、-100 >= 100 + (-200)よりTrueとなります。

これは、(青点+赤点)のy座標橙点のy座標同じ位置、もしくはそれ以上ならばヘッダーは全て表示されていることが保証されていることに由来します。
(皆さんも頭の中で青点を-200分動かしてみてください)

以下の図は上記の考え方を分かりやすく示したものです。(左の状態でTrue、右の状態でFalse)

Untitled Diagram (12).png

青点が二つありますが、上の方にある青点は-200された位置にあるものです。(仮想青点と呼びましょうか)
画面をスクロールしてヘッダーを仕舞う(図の右の状態に遷移)と橙点が移動したのが分かりますね。

この橙点仮想青点よりも上にあるとFalseで、橙点が仮想青点と重なる(もしくは下に位置する)Trueとなります。

この式によって、ヘッダーが画面に全て表示されているかどうか判断できるのです。

では最後に条件Dについても見ていきましょう。ロジックは全く同じです。


条件Dは以下のコードで表現されています。

if(headerViewFrame.origin.y <= scrollViewY + initHeaderFrame["minY"]!)

こちらも先ほど説明した条件Cにとても似ていますね。

不等号が逆になったのと、スクロールビューのy座標に足す値がヘッダービューのデフォルトy座標に変わりました。
(条件式が等しい時にTrueになるよう調節するためです)

こちらもビューがどのような状態の時に条件式が等しくなるのか示しておきましょう。
左の図はTrueの場合で、右の図はFalseの場合です。

それぞれの点は、
赤点の座標(initHeaderFrame["minY"])
青点の座標(scrollViewY)
橙点の座標(headerViewFrame.origin.y)
を示しています。

Untitled Diagram (13).png

この橙点仮想青点よりも下にあるとFalseで、橙点が仮想青点と重なる(もしくは上に位置する)Trueとなります。

つまり、ヘッダーが全て画面の上に隠れたかについての条件式です。


以上で全ての適切なヘッダーの状態を判断する条件式の説明を終えました。
こうして見ると、意外と複雑に考える必要があったんだという事が分かりますね。

さて、次はヘッダーの状態において適切な処理をしていくことになります。

3.ヘッダーの状態をもとにヘッダーに対してスクロール処理を実行する

まずは、ヘッダーの状態と、その際の適切なスクロール処理について表にまとめました。

状態 適切な処理
start 画面の一番上にくっついているように表示する
move_up ヘッダーを下げて表示する
stop_up ヘッダーを画面の一番上にくっついているように固定する
move_down ヘッダーを上げて画面から退場させる
stop_down ヘッダーが画面から見えないギリギリの位置に固定する

つまりヘッダーは以下の図の矢印の間を行ったり来たりするように動きます。

ThirdDiagram (12).png

スクロールについての処理を行うので、y座標に対する操作だけに注目してください。

注意点としては、ヘッダーではなくヘッダービューに対しての操作を行なっているという事です。

ヘッダーはヘッダービューの一部ですからね。


状態がstartの場合は処理は以下のように記述されています。

func start(_ headerViewFrame: CGRect, _ initHeaderFrameMaxY: CGFloat) {
            print("Start")
            myHeaderView.frame 
            = CGRect(x: headerViewFrame.origin.x, 
                     y: initHeaderFrameMaxY, 
                     width: headerViewFrame.width, 
                     height:  headerViewFrame.height)
}

initHeaderFrame_maxYはヘッダーのデフォルトy座標のことなので、ヘッダービューには初期状態がセットされます。


状態がmove_upの場合は処理は以下のように記述されています。

func move_up(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect) {
            print("Move_up")
            myHeaderView.frame 
            = CGRect(x: headerViewFrame.origin.x,  
                     y: headerViewFrame.origin.y + sub, 
                     width: headerViewFrame.width, 
                     height: headerViewFrame.height)
}

subという変数が出てきましたね。これは スクロール量です。
これは1.必要な値を取得するで述べた通り、subSetのキー"up"の値で、以下の計算式で求められます。

(lastContentOffset - scrollView.contentOffset.y)

上向きにスクロールしているので、lastContentOffset > scrollView.contentOffset.yです。
したがって、subの値はになることが分かりますね。

また、headerViewFrame.origin.yは現在のヘッダービューにおけるy座標なので、
(headerViewFrame.origin.y + sub)
は現在のヘッダービューにおけるy座標をスクロール量分大きくしている(下げている)ことが分かります。
ThirdDiagram (13).png


状態がstop_upの場合は処理は以下のように記述されています。

func stop_up(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMaxY: CGFloat) {
            print("Stop_up")
            myHeaderView.frame 
            = CGRect(x: headerViewFrame.origin.x, 
                     y: (scrollViewY + initHeaderFrameMaxY), 
                     width:  headerViewFrame.width, 
                     height:  headerViewFrame.height)
}

これはスクロールビューのy座標(scrollViewY)に常にヘッダーのデフォルトy座標+ヘッダーの高さ(initHeaderFrameMaxY)を足していますね。

条件Cの処理を思い出してください。ロジックは全く同じです。
以下の図に青点が二つありますが、上の方が(scrollViewY)+(initHeaderFrameMaxY)を表している仮想青点です。

仮想青点にヘッダービューのy座標を合わせる処理ということですね。

また、この処理が実行されるのは橙点と青点が重なる場合なので、常に図の左側を表示し続けることになります。
つまりヘッダーを常に表示されるように固定しているという事なんですね。
Untitled Diagram (12).png


状態がmove_downの場合は処理は以下のように記述されています。

func move_down(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect) {
            print("Move_down")
            myHeaderView.frame 
            = CGRect(x: headerViewFrame.origin.x, 
                     y: headerViewFrame.origin.y + sub, 
                     width: headerViewFrame.width, 
                     height: headerViewFrame.height)
}

こちらもmove_upの場合の処理と同じです。
違いはsubが負の値になっている事ですね。スクロールの向きに由来するものです。


状態がstop_downの場合は処理は以下のように記述されています。

func stop_down(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMinY: CGFloat) {
            print("Stop_down")
            myHeaderView.frame 
            = CGRect(x: headerViewFrame.origin.x, 
                     y: (scrollViewY + initHeaderFrameMinY),
                     width: headerViewFrame.width, 
                     height: headerViewFrame.height)
}

こちらもstop_upの場合の処理と同じように、この処理にたどり着くまでの条件をみていくと分かります。

stop_downの場合は条件Dを参照してください。

この処理はヘッダーを常に見えないギリギリの位置に固定していることになります。


必要な値を更新する

最後はプログラムに必要な値を更新する処理です。

と言っても、明示的に行うのは以下のたった一行のコードです。

lastContentOffset = scrollView.contentOffset.y

現在のスクロールビューのy座標を次のサイクルで使えるように、更新しているだけですね。

さて、ここまでで論理的なプログラムの説明を終わります。
お疲れ様でした。

ソースコード

とりあえず、ポイントとなるプログラムは以下に載せておきます。
Githubにサンプルを上げておくので、そちらも参考にされてみてください。

https://github.com/Hajime-Ito/HeaderTestSwift

 var lastContentOffset: CGFloat = 0

 enum headerViewStatus {
        case start(_ headerViewFrame: CGRect, _ initHeaderFrameMinY: CGFloat)
        case move_up(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect)
        case stop_up(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMaxY: CGFloat)
        case move_down(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect)
        case stop_down(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMinY: CGFloat)
    }

    private func getHeaderViewStatus(_ sub: inout[String:CGFloat], _ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ lastScrollViewY: CGFloat, _ initHeaderFrame: [String:CGFloat]) -> headerViewStatus {
        if (scrollViewY <= (0 - initHeaderFrame["height"]!)) {
            return headerViewStatus.start(headerViewFrame, initHeaderFrame["minY"]!)
        } else if (lastScrollViewY > scrollViewY) {
            sub["down"] = 0
            if(headerViewFrame.origin.y >= scrollViewY + initHeaderFrame["maxY"]!) {
                return headerViewStatus.stop_up(scrollViewY, headerViewFrame, initHeaderFrame["maxY"]!)}
            else { return headerViewStatus.move_up(sub["up"]!, scrollViewY, headerViewFrame)}
        } else {
            sub["up"] = 0
            if(headerViewFrame.origin.y <= scrollViewY + initHeaderFrame["minY"]!) { return headerViewStatus.stop_down(scrollViewY, headerViewFrame, initHeaderFrame["minY"]!)}
            else { return headerViewStatus.move_down(sub["down"]!, scrollViewY, headerViewFrame)}
        }
    }


    private func scrolling(status: headerViewStatus) {

        func start(_ headerViewFrame: CGRect, _ initHeaderFrameMaxY: CGFloat) {
            print("Start")
            myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: initHeaderFrameMaxY, width: headerViewFrame.width, height:  headerViewFrame.height)
        }

        func move_up(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect) {
            print("Move_up")
            myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: headerViewFrame.origin.y + sub, width: headerViewFrame.width, height: headerViewFrame.height)
        }

        func stop_up(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMaxY: CGFloat) {
            print("Stop_up")
            myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: (scrollViewY + initHeaderFrameMaxY), width:  headerViewFrame.width, height:  headerViewFrame.height)
        }

        func move_down(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect) {
            print("Move_down")
            myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: headerViewFrame.origin.y + sub, width: headerViewFrame.width, height: headerViewFrame.height)
        }

        func stop_down(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMinY: CGFloat) {
            print("Stop_down")
            myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: (scrollViewY + initHeaderFrameMinY), width: headerViewFrame.width, height: headerViewFrame.height)
        }

        switch status {
        case let .start(headerViewFrame, initHeaderFrameMinY): start(headerViewFrame, initHeaderFrameMinY)
        case let .move_up(sub, scrollViewY, headerViewFrame): move_up(sub, scrollViewY, headerViewFrame)
        case let .stop_up(scrollViewY, headerViewFrame, initHeaderFrameMaxY): stop_up(scrollViewY, headerViewFrame, initHeaderFrameMaxY)
        case let .move_down(sub, scrollViewY, headerViewFrame): move_down(sub, scrollViewY, headerViewFrame)
        case let .stop_down(scrollViewY, headerViewFrame, initHeaderFrameMinY): stop_down(scrollViewY, headerViewFrame, initHeaderFrameMinY)
        }
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        var subSet: [String:CGFloat] = ["up": 0, "down": 0]
        // MARK: -- 以下のinitialHeaderFrameに適切な値を入力するだけで、このプログラムは動作します。Yにはヘッダービューのy座標を、heightにはヘッダーの高さを入力してください。
        let initialHeaderFrame: [String:CGFloat] = ["Y": -260, "height": 60]
        let headerFrame: [String:CGFloat] = ["minY": initialHeaderFrame["Y"]! , "maxY": initialHeaderFrame["Y"]! + initialHeaderFrame["height"]!, "height": initialHeaderFrame["height"]!]

        subSet["up"]! += (lastContentOffset - scrollView.contentOffset.y)*0.1
        subSet["down"]! += (lastContentOffset - scrollView.contentOffset.y)*0.1

        let status = self.getHeaderViewStatus(&subSet, scrollView.contentOffset.y, myHeaderView.frame, lastContentOffset, headerFrame)
        self.scrolling(status: status)
        lastContentOffset = scrollView.contentOffset.y

    }

 コードを紐解く

ではコードを紐解いていきましょう。
全部説明する必要もないと思うので、特筆すべき部分だけにしときます。


1つ目は参照渡しですね。
getHeaderViewStatusの引数に注目してください。

private func getHeaderViewStatus(_ sub: inout[String:CGFloat], ....)

引数subinoutになっていますね。
これは参照渡しを明示的に行なっているという意味です。

通常、Swiftでは関数に渡す引数は値渡しとなります。

しかし、この関数ではsubに対する操作も行いたいので、参照渡しをすることになります。


2つ目はsub[String:CGFloat]についてです。
これは先ほどの参照渡しの話と関係しています。

subはスクロール量をもつ辞書型の変数でしたね。
それぞれ、updownをキーとして同じ値を持っていました。

これはそれぞれ上下にスクロールを始めてからのスクロール量を計算するためです。

そのために、getHeaderViewStatus関数内において、
上にスクロールを始めたら、sub["down"]を初期化してsub["up"]の値を利用する
下にスクロールを始めたら、sub["up"]を初期化してsub["down"]の値を利用する
ようなプログラムになっているんですね。

そして初期化処理を行うために参照渡しにしていた訳ですね。


以上で特筆すべきコードの説明を終わります。

おわりに

以上で終わりとなります。

理解するのに以外と頭をひねる必要があったかも知れませんね。
スクロール方向と画面の進む向きが逆になっていたりするからでしょうか?

また機会があれば、他に役に立ちそうなことも記事にしていきたいと思います。

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

UILabelの行数を調べる方法

UILabelの行数に応じてフォントサイズを変更したりしたかったのでこちらを参考にExtensionとして実装しました
やや使いにくそうでしたので、Swift5に対応してExtensionとして切り出して引数を必要としない形にしました。

環境

Swift 5
Xcode 11.2.1

行数を調べるExtension

行数を計算したいUILabelで利用してください

extension UILabel {

  /// 行数を返す
  func lineNumber() -> Int {
    let oneLineRect  =  "a".boundingRect(
      with: self.bounds.size,
      options: .usesLineFragmentOrigin,
      attributes: [NSAttributedString.Key.font: self.font ?? UIFont()],
      context: nil
    )
    let boundingRect = (self.text ?? "").boundingRect(
      with: self.bounds.size,
      options: .usesLineFragmentOrigin,
      attributes: [NSAttributedString.Key.font: self.font ?? UIFont()],
      context: nil
    )

    return Int(boundingRect.height / oneLineRect.height)
  }

}

参考

UILabel の行数を調べる方法 - Developers.io

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

知っているようで知らないSwift5のアクセス修飾子

はじめに

今回は、普段あまり意識しないアクセス修飾子について記事を作成していきます!
初心者でもわかりやすくまとめいるので覚えておくと便利です!
Swiftでは、バージョンによって意味が異なるので利用するときはSwiftのバージョンにも注意してください!
今回は、Swift5を利用して記事を作成しています。

アクセス修飾子とは?

アクセス修飾子は、クラスや関数等にアクセス制限を設けるために利用します。

//privateなどを以下のように利用します。
class HogeHogeViewController:UIViewController  {
    private let hoge: Int!
}

アクセス修飾子の種類

SwiftModifierSample.playground
    //そのクラス間のみアクセス可能(Swift3からextension内でも利用できるようになりました。)
    private let macBook = "MacBook"

    //private(set)を利用すると、setはprivate、getはinternalの制限にすることができます。
    private(set) var airPods = "airpods"

    //一つのプロジェクト内のターゲットが同一であれば呼び出せる。継承やoverrideもできる。
    internal let iPhone = "iPhone"

    //同じfile内であればアクセス可能
    fileprivate let imac = "iMac"

    // 一つのプロジェクト内のターゲットが同一でなくても呼び出せるが継承やoverrideはできない。
    public var appleWatch = "appleWatch"

    //一つのプロジェクト内のターゲットが同一でなくても呼び出せるが、継承やoverrideもできる。
    open var  macPro = "macPro"

privateが一番制限が強く、openが一番制限が低いです。
private -> private(set) -> internal -> fileprivate -> public -> open
アクセス制限は右に行くほど緩くなります。

実際に使ってみる。

SwiftModifierSample.playground
class Apple {
    private let macBook = "MacBook"
    private(set) var airPods = "airpods"
    internal let iPhone = "iPhone"
    fileprivate let imac = "iMac"
    public var appleWatch = "appleWatch"
    open var  macPro = "macPro"
}

extension Apple {
    public func hoge1(){
        print(macBook)
    }

    func hoge2(){
        print(macBook)
    }

    private func hoge3(){
        print(macBook)
    }
}

let apple = Apple()
apple.iPhone
apple.imac
apple.macBook  //error: 'macBook' is inaccessible due to 'private' protection level
apple.hoge1()
apple.hoge2()
apple.hoge3()  //error: 'hoge3' is inaccessible due to 'private' protection level

publicやopenの修飾子は主にライブラリー等の作成時に利用されることが多い。
アクセス修飾子を記述しないとデフォルトでinternalが適応される。

アクセス修飾子って必要なの?

個人アプリで開発していくならどちらでも良いと思いますが、チーム開発をしてる場合は、変数や関数にprivateをつけることで、クラス内部しか利用されていないことを、明示することができるのでコードレビューをしやすくなります。また、外部でアクセスできないようにすると似ている変数名を誤って呼び出すこともなくなります。逆にpublicをつけることでグローバル変数であること明示できるのでコードの読みやすさにつながります。

参考

https://www.sejuku.net/blog/22679
https://qiita.com/GATA/items/90341f56a56ff584e756

さいごに

今回は、Xcodeのplaygroundでサンプルコードを作成してGithubにアップしました!
週一でQiita記事を公開していくのでいいねとフォローをよろしくお願いします!

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

とりあえずSkyWayでメッセージアプリ

SkyWayを使ってメッセージアプリ

こんにちは.3回目の投稿です.

今回はWebRTCのプラットフォームSkyWayを使ってビデオ通話ではなくメッセージアプリを簡単に紹介します.
(firebaseを使うとメッセージ通信も簡単にできるのに,なぜskywayを選んだかについては,メッセージ機能を作りたくなった時にあまりfirebaseの知識がなく,調べているうちにたまたまたどり着いたのがSkywayだったんです.)

 利点/欠点

SkyWayを使うことでの利点は
- 無料で使える(有料なものあるけど使うなら調べてね)
- 簡単に実装できる
- メッセージアプリからビデオ通話アプリにも簡単に実装変更できる.
- 利用ユーザが登録が必要ない
- サーバレス(P2P通信)
だと思います.
これらの利点を活かした使い方としては,一時的なゲームアプリ内でのルームチャットや匿名チャットにはちょうどいい感じかと思います.
(実際,私が使おうと調べた理由は,刑ドロアプリ内でのチーム内チャットの実装のためです)

欠点として感じたのは
- WiFi環境からcellular環境に移った時には通信が行えない(逆も然り,cellular環境→WiFi環境)
- 多人数チャット用にRoomを作りメッセージの送受信ができるが,Roomは接続している人がいなくなると消えてしまう
が挙げられると思います.実際に使用するユースケースも考え,この辺りの対策が必要だと思います.

今回の目的

複数人でのメッセージ交換アプリの作成.

利用

開発者登録

こちらのサイトから簡単に新規登録が行えます.→ SkyWay:新規登録
登録すると仮登録状態になるので,送られてくるメールで本登録にアクティベートしましょう.

アプリケーション作成

ダッシュボードから新しくアプリケーションを追加するを選択
スクリーンショット 2020-02-01 15.15.04.png
公式のチュートリアルがかなり詳しく書かれているので参考に作成します.→チュートリアル
今回は何も入力しなくても大丈夫です.作成すると次のような画面になりAPIキーが表示されます.
スクリーンショット 2020-02-01 15.51.21.png
APIキーはあとで使います.

Xcode

サンプルプロジェクトを適当に作成してpodfileを作成, SkyWay.frameworkを導入してください.

Swiftプログラム

接続先のRoomは開発者アカウントごとに独立しています.IDを指定してRoomに接続した場合,Roomが存在する場合は接続し,存在しない場合はRoomが生成されます.
Roomの識別子として今回はUUIDを使用しますが,文字列ならば何でもいいみたいです.

Peer取得

P2P通信と言っても接続するためには,アドレスなどがわからないと接続できません.
SkywayではP2P接続およびルーム接続機能を操作するためのクラスとしてPeerクラスのインスタンスを最初に作成します.

サンプルクラス
import Foundation
import SkyWay

class Sample {
    let APIkey = "APIキー"
    let domain = "localhost"
    var peer: SKWPeer?
    var peerID: String?
    var connectedRoom: SKWRoom?

    init() {
        //peerインスタンスの作成
        let option: SKWPeerOption = SKWPeerOption.init()
        option.key = APIkey
        option.domain = domain
        peer = SKWPeer(options: option)
        if let _peer = peer {
            setupPeerCallback(peer: _peer)
        }
    }
    // peerに対してコールバック処理の設定
    private func setupPeerCallback(peer: SKWPeer) {
        peer.on(.PEER_EVENT_ERROR, callback: { (obj) in
            if let error = obj as? SKWPeerError {
                //エラーの処理...
            }
        })

        peer.on(.PEER_EVENT_OPEN, callback: {(obj) in
            if let id = obj as? String {
         //peer接続がopenした.
                print("your PeerID: \(id)")
            }
        })

        peer.on(.PEER_EVENT_CLOSE, callback:{ obj in
            //peer接続がcloseした.
            print("closed your peer")
        })
    }
    ・・・

Room接続

RoomのIDさえわかってしまえば誰でも入れます.
(パスワード付きに出来るのかは調べてないです.ざっと見た感じなかったと思います)

room接続
  open func roomIn(_ roomID: String) -> Bool {
        let option = SKWRoomOption()
        option.mode = .ROOM_MODE_MESH
        guard let peer = peer else {
            print("peerが確立されていません.")
            return
        }

        if let roomConnection = peer.joinRoom(withName: roomID, options: option) {
            print("connected in Room")
            self.connectedRoom = roomConnection
            setupRoomCallback(roomConnection)

        } else {
            print("Roomへの接続が失敗しました.")
        }
    }
    //Roomのコールバック処理の設定
    private func setupRoomCallback(_ room: SKWRoom) {
        room.on(.ROOM_EVENT_ERROR, callback: { obj in
           //Roomイベントのエラー時の処理を
        })

        room.on(.ROOM_EVENT_OPEN, callback: { obj in
          //Roomに入った時の処理を
        })

        room.on(.ROOM_EVENT_CLOSE, callback: { obj in
            //Roomから出た時の処理を
        })

        room.on(.ROOM_EVENT_DATA, callback: { obj in
            //データを受信した時の処理を
        })

        room.on(.ROOM_EVENT_LOG, callback: { obj in
            //Roomのイベントログが返ってきた時の処理を
        })
    }

メッセージの送信

この関数を呼び出すことでデータの送信ができます.

メッセージの送信
    open func send(strData: String){
        guard let room = connectedRoom else {
            print("おそらく接続していません")
            return
        }
        room.send(strData as NSString)
    }

    open func send(data: NSData) {
        guard let room = connectedRoom else {
            print("おそらく接続していません")
            return
        }
        room.send(data)
    }

メッセージの受信

受信
        room.on(.ROOM_EVENT_DATA, callback: { obj in
              if let data = obj as? SKWRoomDataMessage {
              let src = data.str //送信者
              let mess = data.data as! String //受信データ
         //ここに処理を書く 
              }
        })

コード全体

下のようなクラスを作成してインスタンスとして使うのが無難な気がします.

サンプルコード
//
//  QIita記事用サンプルコード.swift
//
//  Created by KEI on 2020/02/04.
//  Copyright © 2020 KEI. All rights reserved.
//

import Foundation
import SkyWay

class Sample {
    private let APIkey = "APIキー"
    private let domain = "localhost"
    private var peer: SKWPeer?
    var connectedRoom: SKWRoom?

    init() {
        let option: SKWPeerOption = SKWPeerOption.init()
        option.key = APIkey
        option.domain = domain
        peer = SKWPeer(options: option)
        if let _peer = peer {
            setupPeerCallback(peer: _peer)
        }
    }

    private func setupPeerCallback(peer: SKWPeer) {
        peer.on(.PEER_EVENT_ERROR, callback: { (obj) in
            if let error = obj as? SKWPeerError {
                //エラーの処理...
            }
        })

        peer.on(.PEER_EVENT_OPEN, callback: {(obj) in
            if let id = obj as? String {
                print("your PeerID: \(id)")
            }
        })

        peer.on(.PEER_EVENT_CLOSE, callback:{ obj in
            print("peer 接続がcloseしました.")

        })
    }

    open func roomIn(_ roomID: String) -> Bool {
        let option = SKWRoomOption()
        option.mode = .ROOM_MODE_MESH
        guard let peer = peer else {
            print("peerが確立されていません.")
            return
        }

        if let roomConnection = peer.joinRoom(withName: roomID, options: option) {
            print("connected in Room")
            self.connectedRoom = roomConnection
            setupRoomCallback(roomConnection)

        } else {
            print("Roomへの接続が失敗しました.")
        }
    }

    private func setupRoomCallback(_ room: SKWRoom) {
        room.on(.ROOM_EVENT_ERROR, callback: { obj in

        })

        room.on(.ROOM_EVENT_OPEN, callback: { obj in

        })

        room.on(.ROOM_EVENT_CLOSE, callback: { obj in

        })

        room.on(.ROOM_EVENT_DATA, callback: { obj in

        })

        room.on(.ROOM_EVENT_LOG, callback: { obj in

        })
    }

    open func send(strData: String){
        guard let room = connectedRoom else {
            print("おそらく接続していません")
            return
        }
        room.send(strData as NSString)
    }

    open func send(data: NSData) {
        guard let room = connectedRoom else {
            print("おそらく接続していません")
            return
        }
        room.send(data)
    }
}


最後

初心者には使いやすいライブラリだと思います.
また,開発はNTTの社内会社(??)が行なっているみたいなので公式ドキュメントも日本語でわかりやすかったです.

参考サイト

公式ドキュメント <- 日本語なのでわかりやすい!!
SkyWay初心者からステップアップしよう <- おそらく開発者さまの記事です!

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

NotificationCenter.addObserver(forName:object:queue:using:)にインスタンスメソッドを渡すとリークする

これまで、NotificationCenterのaddObserverはSwiftでもaddObserver(_:selector:name:object:)で書いていたのだけど、ふと「あー@objc書きたくないな〜」と思った。

そこで単純にaddObserver(forName:object:queue:using:)に置き換えたのだけど、これでリークを作ってしまった。

ようは、元々Objective-Cのセレクタを呼び出すこういうコードがあって

class TestViewController: UIViewController {
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        NotificationCenter.default.addObserver(self, selector: #selector(onAlpha(_:)), name: .myAppAlphaActionNotificaiton, object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    @objc
    func onAlpha(_ notificaiton: Notification) {
        // any
    }
}

こんな感じでaddObserver(forName:object:queue:using:)に書き換えたところ、このViewControllerをNavigationControllerからpopしてもdeinitされなくなった。

class TestViewController: UIViewController {
    private var alphaNotificationHandle: AnyObject?

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        alphaNotificationHandle = NotificationCenter.default.addObserver(
            forName: .myAppAlphaActionNotificaiton,
            object: nil,
            queue: OperationQueue.main,
            using: onAlpha(_:)
        )
    }

    deinit {
        alphaNotificationHandle = nil
    }

    func onAlpha(_ notificaiton: Notification) {
        // any
    }
}

よく考えたら、そのonAlphaの参照を残すために強参照が作られてしまって解放されなくなるのが原因だとわかるけど単純に書き換えでオッケーだなって軽く思っていて痛い目を見た感じ。

こう書いてある箇所では問題なかった。

class TestViewController: UIViewController {
    private var alphaNotificationHandle: AnyObject?

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        alphaNotificationHandle = NotificationCenter.default.addObserver(
            forName: .myAppAlphaActionNotificaiton,
            object: nil,
            queue: OperationQueue.main
        ) {[weak self] notification in 
            self?.onAlpha(notification)
        }
    }

    deinit {
        alphaNotificationHandle = nil
    }

    func onAlpha(_ notificaiton: Notification) {
        // any
    }
}

addObserver(forName:object:queue:using:) のusingは、ブロックを渡すのみで運用した方が良さそう。同様に、パラメータが一緒だからといって、引数にインスタンスメソッドそのものを渡すのは避けておいた方がトラブル回避できて良さそうだなと思った。

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