iOSモバイルアプリ「muupa」作ったよ！

# はじめに
SwiftUIの勉強のためにアプリを作ろうと思い、muupa（読み方：ムーパ）を作成しました。
この記事では、主な3つの機能についてと私のこだわりについてを書いていこうと思います。
参考にしたサイトや実際のコードを記載するので、誰かの役に立てれば嬉しいです！
（技術的内容は薄めで私のこだわりが9割占めます）

https://apps.apple.com/jp/app/muupa/id6502267909

# 主な機能
## ①色辞書
色の追加や削除、名称変更、お気に入り登録、色の詳細情報の表示などができます。
<img width="250" alt="色辞書" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/9cb1380a-15e5-d442-9db9-8c47b0968442.png">

## ②カラーピッカー
画面中央にある十字線と画像の交点のカラーコードを抽出します。
<img width="250" alt="カラーピッカー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/6f3088b5-1086-56ff-a012-34e7aedcd52a.png">


:::note warn
実はこの機能、未完なんです。正確なカラーコードが抽出できないんです。。。
これは残課題として後々改修します。
実際のソースコードを置いておくので「こうしたらどう？」みたいなご意見あればコメントお願いします！
:::

<details><summary>実際のコード（ColorPicker）</summary>

```Swift
import SwiftUI

// TODO: 抽出色がおかしい。
// TODO: 画像の縦幅が画面より大きいとタブバーに重なる。
struct ColorPicker: View {
    
    @State private var image: UIImage = UIImage(named: "AppIcon")!
    // リサイズ後の画像サイズ
    @State private var resizedImageSize: CGSize = .zero
    // 画像の可動域
    @State private var rangeOfMotionX: CGFloat = .zero
    @State private var rangeOfMotionY: CGFloat = .zero
    @State private var rValue: Int = 0
    @State private var gValue: Int = 0
    @State private var bValue: Int = 0
    // 移動中の座標
    @State private var dragOffset: CGSize = .zero
    // 移動開始前の座標
    @State private var dragStartPosition: CGPoint = CGPoint.zero
    @State private var isDragging: Bool = false
    
    var body: some View {
        GeometryReader { geometry in
            VStack {
                ZStack {
                    Image(uiImage: image)
                        .resizable()
                        .scaledToFit()
                        .offset(dragOffset)
                        .background() {
                            GeometryReader { geometry in
                                Path { path in
                                    let size = geometry.size
                                    DispatchQueue.main.async {
                                        if resizedImageSize != size {
                                            resizedImageSize = size
                                            
                                            let offsetX = (geometry.size.width - resizedImageSize.width) / 2
                                            let offsetY = (geometry.size.height - resizedImageSize.height) / 2
                                            dragOffset = CGSize(width: offsetX, height: offsetY)
                                        }
                                    }
                                }
                            }
                        }
                    
                    // 縦線
                    Rectangle()
                        .frame(width: 1, height: 100)
                    // 横線
                    Rectangle()
                        .frame(width: 100, height: 1)
                    
                    // 透明なカバー
                    Color.clear
                        .contentShape(Rectangle())
                        .gesture(DragGesture()
                            .onChanged { gesture in
                                if isDragging == false {
                                    dragStartPosition = CGPoint(x: dragOffset.width, y: dragOffset.height)
                                    isDragging = true
                                }
                                
                                dragOffset = CGSize(width: gesture.translation.width + dragStartPosition.x
                                                    , height: gesture.translation.height + dragStartPosition.y)
                                
                                rangeOfMotionX = ceil(resizedImageSize.width / 2 * 10) / 10
                                rangeOfMotionY = ceil(resizedImageSize.height / 2 * 10) / 10
                                
                                dragOffset.width = min(max(dragOffset.width, -rangeOfMotionX), rangeOfMotionX)
                                dragOffset.height = min(max(dragOffset.height, -rangeOfMotionY), rangeOfMotionY)
                                
                                (rValue, gValue, bValue) = getRGBFromImage(in: image.resize(to: resizedImageSize)!, at: CGPoint(x: dragOffset.width + rangeOfMotionX, y: dragOffset.height + rangeOfMotionY)) ?? (red: 0, green: 0, blue: 0)
                            }
                            .onEnded { gesture in
                                isDragging = false
                            }
                        )
                }
                PickedColor(rValue: $rValue, gValue: $gValue, bValue: $bValue, image: $image)
            }
        }
    }
}
```
</details>

<details><summary>実際のコード（getRGBFromImage）</summary>

```Swift
func getRGBFromImage(in image: UIImage, at point: CGPoint) -> (red: Int, green: Int, blue: Int)? {
    guard let cgImage = image.cgImage else {
        return nil
    }
    
    let width = cgImage.width
    let height = cgImage.height
    
    // ピクセルデータの取得
    guard let dataProvider = cgImage.dataProvider,
          let pixelData = dataProvider.data,
          let data = CFDataGetBytePtr(pixelData) else {
        return nil
    }
    
    // 指定座標の画像内での位置を計算
    let x = Int(point.x)
    let y = Int(point.y)
    
    // 画像内での座標が有効か確認
    guard x >= 0 && x < width && y >= 0 && y < height else {
        return nil
    }
    
    // ピクセル情報の取得
    let bytesPerPixel = cgImage.bitsPerPixel / 8
    let bytesPerRow = cgImage.bytesPerRow
    
    let pixelInfo = y * bytesPerRow + x * bytesPerPixel
    
    // RGB値の取得
    let red = Int(data[pixelInfo + 2])
    let green = Int(data[pixelInfo + 1])
    let blue = Int(data[pixelInfo])
    
    return (red, green, blue)
}
```
</details>

参考にしたサイト

https://swift-salaryman.com/pixcelcolor.php


## ③カラーパレット
SwiftDataを利用して、保存した色を画面にむわむわと表示します。
ファイルサイズの影響で添付ができなかったので、AppStoreで確認するか、実際にアプリを触ってみてくださいm(._.)m


# 私のこだわり
ここからは私のこだわりについて書いていきます。

## ⭐️神は細部に宿る⭐️
SwiftUIでリスト表示をすると、下の画像のように区切り線の左端が途切れてしまいます。
![リスト表示.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/8ca8302e-774d-1e4c-e00d-8e4b0aa93be9.png)

私はこれが気に食わなかったんです。
幅いっぱいにできないか探したら[こちら](https://webtopi.biz/archives/31653757382196)でちょうどそのことを解説していたので参考にしました。
結果、下の画像のように表現することが出来ました！
![色辞書こだわり.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/a7ba8cc4-0969-7d2d-66ff-d30b6a2fc783.png)


## ⭐️あなたの事を想って⭐️
検索バーって画面下部にある方が良くないですか？
もし上部にあると私の場合、
1. 親指を上の方に持っていって、検索バーをタップする
1. 画面下部に出てきたキーボードを親指で操作する

親指が上に下にと忙しいですね。
これ常々思っていたんですよね、なのでmuupaでは下の画像のように検索バーを画面下部に設置しました。
こうする事で指の操作範囲が狭まり、ユーザ負担の削減になると考えています。
![検索バーの設置位置.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/f72a76f4-a3fe-edc8-01ef-46b5077cc27b.png)


## ⭐️秩序ある自由⭐️
カラーピッカー機能にて、特に制限をしないと画像の可動域がどこまででも広がってしまいます。
それだと不親切かなと思ったので、muupaでは画像の各辺が十字線までしか移動できないように制限してあります。
[カラーピッカー](#カラーピッカー)の「実際のコード（ColorPicker）」に移動制限を実現するコードが書かれているので参考にしてみてください！

<img width="250" alt="左頂点" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/8a23d393-629d-dafe-67b2-a6359e98a2eb.png">（画像の左頂点が十時線まで）
<img width="250" alt="上辺" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/d82c9bb1-0730-a4fd-6260-eb0edc16ea56.png">（画像の上辺が十時線まで）
<img width="250" alt="右頂点" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/b4eae964-180d-1632-2ad2-caf55213ec40.png">（画像の右頂点が十時線まで）


## ⭐️願いが叶うなら、それでいい⭐️
muupaを作るにあたって似たようなアプリをいくつかダウンロードして、使ってみました。
カッコよく言うと”市場調査”ってやつですかね！
そうしたら、こういうの↓がまぁよくある
![よくあるやつ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/b6bf41c2-5947-6fc9-a024-e7967ced00d1.png)
これと同じものを作っても存在意義がないなと思ったのと、こういうのが使いたいなら他のアプリを使えばいいじゃないと思い、muupaでは必要かどうかは分からないけど、カラーパレットを動かしてみました。

下のライブラリを参考にSwiftDataで色を管理できるように修正を加えました。
SwiftDataについては色々知見が貯まったのでそのうち記事を書こうと思います。

https://github.com/Lakr233/Colorful


## ⭐️何事もほどほどに⭐️
”ムーパ”という響きから私は紫色をイメージしました。また、カラーパレットがむわむわ動くので雲もイメージして、アイコン案をいくつか作成しました。
採用したのはアイコン案③です。
本番用は文字サイズを大きくしてあります。
結局、雲感は他と比べると薄いですね。

私は凝り性なので、このアイコン作成にめっちゃ時間を掛けてしまいました。
でもそのおかげもあり、自分で納得のいくアイコンを作成することが出来ました！

アイコン案①
<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/79cce8f5-5809-d206-de1e-6d7a25750532.png">
アイコン案②
<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/d4d829eb-08a7-b13d-8061-31558ff2f02e.png">
アイコン案③
<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/1a4e2b0b-05fa-14a7-213f-60afb477bda8.png">
アイコン案④
<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/b7b81944-3dc1-e2c2-c844-7afb3e04cb71.png">
アイコン案⑤
<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808248/a8e1e96b-71e9-c832-016b-3bf2d349e829.png">


# まとめ
今回は「マネタイズが〜」とか「他のアプリとの差別化を図り〜」云々は考えず、SwiftDataというフレームワークに手を出したり、SwiftUIの最初から用意されている機能に満足せず、もっとこうしたいという願望を叶えられないかと孤軍奮闘しました。
私は結構怠惰な人間だと思っていたので、リリースまで出来たのは自分でも驚きです。
きっと、楽しかったのでしょうね。
muupaを作成したことでiOSモバイルアプリやSwiftui、SwiftDataなどの知見を貯めることが出来たと思います。
まだ成長できると思うので貪欲に勉強していこうと思います！
今後もアプリを開発していきます！