ホームコントロールアプリのコンセプトデザインの実装


# はじめに
最近、デザイン実装のレベルを上げるために、[Dribbble](https://dribbble.com/)で見つけたコンセプトデザインの模写をしています。
今回は以下のデザインの実装について紹介したいと思います。
画面をスクロールをすると、スクロールに合わせて水温を調整することができるホームコントロールアプリのコンセプトデザインです。

<img src="https://cdn.dribbble.com/users/912634/screenshots/5435750/smarthomecontrol.gif">
[Smart home control app concept by Sam Atmore (Kiwi Sam) ](https://dribbble.com/shots/5435750-Smart-home-control-app-concept)



# 実際に作ったもの

![sample.gif](https://qiita-image-store.s3.amazonaws.com/0/308861/9e34841b-ed22-49f3-b8f0-9278f7708ee0.gif)

数字のアニメーションや波模様の動きなどを忠実には実装できていませんが、だいたいの実装はできました。
コードは公開しているので、よければ見てみてください。
https://github.com/EndouMari/ConceptDesign-HomeControlApp

# 構成
このデザインの構成は主に3つです。

1. スクロールに合わせて変化する水温
2. 水温に合わせて変化する背景
3. 波模様


# 実装について

今回は「スクロールに合わせて変化する水温」の表示の実装について説明します。
全体の説明については公開しているコードを見てください。

水温に合わせて変化する背景についてはpotatotipsで発表した資料があるので、こちらを見てください。
https://speakerdeck.com/endoumari/potatotips-56-concept-design

今回の実装では、水温は0~60の範囲とします。

スクロールに合わせて変化する水温を実装するために、以下２つについて考えました。

- スクロールに応じての水温の計算
- 水温の数値表示

それぞれどう実装したかを紹介します。

## スクロールに応じての水温の計算 

画面のスクロールに応じて水温を変更するために、指をおいた地点から動かした地点までの移動距離を求めます。
水温を求めるのに、画面全体の何割スクロールしているかを元に計算するので、スクロールするたびに移動距離を求めます。


今回計算をするために`touchesMoved(_:with:)`を使用しました。

```swift
var currentPointY: CGFloat = 0.0 // どのくらいスクロールしているかの数値
let maxWaterTemperature: Int = 60

class ViewController: UIViewController {

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }

        let diff = touch.previousLocation(in: view).y - touch.location(in: view).y

        guard diff != 0.0 else { return }

        currentPointY = diff
        let value: Int = Int(currentPointY / view.frame.height * CGFloat(maxWaterTemperature))
    }
}
```
これで、水温を求めることができました。
次はこの求めた水温をどうやってコンセプトデザインのように表示するかについてです。

## 水温の数値表示

コンセプトデザインをよく観察してみると、15から49に数値が変わるのに一の位の動きが5から9にしか変化していません。
これを実装するには、あらかじめ設定する水温を先に決めないと実装するのは難しいです。
しかし、コンセプトデザイン上では指の動きと同じタイミングで変化をしています。
これと全く同じ動きを実装するのは難しいので、一の位の動きは妥協して数値が変化するたびに反映するようにしました。

水温を表示するのに、一の位を表示するためのUICollectionView、十の位を表示するためのUICollectionView、2つを使用して実装しました。

数値をcollectionViewに渡すと`scrollToItem(at:at:animated:)`を使用して数値の表示変更をします。

また、0から9、9から0のようにくらいが変わるときにきれいに表示できるように要素数を3倍持つようにしています。

今回はUICollectionViewを継承したNumberCollectionを実装しました。


```swift
class NumberCollectionview: UICollectionView {
    
    var numberData: [Int] = []
    var currentNumberIndex: Int = 0
    private var currentSection: Int = 0
    
    enum Direction {
        case up
        case down
    }
    
    func setup(numberData: [Int], index: Int) {
        self.numberData = numberData
        scrollToItem(at: .init(item: index, section: 1), at: .top, animated: false)
    }

    func scrollToItem(at itemIndex: Int, direction: Direction) {
        let indexPath: IndexPath
        
        if direction == .up && itemIndex <= currentNumberIndex {
            indexPath = .init(item: itemIndex, section: 2)
        } else if direction == .down && itemIndex >= currentNumberIndex {
            indexPath = .init(item: itemIndex, section: 0)
        } else {
            indexPath = .init(item: itemIndex, section: 1)
        }
        scrollToItem(at: indexPath, at: .top, animated: true)
        currentNumberIndex = itemIndex
        currentSection = indexPath.section
    }
}

extension NumberCollectionview: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 3
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return numberData.count
    }

   ...
   
    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        if currentSection != 1 {
            scrollToItem(at: .init(item: currentNumberIndex, section: 1), at: .top, animated: false)
        }
    }
}
```
NumberCollectionViewに一の位、十の位の数値を設定して数値の表示をします。

```swift
var currentPointY: CGFloat = 0.0 // どのくらいスクロールしているかの数値
let maxWaterTemperature: Int = 60

@IBOutlet weak var numberCollectionView1: NumberCollectionview!
@IBOutlet weak var numberCollectionView2: NumberCollectionview!

class ViewController: UIViewController {

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }

        let diff = touch.previousLocation(in: view).y - touch.location(in: view).y

        guard diff != 0.0 else { return }

        currentPointY = diff
        let value: Int = Int(currentPointY / view.frame.height * CGFloat(maxWaterTemperature))

        let onesDisit = value % 10
        if numberCollectionView2.currentNumberIndex != onesDisit {
            let direction: NumberCollectionview.Direction = diff > 0 ? .up : .down
            numberCollectionView2.scrollToItem(at: onesDisit, direction: direction)
        }
        
        let tensDisit = value / 10
        if numberCollectionView1.currentNumberIndex != tensDisit {
            let direction: NumberCollectionview.Direction = diff > 0 ? .up : .down
            self.numberCollectionView1.scrollToItem(at: tensDisit, direction: direction)
        }
    }
}
```

以上の実装でスクロールに応じての水温を表示することができます。
詳しくは公開されているコードを見てください！

# まとめ
コンセプトデザインを実装してみて、普段の業務では使わない内容を学ぶことができてよかったです。
最初はどう実装するか悩みましたが、デザインを観察して分解してみることで何が必要かがわかり実装することができました。

デザインの実装の幅を広めるためにも、これからもコンセプトデザインの実装を続けていこうと思います。