20200916のSwiftに関する記事は12件です。

【Swift】iOSで取得できる位置情報を実測してみた

iOSでの取得できる位置情報がどの程度正確かが気になったので、実測してみた。

前提知識

iOSにおいて、位置情報の精度は以下の6パターンを設定することが可能。

No. 補足
1 kCLLocationAccuracyBestForNavigation ナビアプリなどで利用できる最高精度
2 kCLLocationAccuracyBest 最高精度(デフォルト値)
3 kCLLocationAccuracyNearestTenMeters 誤差10m程度の精度
4 kCLLocationAccuracyHundredMeters 誤差100m程度の精度
5 kCLLocationAccuracyKilometer 誤差1,000m程度の精度
6 kCLLocationAccuracyThreeKilometers 誤差3,000m程度の精度

参考:CLLocationAccuracy

測定方法

下図のルート(A地点からB地点)を歩いてみて、OSが取得する位置情報の経度/緯度をプロットしてみる。
OSが取得する位置情報とは、locationManager(_:didUpdateLocations:) で取得できる位置情報のこと。

image.png

実測結果

誤差10m 誤差100m 誤差1,000m
image.png image.png image.png
平均して1分間に約60回位置情報を取得。
移動に8分15秒(495秒)かかり、その間に500回、位置情報更新メソッドが呼ばれた。
平均して1分間に約10回位置情報を取得。
移動に8分45秒(525秒)かかり、その間に90回、位置情報更新メソッドが呼ばれた。
平均して1分間に約5回位置情報を取得。
移動に8分36秒(516秒)かかり、その間に45回、位置情報更新メソッドが呼ばれた。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ニュースアプリにコロナ関連の記事があったら、リジェクトされた。

1.リジェクトされた理由

ニュースアプリのAppStoreに申請をしたのですが、
コロナ関連の記事があったので、リジェクトされました。

2.対策

コロナ関連の記事は削除しました。

3.注意点

今回は外部データをいじるだけだったので、新たにビルドする必要なかったら、
再申請では無く、問題解決センターに返信をしました。
こっちの方が早いからね!
再申請だとまた後ろから、並び直しだしね!

4.宣伝

これがコロナ関連の記事が無くなってしまったニュースアプリです。
NewsTweet(ニューズツイート)
https://apps.apple.com/jp/app/newstweet-ニューズツイート/id1531315934

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

【iOS14】デフォルトブラウザを変更した時にcanOpenURLがfalseになる問題

何が起こっている?

iOS14 PublicBeta8以降を入れたiOS端末で、デフォルトブラウザを変更した際にブラウザ遷移が行えない。

発生条件

  • iOS14PB版、もしくはGM版、正式版(iOS14.0)
  • デフォルトブラウザをsafari以外に設定していること

デフォルトブラウザ設定方法

  1. 「設定」アプリ起動
  2. デフォルトブラウザにしたいアプリへ移動(例:Chrome,Microsoft Edge)
  3. 「デフォルトブラウザApp」を開く
  4. 標準ブラウザをSafariからChromeに変更する

原因

UIApplicationのcanOpenURL メソッドがfalseを返却している。
これが、iOS14の仕様なのかバグなのかは現時点(日本時間2020/09/16)では不明。
canOpenURL returns false when I changed the default browser on iOS 14

そもそもcanOpenURLとは?

Determine whether there is an installed app registered to handle a URL scheme (canOpenURL(_:))
(URLスキームを処理するために登録されたインストール済みアプリがあるかどうかを判別する)

アプリから別のアプリを起動する(URLスキーム)際に、処理するためのアプリが端末に存在しているかを返す関数。
(そもそもここの認識が間違えていて、「全てのURL起動にcanOpenURLを通す」という誤用?が今回の事件を引き起こしたと思われる。私もその一人です)

対応方法

その1:canOpenURLを通さない

そもそもiOS端末にはブラウザアプリとしてSafariが必ず入っている。
かつ長押ししてもsafariアプリは削除もできないので、
ブラウザ起動をしようとして、iOS端末にブラウザアプリがないことは有り得ない はず。
(とはいえ、ブラウザ起動でcanOpenURLがfalseを返すことはないはずが、今回起こっているわけですが)

上記の通り、canOpenURLの目的は別のアプリの起動ができるかを確認するためのもののため、
ブラウザ起動のためのURL起動の場合は、canOpenURLを通す必要はなさそう。

よっていきなりopenを叩くように修正する。
これでデフォルトブラウザを変更していても、ちゃんとsafariではないブラウザアプリで起動される。

// AS-IS例
if UIApplication.shared.canOpenURL(URL(string: "https://qiita.com/")!) {
      UIApplication.shared.open(URL(string: "https://qiita.com/")!)
} else {
      print("ブラウザ起動失敗")
}
// To-BE例
UIApplication.shared.open(URL(string: "https://qiita.com/")!)

その2:plistにhttp/httpsを追加する

canOpenURLの判定には、plistの設定が反映されるためブラウザ起動のURLを許可する。

    <key>LSApplicationQueriesSchemes</key>
    <array>
        <string>http</string>
        <string>https</string>
    </array>

参考

公式:
UIApplication | Apple Developer Documentation(引用はこちらより)
canOpenURL(_:) | Apple Developer Documentation

参考:
特定のアプリがインストール済みかチェックする
[iOS] ディープリンク(Custom URL Scheme)でアプリを起動する


間違っている点や他の方法があれば教えてください!
iOS14の発表でバタバタしている同志のiOSエンジニアのみなさま、お互いに本当にお疲れ様です。

免責:全てのiOS14のアプリの動作を保証する解決方法ではございません

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

iOS14GMでcanOpenURLがおかしいから改めて整理してみた

何が起こっている?

iOS14 PublicBeta8以降(〜iOS14 GM版)を入れたiOS端末で、デフォルトブラウザを変更した際にブラウザ遷移が行えない
canOpenURL returns false when I changed the default browser on iOS 14

発生条件

  • iOS14
  • デフォルトブラウザをsafari以外に設定していること

デフォルトブラウザ設定方法

  1. 「設定」アプリ起動
  2. デフォルトブラウザにしたいアプリへ移動(例:Chrome)
  3. 「デフォルトブラウザApp」を開く
  4. 標準ブラウザをSafariからChromeに変更する

原因

UIApplicationのcanOpenURL メソッドがfalseを返却している。
これが仕様なのかバグなのかは現時点(日本時間2020/9/16)では不明

そもそもcanOpenURLとは?

アプリから別のアプリを起動する(Custom URL Scheme)際に、開こうとしているアプリが存在するかを返す関数。
参考:特定のアプリがインストール済みかチェックする
[iOS] ディープリンク(Custom URL Scheme)でアプリを起動する

対応方法

その1:canOpenURLを通さない

そもそもiOS端末にはブラウザアプリとしてSafariが入っており、かつ削除もできないので、
ブラウザ起動をしようとしてアプリがないことは有り得ない はず。
(だから、ブラウザ起動でcanOpenURLがfalseを返すことはないはずなのに今回起こっているわけですが)
そのため、ブラウザ起動のためのURL起動の場合は、canOpenURLを通さずにいきなりopenを叩くように修正する。

// AS-IS
if UIApplication.shared.canOpenURL(URL(string: "https://google.co.jp")!) {
      UIApplication.shared.open(URL(string: "https://google.co.jp")!)
} else {
      print("ブラウザ起動失敗")
}

// To-BE
UIApplication.shared.open(URL(string: "https://google.co.jp")!)

その2:plistにhttp/httpsを追加する

canOpenURLの判定には、plistの設定が反映されるためブラウザ起動のURLを許可する。

    <key>LSApplicationQueriesSchemes</key>
    <array>
        <string>http</string>
        <string>https</string>
    </array>

間違っている点や他の方法があれば教えてください

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

iOS14GMでデフォルトブラウザを変更した時のcanOpenURLがおかしいから整理してみた

何が起こっている?

iOS14 PublicBeta8以降を入れたiOS端末で、デフォルトブラウザを変更した際にブラウザ遷移が行えない。

発生条件

  • iOS14PB版、もしくはGM版、正式版(iOS14.0)
  • デフォルトブラウザをsafari以外に設定していること

デフォルトブラウザ設定方法

  1. 「設定」アプリ起動
  2. デフォルトブラウザにしたいアプリへ移動(例:Chrome,Microsoft Edge)
  3. 「デフォルトブラウザApp」を開く
  4. 標準ブラウザをSafariからChromeに変更する

原因

UIApplicationのcanOpenURL メソッドがfalseを返却している。
これが、iOS14の仕様なのかバグなのかは現時点(日本時間2020/09/16)では不明。
canOpenURL returns false when I changed the default browser on iOS 14

そもそもcanOpenURLとは?

Determine whether there is an installed app registered to handle a URL scheme (canOpenURL(_:))
(URLスキームを処理するために登録されたインストール済みアプリがあるかどうかを判別する)

アプリから別のアプリを起動する(URLスキーム)際に、処理するためのアプリが端末に存在しているかを返す関数。
(そもそもここの認識が間違えていて、「全てのURL起動にcanOpenURLを通す」という誤用?が今回の事件を引き起こしたと思われる。私もその一人です)

対応方法

その1:canOpenURLを通さない

そもそもiOS端末にはブラウザアプリとしてSafariが必ず入っている。
かつ長押ししてもsafariアプリは削除もできないので、
ブラウザ起動をしようとして、iOS端末にブラウザアプリがないことは有り得ない はず。
(とはいえ、ブラウザ起動でcanOpenURLがfalseを返すことはないはずが、今回起こっているわけですが)

上記の通り、canOpenURLの目的は別のアプリの起動ができるかを確認するためのもののため、
ブラウザ起動のためのURL起動の場合は、canOpenURLを通す必要はなさそう。

よっていきなりopenを叩くように修正する。

// AS-IS例
if UIApplication.shared.canOpenURL(URL(string: "https://qiita.com/")!) {
      UIApplication.shared.open(URL(string: "https://qiita.com/")!)
} else {
      print("ブラウザ起動失敗")
}
// To-BE例
UIApplication.shared.open(URL(string: "https://qiita.com/")!)

その2:plistにhttp/httpsを追加する

canOpenURLの判定には、plistの設定が反映されるためブラウザ起動のURLを許可する。

    <key>LSApplicationQueriesSchemes</key>
    <array>
        <string>http</string>
        <string>https</string>
    </array>

参考

公式:
UIApplication | Apple Developer Documentation(引用はこちらより)
canOpenURL(_:) | Apple Developer Documentation

参考:
特定のアプリがインストール済みかチェックする
[iOS] ディープリンク(Custom URL Scheme)でアプリを起動する


免責:全てのiOS14のアプリの動作を保証する解決方法ではございません
間違っている点や他の方法があれば教えてください!

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

Swiftでデザインパターン【Decorator】

元ネタ → ochococo/Design-Patterns-In-Swift

クラス図

image.png

図の引用元:Wikipedia: Decorator パターン

概要

The decorator pattern is used to extend or alter the functionality of objects at run- time by wrapping them in an object of a decorator class. This provides a flexible alternative to using inheritance to modify behavior.
Decoratorパターンは、Decoratorクラスのオブジェクトでオブジェクトをラップすることにより、実行時にオブジェクトの機能を拡張または変更するために使用する。これは、継承を使用して動作を変更する代わりに柔軟な方法を提供する。

サンプルコード

// Component
protocol CostHaving {
    // operation
    var cost: Double { get }
}

// Component
protocol IngredientsHaving {
    // operation
    var ingredients: [String] { get }
}

// Component
typealias BeverageDataHaving = CostHaving & IngredientsHaving

// ConcreteComponent
struct SimpleCoffee: BeverageDataHaving {
    // operation
    let cost: Double = 1.0
    let ingredients = ["Water", "Coffee"]
}

// Decorator
protocol BeverageHaving: BeverageDataHaving {
    // component
    var beverage: BeverageDataHaving { get }
}

// ConcreteDecorator
struct Milk: BeverageHaving {
    // component
    let beverage: BeverageDataHaving
    // operation
    var cost: Double {
        return beverage.cost + 0.5
    }
    // operation
    var ingredients: [String] {
        return beverage.ingredients + ["Milk"]
    }
}

// ConcreteDecorator
struct WhipCoffee: BeverageHaving {
    // component
    let beverage: BeverageDataHaving
    // operation
    var cost: Double {
        return beverage.cost + 0.5
    }
    // operation
    var ingredients: [String] {
        return beverage.ingredients + ["Whip"]
    }
}


// usage //
// 型推論に任せずに明示的に型を決定している。これによって、MilkもWhipCoffeeも同等に扱えるようにしている。
var someCoffee: BeverageDataHaving = SimpleCoffee()
print("Cost: \(someCoffee.cost); Ingredients: \(someCoffee.ingredients)")
someCoffee = Milk(beverage: someCoffee)
print("Cost: \(someCoffee.cost); Ingredients: \(someCoffee.ingredients)")
someCoffee = WhipCoffee(beverage: someCoffee)
print("Cost: \(someCoffee.cost); Ingredients: \(someCoffee.ingredients)")

クラス図との対応

サンプルコード クラス図
CostHaving Component
cost operation
サンプルコード クラス図
IngredientsHaving Component
ingredients operation
サンプルコード クラス図
BeverageDataHaving Component
サンプルコード クラス図
SimpleCoffee ConcreteComponent
cost, ingredients operation
サンプルコード クラス図
BeverageHaving Decorator
beverage Component
サンプルコード クラス図
Milk, WhipCoffee ConcreteDecorator
beverage component
cost, ingredients operation

考察

SimpleCoffeMilk/WhipCoffeに同じプロトコルBeverageDataHavingを準拠させ、同じ型のインスタンスとして扱っている(ポリモーフィズム)。
someCoffeeは、はじめはSimpleCoffeeのイニシャライザで生成され、その後、MilkWhipCoffeeのイニシャライザで置き換えられているが、BeverageDataHaving型としてアップキャストされているので、すべて同じ型として扱うことができる。
また、MilkWhipCoffeeともにbeverageを持つことで(集約)、デフォルトの値を参照している。

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

SceneKitでobjファイルで読み込んだオブジェクトにテクスチャを貼る

はじめに

Structureセンサでスキャン後に書き出したデータ(.objファイル)を、下記の記事を参考にSceneKitで読み込んだところ、テクスチャが反映されなかった1
参考記事:SceneKit to show 3D content in Swift 5

.mtlを上書き

.mtlファイルの記述が足りないようなので、.mtlファイルを開き、map_Ka xxx.jpgを書き足すとテクスチャが表示された(xxx.jpgはテクスチャ画像)。
なので、.mtlファイルの上書きをすることでテクスチャを表示することはできる。

コードでテクスチャ画像を指定する

SceneKitのオブジェクトでテクスチャを設定することもできる。
上記参考記事では、SCNSceneのイニシャライザで.objファイルを読み込んでいる。

let scene = SCNScene(named: "xxx.obj")

しかし、SCNSceneだと、テクスチャを貼る方法がない(と思われる)。
なので、オブジェクト用のSCNNode()を用意して、これにテクスチャを貼ることにした。

// Sceneに直接オブジェクトを貼るとテクスチャをイジれないので、チャイルドノードとして持たせる
let modelObj = SCNMaterial()

// objファイル読込(URL)
// アプリのディレクトリにファイルを置いている想定
if let documentDirectoryFileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last  {
    let fileName                = "hoge/xxx.obj" // .objファイル
    let url                     = documentDirectoryFileURL.appendingPathComponent(fileName)
    let objNode                 = SCNNode(mdlObject: MDLAsset(url: url).object(at: 0))
    objNode.geometry?.materials = [modelObj]
    scene.rootNode.addChildNode(objNode)
}

// diffuse.contentsにテクスチャを指定する
// テクスチャファイル読込(path)
if let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last {
    let fileName = "/hoge/xxx.jpg" // テクスチャファイル
    modelObj.diffuse.contents   = UIImage(named: path+fileName)
}

参考:ARKitで立方体の6面それぞれに異なるテクスチャを貼る方法


  1. Srructure SDKのサンプルコードはOpen GLで描画している。また、保存した.objファイルを詠み込む方法は示されていない。Open GLを自力で書いていくのは避けたいため、SceneKitに逃げたのであった。 

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

[Swift]3DモデルをSCNSceneに変換する方法2つ

Swiftで3DモデルをSCNSceneに変換するにはModelIOを経由する方法とscnファイルを作成する方法の2つがあります。
片方を実装するともう片方を忘れることが多くてその度に調べることになっているので、備忘録として残しておきます。

前提条件は以下の通りです。

  • Swift 5
  • Xcode 11.7
  • iOS 13.7

ModelIOを経由する方法

ModelIOのMDLAssetを使い、そこからSCNSceneを読み込みます。
最初にModelIOをimportしておきましょう。注意点としては本体ではなくSceneKitの方にあるものをimportすることです。

example.swift
import SceneKit.ModelIO

変換したい3Dモデルはプロジェクトに追加しておきます。
階層はどこでもいいですが複数の3Dモデルを使うのであればちゃんと1つのディレクトリにまとめておく方がいいでしょう。
今回は"hoge.usdz"という3Dモデルを追加しているものとしています。
あとはBundleから3DモデルのURLを取得して、そのURLをもとにMDLAssetを作成し、作成したMDLAssetからSCNSceneに変換します。

example.swift
guard let url = Bundle.main.url(forResource: "hoge", withExtension: "usdz") else { return }
let asset = MDLAsset(url: url)
let scene = SCNScene(mdlAsset: asset)

URL取得時のforResourceにはファイル名を、withExtensionには拡張子を指定します。
今回はusdz形式ですがobjでもgltfでも大丈夫です。

scnファイルを作成する方法

コードで実装せず最初からscnファイルを作成しておく方法です。
まずはModelIOを経由する方法と同じように3Dモデルをプロジェクトに追加します。
その後追加した3Dモデルを選択した状態で、Xcode上部のメニューにあるEditorから Convert to SceneKit file format (.scn)を実行します。
pic.png
するとプロジェクトに3Dモデルのファイルと同名の.scnファイルが追加されます。
例)hoge.usdz → hoge.scn
このscnファイルをSCNSceneで直接読み込みます。

example.swift
let scene = SCNScene(named: "hoge.scn")

どちらの方法でも難しいところはありませんが、ModelIOをimportしたりMDLAssetを作成する手間を考えると、scnファイルを作成する方法が便利ではないかと思います。

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

【iOS14】UNUserNotificationCenterでSwitch must be exhaustiveのwarning対応

Xcode12(GM)でビルドするとSwitch must be exhaustiveのwarningが👀
実装を確認すると、通知許可ステータスを確認するコードで吐き出されている様子

        UNUserNotificationCenter.current().getNotificationSettings { settings in
            switch settings.authorizationStatus {
            case .authorized, .provisional:
                print("許可")
            case .denied:
                print("拒否")
            case .notDetermined:
                print("許可求む")
            @unknown default:
                fatalError()
            }

UNAuthorizationStatusのリファレンスを覗くとiOS14から新たに
.ephemeral
が追加されていました。
https://developer.apple.com/documentation/usernotifications/unauthorizationstatus

この子は
The application is temporarily authorized to post notifications. Only available to app clips.
つまり、iOS14から実装可能なApp Clipsにおいて、一時的に通知の許可を取ることができ、そのステータスということです。

App Clipsを実装している場合の実装はこちらの公式リファレンスを参考に
https://developer.apple.com/documentation/app_clips/enabling_notifications_in_app_clips

let center = UNUserNotificationCenter.current()
center.getNotificationSettings { (settings) in {
    if settings.authorizationStatus == .ephemeral {

        // The user didn't disable notifications in the app clip card.
        // Add code for scheduling or receiving notifications here.
        return
    }
}

などを追加します。

App Clipsを実装していない場合は、通らない想定なので

        UNUserNotificationCenter.current().getNotificationSettings { settings in
            switch settings.authorizationStatus {
            case .authorized, .provisional:
                print("許可")
            case .denied:
                print("拒否")
            case .notDetermined:
                print("許可求む")
            case .ephemeral:
                fatalError()// or assertionFailure()
            @unknown default:
                fatalError()
            }

でwarning回避できます。
リファレンスにbetaと書かれているし、fatalError利用したくない方は念の為
assertionFailureで、取得失敗の扱いで良いかと思います🙋‍♂️

ご参考までに🐣🌱

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

ライブラリを使わずに美しいカラーピッカーを実装【iOSアプリ】【Swift】

経緯

お絵かきアプリにカラーピッカーを付けたかったのでいろいろ探したのですが、ライブラリを使ったのしか出てこなかった。
...
...
じゃあ自力で1から作っちゃおう!

環境

・macOS Catalina
・Xcode 11.7
・Swift 5

Assetsに画像を追加

・千鳥柄の画像 × 2枚
※なくても良い。(alphaが1より低いときにわかりやすくするため)
1枚目 -> サイズ:220 × 37.5 、名前:chidori_s
2枚目 -> サイズ:290 × 290 、名前:chidori
・Hueスライダー用の画像
サイズ:300×13、名前:colors
画像は私のブログ記事から入手できます。

全コード

ボタンを押すとColor Pickerが現れます。

import UIKit

class ViewController: UIViewController {

    // 現在表示されているSlider
    var sliderNow = ""
    // Hue一時保存用
    var hue: CGFloat!
    // Color Picker を表示するボタン
    var colorBtnNow: UIButton!

    // Color Picker
    var picker: UIView!
    var sliderPallete: UIView!
    // Color Pickerの上の白丸
    let thumb = UIView()
    // 変更前、現在の色プレビュー
    var twoColors = [UIButton(), UIButton()]
    // パレットのグラデーションレイヤー
    var gradLayer = [CAGradientLayer(), CAGradientLayer()]
    // Slider
    var colorSlider = [UISlider(), UISlider(), UISlider(), UISlider()]
    // Slider の色
    let sliderGrad = [CAGradientLayer(), CAGradientLayer(), CAGradientLayer(), CAGradientLayer()]
    // 現在のSliderの値
    var numText = [UILabel(), UILabel(), UILabel(), UILabel()]

    // 現在の色
    var colorNow: UIColor!
    // 現在のrgba/hsba
    var changingColor = [CGFloat(), CGFloat(), CGFloat(), CGFloat()]

    override func viewDidLoad() {
        super.viewDidLoad()

        let b = UIButton()
        b.frame = CGRect(x: 10, y: 60, width: 80, height: 50)
        b.backgroundColor = .systemOrange
        b.setTitle("button", for: .normal)
        view.addSubview(b)
        b.addTarget(self, action: #selector(showColorPicker(sender:)), for: .touchUpInside)
    }

    @objc func showColorPicker(sender: UIButton) {

        picker = UIView()
        picker.backgroundColor = UIColor(white: 0.2, alpha: 1)
        picker.layer.cornerRadius = 10
        picker.frame = CGRect(x: 60, y: 120, width: 310, height: 615)
        view.addSubview(picker)

        colorBtnNow = sender

        let parameters = ["RGB", "HLS"]
        let seg = UISegmentedControl(items: parameters)
        seg.frame = CGRect(x: 95, y: 20, width: 120, height: 35)
        seg.backgroundColor = .gray
        seg.selectedSegmentIndex = 0
        seg.addTarget(self, action: #selector(segmentChanged), for: .valueChanged)
        picker.addSubview(seg)

        // 色プレビュー
        let colorView = UIImageView()
        colorView.image = UIImage(named: "chidori_s")
        colorView.frame = CGRect(x: 45, y: 70, width: 220, height: 37.5)
        colorView.clipsToBounds = true
        colorView.layer.cornerRadius = 6
        colorView.isUserInteractionEnabled = true
        picker.addSubview(colorView)

        var x: CGFloat = 0
        for c in twoColors {
            c.frame = CGRect(x: x, y: 0, width: 110, height: colorView.frame.height)
            c.backgroundColor = sender.backgroundColor!
            colorView.addSubview(c)
            c.addTarget(self, action: #selector(setColor), for: .touchDown)
            x = 110
        }
        // グラデーションパレット
        let hsb = sender.backgroundColor!.hsb()
        let gradView = UIButton(frame: CGRect(x: 10, y: 120, width: 290, height: 290))
        gradView.addTarget(self, action: #selector(colorGradLayer_tapped), for: .allTouchEvents)
        gradView.setBackgroundImage(UIImage(named: "chidori"), for: .normal)

        hue = hsb[0]
        func set_gradLayer(_ g: CAGradientLayer, colors: [CGColor], start: CGPoint) {
            g.colors = colors
            g.startPoint = start
            g.endPoint = CGPoint(x: 0.0, y: 0.0)
            g.frame = CGRect(x: 0, y: 0, width: 290, height: 290)
            g.locations = [0, 1]
            gradView.layer.addSublayer(g)
        }
        set_gradLayer(gradLayer[0],
                      colors: [hsb_color([hsb[0], 1, 1, hsb[3]]).cgColor,
                               UIColor(white: 1, alpha: hsb[3]).cgColor],
                      start: CGPoint(x: 1.0, y: 0.0))
        set_gradLayer(gradLayer[1],
                      colors: [UIColor.black.cgColor, UIColor.clear.cgColor],
                      start: CGPoint(x: 0.0, y: 1.0))

        thumb.center = CGPoint(x: 290*hsb[1], y: 290*(1-hsb[2]))
        thumb.frame.size = CGSize(width: 24, height: 24)
        thumb.layer.cornerRadius = 12
        thumb.backgroundColor = .white
        thumb.isUserInteractionEnabled = false
        gradView.addSubview(thumb)
        picker.addSubview(gradView)

        setUpRGBView()
    }

    @objc func colorGradLayer_tapped(sender: UIButton, event: UIEvent) {
        if let touch = event.touches(for: sender)?.first {
            // タップした場所を取得
            let loc = touch.location(in: sender)

            if 0..<290 ~= loc.x { thumb.center.x = loc.x }
            if 0..<290 ~= loc.y { thumb.center.y = loc.y }

            changingColor[0] = hue
            changingColor[1] = thumb.center.x/290
            changingColor[2] = 1 - thumb.center.y/290

            if sliderNow == "hls" {
                for i in 1...2 { updateSlider(tag: i, value: changingColor[i]) }
            } else {
                changingColor = hsb_color(changingColor).rgb()
                for i in 0...2 { updateSlider(tag: i, value: changingColor[i]) }
            }
            changeColor()
        }
    }

    @objc func segmentChanged(_ segment: UISegmentedControl) {
        switch segment.selectedSegmentIndex {
        case 0: setUpRGBView()
        case 1: setUpHLSView()
        default: break
        }
    }

    @objc func setColor(sender: UIButton) {
        if sliderNow == "hls" { changingColor = sender.backgroundColor!.hsb()
        } else { changingColor = sender.backgroundColor!.rgb() }
        changeColor()
    }

    @objc func setUpHLSView() {
        sliderNow = "hls"
        setSlider(colors: colorBtnNow.backgroundColor!.hsb(), text: ["colors", "satu", "br", "alpha"])
        colorSlider[0].setMinimumTrackImage(UIImage(named: "colors"), for: .normal)
        colorSlider[0].setMaximumTrackImage(UIImage(named: "colors"), for: .normal)
        for s in colorSlider[0].layer.sublayers! { s.removeFromSuperlayer() }
        set_grad_hls(1...3)
    }
    func set_grad_hls(_ range: ClosedRange<Int>) {
        for i in range {
            var c = changingColor
            if i != 3 { c[3] = 1 }
            c[i] = 0
            let color1 = UIColor(hue: c[0], saturation: c[1], brightness: c[2], alpha: c[3])
            c[i] = 1
            let color2 = UIColor(hue: c[0], saturation: c[1], brightness: c[2], alpha: c[3])
            sliderGrad[i].colors = [color1.cgColor, color2.cgColor]
        }
    }

    @objc func setUpRGBView() {
        sliderNow = "rgb"
        let color = colorBtnNow.backgroundColor!
        setSlider(colors: color.rgb(), text: ["red", "green", "blue", "alpha"])
        set_grad_rbg(0...3)
    }
    func set_grad_rbg(_ range: ClosedRange<Int>) {
        for i in range {
            var c = changingColor
            if i != 3 { c[3] = 1 }
            c[i] = 0
            let color1 = UIColor(red: c[0], green: c[1], blue: c[2], alpha: c[3])
            c[i] = 1
            let color2 = UIColor(red: c[0], green: c[1], blue: c[2], alpha: c[3])
            sliderGrad[i].colors = [color1.cgColor, color2.cgColor]
        }
    }

    func setSlider(colors: [CGFloat], text: [String]) {
        sliderPallete?.removeFromSuperview()
        sliderPallete = UIView(frame: CGRect(x: 0, y: 410, width: 310, height: 250))
        picker.addSubview(sliderPallete)
        for i in 0..<colors.count {
            colorSlider[i] = slider(tag: i, img: text[i], value: colors[i], superV: sliderPallete)
            colorSlider[i].addTarget(self, action: #selector(slider_value_changed), for: .valueChanged)
            changingColor[i] = colors[i]
        }
        for i in 0..<sliderGrad.count {
            sliderGrad[i].frame = CGRect(x: 0, y: 0, width: 220, height: 20)
            sliderGrad[i].startPoint = CGPoint(x: 0.0, y: 0.0)
            sliderGrad[i].endPoint = CGPoint(x: 1.0, y: 0.0)
            sliderGrad[i].locations = [0, 1]
            sliderGrad[i].cornerRadius = 10
            colorSlider[i].layer.addSublayer(sliderGrad[i])
        }
    }
    func slider(tag: Int, img: String, value: CGFloat, superV: UIView) -> UISlider {
        numText[tag].backgroundColor = UIColor.clear
        numText[tag].font = .systemFont(ofSize: 16, weight: UIFont.Weight(6))
        numText[tag].textColor = .white
        superV.addSubview(numText[tag])

        let s = UISlider()
        s.tag = tag
        s.tintColor = .clear
        s.frame = CGRect(x: 65, y: 25+tag*40, width: 220, height: 20)
        superV.addSubview(s)
        if img == "alpha" {
            numText[tag].text = String(format: "%.1f", value)
        } else { numText[tag].text = "\(Int(value*255))"}
        s.maximumValue = 0
        s.maximumValue = 1
        s.setValue(Float(value), animated: false)
        numText[tag].frame = CGRect(x: 14, y: s.frame.origin.y-9, width: 60, height: 35)
        return s
    }

    @objc func slider_value_changed(slider: UISlider) {
        updateSlider(tag: slider.tag, value: CGFloat(slider.value))
        changeColor()
        changingColor[slider.tag] = CGFloat(slider.value)
        let c = changingColor
        // update grad layer
        if sliderNow == "hls" {
            hue = c[0]
            switch slider.tag {
            case 0, 3:
                gradLayer[0].colors = [hsb_color(c).cgColor, UIColor(white: 1, alpha: c[3]).cgColor]
                set_grad_hls(1...3)
            case 1: thumb.center.x = 290*c[1]; break
            case 2: thumb.center.y = 290*(1-c[2]); break
            default: break
            }
        } else {
            let hsb = rgb_color(c).hsb()
            hue = hsb[0]
            thumb.center = CGPoint(x: 290*hsb[1], y: 290*(1-hsb[2]))
            gradLayer[0].colors = [hsb_color([hsb[0], 1, 1, hsb[3]]).cgColor,
                                   UIColor(white: 1, alpha: hsb[3]).cgColor]
        }
    }

    func updateSlider(tag: Int, value: CGFloat) {

        colorSlider[tag].setValue(Float(value), animated: false)
        if tag == 3 { numText[tag].text = String(format: "%.1f", value) }
        else { numText[tag].text = "\(Int(value*255))" }
    }

    func changeColor() {
        var color = rgb_color(changingColor)
        if sliderNow == "hls" { color = hsb_color(changingColor) }
        else { set_grad_rbg(0...3) }
        colorBtnNow.backgroundColor = color
        colorNow = color
        twoColors[1].backgroundColor = color
    }


    func hsb_color(_ c: [CGFloat]) -> UIColor {
        return UIColor(hue: c[0], saturation: c[1], brightness: c[2], alpha: c[3])
    }
    func rgb_color(_ c: [CGFloat]) -> UIColor {
        return UIColor(red: c[0], green: c[1], blue: c[2], alpha: c[3])
    }
}

extension UIColor {
    func hsb() -> [CGFloat] {
        var (h, s, b, a) = (CGFloat(), CGFloat(), CGFloat(), CGFloat())
        getHue(&h, saturation: &s, brightness: &b, alpha: &a)
        return [h, s, b, a]
    }
    func rgb() -> [CGFloat] {
        var (r, g, b, a) = (CGFloat(), CGFloat(), CGFloat(), CGFloat())
        getRed(&r, green: &g, blue: &b, alpha: &a)
        return [r, g, b, a]
    }
}


実装例

スクリーンショット 0002-09-16 午前11.41.50.png

まとめ

スライダーの色を動的に変えることで
直感的に操作できるUIになりましたね!

もっと詳しく

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

iOSアプリ開発に必要なスキルをググりまくった

どうも、ねこきち(@nekokichi1_yos2)です。

未経験からiOSエンジニアとしての実力を身に付けるために、とりあえずアプリをリリースすればいいと最近まで思っていました。

ですが、とあるツイートがきっかけで、ポートフォリオを充実させるのではなく、スキルセットを揃えることも重要だと知りました。

確かにエンジニアの転職ではポートフォリオが重要と言われてますが、
(どんなに良い制作物を揃えても、転職先が求める人材、つまり求められるスキルを持ってないと転職は簡単じゃないのでは?)
とも言えます。

そもそも、ポートフォリオとはいえ、
・いくつ作ればいい?
・どのような内容が良いのか?
・どんなスキルが望ましいのか?
など、必要なことを挙げればキリがありません。

ポートフォリオだけに依存するのではなく、スキルセットにも目を向けるべきだなぁと。

ゴールを一から決めておけば、進む道が見えてくるので。

ということで、iOSアプリ開発、もしくはiOSエンジニアで求められるスキルを片っ端から調べて、自分なりにまとめてみました。

言語

Swift

  • 2014年に誕生
  • Appleが開発
  • モダンでパワフルなオープンソース言語
  • iOSのネイティブアプリを開発できる
  • Apple製品(Mac,AppleTV,AppleWatch,iPod)のアプリを開発できる
  • Objective-Cよりも、学習コストが低く、パフォーマンスでも優れている
  • 直感的で、書きやすくて、書くコードの量が少なく、初学者におすすめ

フレームワーク、API

SwiftUI

  • WWDC2020で登場
  • SwiftよりもUIの開発を抽象化したフレームワーク
  • ビルド要らずで、リアルタイムにコードのプレビューが見られる
  • コードの依存関係を自動で監視し、UIや値の更新が楽になる
  • iOS12以下は非対応
  • 複雑なUIや機能の実装には不向き

Alamofire

  • API通信用のライブラリ
  • 同じ系統のライブラリでは有名
  • データ通信の処理を簡単に書ける
  • Swiftに搭載されたURLSessionと比較されがち

RxSwift

  • FRP(関数型リアクティブプログラミング)のフレームワーク
  • オープンソース
  • RP:非同期処理、値が変化する度の処理をシンプルに書けるプログラミング
  • (Rx系のフレームワークは)エンジニアの必須技術
  • GCDやDispatchなど、非同期処理を簡潔に書ける
  • 学習コストが高く、チームに導入するのが面倒

Combine

  • FRPのフレームワーク
  • WWDC2019で登場
  • Appleの公式フレームワーク
  • RxSwiftと比較されがち
  • iOS13以降でないと使用できない

Vapor

  • Swiftでサーバーサイド開発ができるフレームワーク
  • GoやRailsよりも処理速度が高い
  • さくっとサーバーサイドを利用するなら、FirebaseやRealm
  • 独自のサーバーサイドを構築するのに向いてる

XCTest

  • XcodeのTDD(ユニットテスト)フレームワーク
  • UIや機能が想定どおりに動作するかテスト
  • 似たフレームワークでQuickがある
  • UIをテストできるXCUITestもある
  • リファクタリングと併用すれば、コードの安全性が保証される
  • テストコードの記述、改修の手間が増える

アーキテクチャ

MVC(Model View Controller)

  • Model:データ全般の処理
  • View:UIやデータの出力を担当
  • Controller:ユーザーからの入力を処理、ViewとModelを制御
  • 学習コストが低く、少人数規模向け
  • 機能別に分割されているので開発がスムーズ
  • 各機能の依存度が高く、テスト、再利用が難しい

CocoaMVC

  • Appleが推奨するMVC
  • 従来のMVCと違い、ViewとModelが完全に独立
  • ControllerがViewとModelを仲介
  • Controllerのコードが肥大化し、テストが難しくなる

MVP(Model View Presenter)

  • Model:データ処理全般
  • View:Modelの監視、Delegateを送る
  • Presenter:View,Modelの制御
  • データとUIの更新が連動してるので、フローを追いやすい
  • MVCよりも学習コストが高く、書くコードが多い

MVVM(Model View ViewModel)

  • Model:データ処理全般
  • View:UIを表示、ユーザーの入力を受け取る
  • ViewModel:Viewと自身を制御
  • ViewとViewModelの関係が密で、効率よく更新と表示を処理
  • 大規模向けで、学習コストが高く、個人開発向けでない
  • RP(RxSwift,ReactiveSwift)のフレームワークを理解する必要があり

データベース

CoreData

  • AppleのmBaaS
  • 保守性や安定性に優れている
  • XcodeとSwiftと相性が良い
  • Swiftでしか使えない
  • 学習コストが高い

Realm

  • オープンソースのmBaaS
  • Swift,Kotlin,Java,C#、などに対応
  • CoreDataよりも処理速度が早い
  • UserDefaultと同じくらい書きやすい
  • マルチプラットフォーム開発でも使用可
  • オープンソースなので、メンテナンスや改善の手間がかかる

CloudKit

  • AppleのmBaaS
  • iCloudにデータを保存
  • CoreDataと連携可能(ただしiOS13以降)
  • 他ユーザーの情報を行き来させるアプリの開発に不向き(チャットアプリなど)
  • iOS,Mac間でデータ共有したい人向け

Firebase

  • GoogleのmBaaS
  • C++,Java,Unityなどにも対応
  • リアルタイムのデータ共有
  • 多機能で拡張性が高い
  • 大規模な開発では、柔軟性が不十分
  • 学習コストが高い

ツール、エディタ

Xcode

  • Appleが提供するSwift用の統合開発環境(IDE)
  • Playgroundにより、リアルタイムでコードを実行できる
  • StoryBoardにより、ドラッグ&ドロップでインターフェースを構築できる
  • AppStoreにiOSアプリをリリースするのに必要不可欠
  • コードをその場で手軽にテストできる
  • Gitと連携して効率的なソース管理が可能

ターミナル

  • CUI(文字で操作する)ツール
  • 主な用途は、ファイル操作、Git
  • カーネル(パソコンの重要部分)を安全な形でいじれる
  • マウスで行っていた操作をコマンドですぐ実行できる
  • プログラミングも可能だが、IDEの専用エディタが大半

Git/GitHub

  • Git:ソースコードの変更履歴を分散管理するツール
  • GitHub:GitをSNSのように、誰もが閲覧、利用できるサービス
  • ソースコード管理ツールはエンジニアの必須技術
  • GitHubはGitの派生サービスで、似たサービスは複数ある(GitLab,BitBucket)
  • ユーザー数は業界で世界1位
  • MSに買収されて、GitLabの台頭を許した
  • 全機能を利用するのに料金がかかる
  • プライベートリポジトリが有料

GitLab

  • GitHubのライバル
  • GitLab.com(Webサービス)、CEとEE(オープンソース)の3種類
  • 自社サーバー、クラウド(AWS,GCP)での運用が可能
  • プライベートリポジトリが無償
  • チーム開発での利用が何人でも無償
  • Gitリポジトリのimport/exportが可能
  • 自社サーバーにセルフホストできて、セキュリティ性が高い
  • DevOps機能が圧倒的に充実している
  • GitLabの関連ツールを含めた、アップグレードが面倒

CocoaPods

  • パッケージ管理ツール
  • 使いやすく、多様な操作が可能
  • 対応してるライブラリの数が多い
  • ライブラリの導入、ビルドに時間がかかる
  • Xcodeの使用に依存するので、エラーが起こりやすい
  • さくっとライブラリを使いたい人向け

Carthage

  • パッケージ管理ツール
  • CocoaPodsよりも軽量でビルドが高速
  • 依存度が少なく、エラーが起こりにくい
  • 手動で設定する必要があり、導入に手間取る
  • CocoaPodsよりもライブラリの対応数は少ない
  • 安定して柔軟に開発したい人向け

SwiftPackageManager

  • Appleの公式パッケージ管理ツール
  • CocoaPod,Carthageより新しい
  • 他管理ツールに比べ、独立性が高い
  • 最新のツールなので、対応ライブラリ数が少ない
  • CUIやバックエンドのアプリが得意

CI/CDツール

Bitrise

  • クラウド型
  • CircleCIと比較される
  • 公式サイトは英語
  • モバイルアプリ開発向け
  • 料金プラン:個人40ドル、チーム100ドル、300ドル
  • GUIで操作するので分かりやすい
  • iOSに特化しており、Xcodeと相性が良い
  • GitHub,GitLab,BitBucketをサポート

CircleCI

  • クラウド型
  • Bitriseと比較される
  • Bitriseと違い、メモリに上限がある
  • 料金プラン:無料、30ドル、カスタム
  • SaaSで運用コストが低い
  • YAML(用途は設定やデータ処理)で記述
  • ssh接続によるデバッグ、テスト
  • GitHub,BitBucketをサポート

fastlane

  • CUI
  • モバイルアプリ用の自動化ツール
  • ビルド、テスト、デプロイなどを自動化
  • リリース作業(証明書、ipa、の設定や作成)も自動化
  • アクションと呼ばれる機能を組み合わせて使用
  • iTunesConnectで載せるアプリの情報を遠隔で管理できる

SwiftLint

  • Realmの静的解析ツール
  • Lint:コードをチェックするプログラム
  • Swiftのコーディングスタイルに強制する
  • バグに繋がる良くないコードを見つけてくれる
  • 警告やエラーで知らせてくれる

Danger

  • 自動コードレビューツール
  • CIツールとの連携が可能
  • プラグインが充実している
  • PullRequestにコメントでレビュー結果を表示
  • PullRequestでのレビューに特化
  • Swiftに特化したDanger-Swiftがある

reviewdog

  • 自動コードレビューツール
  • 機能はDangerとほぼ同じ
  • CircleCiなどのCIツールに対応
  • linter(SwiftLintなど)とも連携が可能
  • 1行単位でのレビューなど、Dangerよりも優れる

サービス

Sketch

  • UIデザインツール
  • 世界でシェア1位
  • 年額$99、無料プランは30日間
  • 他デザインツールとの互換性あり(AdobeXD,Figma,Studio)
  • 歴史が長く、完成度が高い
  • プラグインの数が圧倒的
  • プロトタイピングには不向き

AdobeXD

  • UIデザインツール
  • 日本でシェア1位
  • 年額¥14000、無料プランでも大半の機能を使える
  • 日本語のチュートリアルが充実している
  • プロトタイピングのアニメーションが優れている
  • シンプルなUIデザイン、個人開発などに向いてる
  • デザインの表現機能がSketchと比べて、イマイチ

Slack

  • チャットツール
  • ChatWork(日本発祥)と比較される
  • 月額¥960,年契約で月額¥850
  • 無料プランでも十分に使える
  • ワークスペースを複数作成できる
  • GitHubやDropboxなどのクラウドサービスと連携できる
  • 返信がスレッドで表示されるので、見やすい
  • ChatWorkはコスパ、Slackはカスタマイズ性に優れてる

まとめ

本記事を書くまで、全く知らなかった技術ばかりで、以前の無知な自分が恥ずかしく思えてきました。

エンジニアの技術は、”扱う言語で実装できる範囲と深さ”、で決まると思ってましたが、
- ツール
- ライブラリ、フレームワーク
- データベース
- アーキテクチャ
を最適な形で使いこなすことも必要なんだと知りました。

利用できる技術の幅が広ければ、開発できるアプリの幅も広がる。

また、エンジニア転職する際も、扱える技術が多ければ、即戦力として採用されやすい。

言語の技術力だけでなく、スキルの幅も、エンジニアになるのに必要なもの。

実務経験がなくても、本記事で上げた技術(もしくは業務や受託で使われている技術)を習得できれば、独学でも相当の開発力が身に付くでしょう。(僕はそう信じたい。)

参考

[ツール、エディタ]
【超初心者向け】ターミナルとは?Macの便利ツールを徹底解説
GitLab って何?
GitLabの運用方法を入門者向けに解説!GitHubとの違いも比較。ダウンロード、日本語化する方法も紹介!

GitHub.com・BitBucket.org・GitLab.comの月額料金比較 + α
GitLab自社運用のための注意点とノウハウ(2018/06版)

CI/CDツール
iOS・Androidで、CI/CD環境を作る際の選択肢
iOS用のCIサービスBitriseを使ってみた
いまさらだけどCircleCIに入門したので分かりやすくまとめてみた
AndroidのCIサービスをCircleCIからBitriseへ移行しました
fastlaneを使ってみる
iOSアプリ申請を驚くほど簡単に!fastlaneではじめる自動化入門
SwiftLint × Sider + SwiftFormat で Swift らしくリファクタする
reviewdogによるGoのコードレビュー
Danger から Danger-Swift 移行への手引き

データベース
Realmは「CoreDataもよくわかってないのに新しいDBなんて・・・」という人にこそオススメ
「Realmの基礎知識 〜特徴と強みの再認識〜」
RealmSwift vs CoreData
CloudKitについて調べてみた(その1)
iOS / iPadOS 13からCore DataとCloudKitの連携が自動化されたということで試してみた
mBaaSとしてのApple CloudKit の利用シーンと利用制限についての考察
Firestore導入前に知ってほしい。3層に分解して、メリット・デメリット比較と使いどころを考える

フレームワーク、API
iOSアプリ個人開発で使ってるツールとかノウハウを公開してみる
モバイルアプリ開発ツールメモ2016
SwiftUIのすすめ – 1.メリットとデメリット –
Alamofire vs URLSession: a comparison for networking in Swift
RxSwiftについてようやく理解できてきたのでまとめることにした(1)
RxSwiftを“チーム開発に”導入する話
SwiftのCombineを知る
【Server-side-Swift】【Vapor】iOSアプリエンジニアが挑戦する初めてのサーバーサイド【勉強会レポ】
iOSDCでVaporを布教してきた
Firebase Vs Vapor

[パッケージ管理ツール]
[Swift] CocoaPodsとCarthageの違い / ライブラリ管理
CarthageとCocoaPodsの違いを経験交えて比較する
Xcode 11 + Swift Package Managerでライブラリを管理する
Swift Package Manager vs CocoaPods vs Carthage for All Platforms
iOSアプリのライブラリ依存管理ツールとして Swift Package Managerを使うのは まだしばらく先かなと思った話

ユニットテスト、XCTest
XCTest入門 (Swift) ~UITest編~
https://techblog.yahoo.co.jp/advent-calendar-2018/ios/

アーキテクチャ
現場で選ばれているiOSアーキテクチャ
iOSアプリアーキテクチャ比較検討(Cocoa MVC,MVVM,MVP,CleanArchtecture)
MVCモデルについて
今更MVCとかでiOSアプリつくってみた(Swift)・改

サービス、他
受託開発での iOS アプリプロジェクト新規作成プラクティス(上編:Xcode 編)
地方エンジニアのための、オススメアプリ&サービスまとめ【思考整理/情報収集】
いちばん詳しい Sketch / XD / Figma / Studio の比較 〜1. 導入と背景知識
いちばん詳しい Sketch / XD / Figma / Studio の比較 〜6. まとめ
9 Skills you Require to Get Hired As An iOS Developer
iOS Developer Skills Matrix

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

UICollectionViewCompositionalLayoutでWaterfall(Pinterest風)レイアウトを実現する

iOS 13が登場して1年が経過しました。(もうiOS 14も出ますね)
昨年登場した物としてSwiftUIやCombineは注目度が高かったですがUICollectionViewCompositionalLayoutも忘れてはいけない存在です。
UICollectionViewCompositionalLayoutは簡単に柔軟なレイアウトを構築することができるので使い始めるとかなりの画面で役に立ちます。
しかし、少し凝ったレイアウトをしようと思うとどうすればいいのか分からなかったので、今回はPinterest風のレイアウトをどう実現するのかを考えてみました。

Waterfallレイアウト(Pinterest風レイアウト)とは

名称未設定.png

Waterfallレイアウトはこのように上からサイズが異なるコンポーネントが上から積み重なっているようなレイアウトのことを言います。
水が上から下に落ちるように見えるのでWaterfall(滝)ということですね。

レイアウトの考え方

レイアウトを作る際に重要なのは次の2点です。

  • カラム数(横に何列表示するか)
  • それぞれのセルのサイズが明確なこと

上記が分かっていればカラムごとに高さを足していけばそれっぽいレイアウトが作れます。

実装サンプル

protocol WaterfallLayoutDelegate: AnyObject {
    func numberOfColumns() -> Int
    func columnsSize(at indexPath: IndexPath) -> CGSize
    func columnSpace() -> CGFloat
}

lazy var collectionViewLayout: UICollectionViewCompositionalLayout = {
    return UICollectionViewCompositionalLayout { [unowned self] (section, environment) -> NSCollectionLayoutSection? in
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(environment.container.effectiveContentSize.height))
        let group = NSCollectionLayoutGroup.custom(layoutSize: groupSize) { [unowned self] (environment) -> [NSCollectionLayoutGroupCustomItem] in
            var items: [NSCollectionLayoutGroupCustomItem] = []
            var layouts: [Int: CGFloat] = [:]
            let space: CGFloat = self.waterfallLayout.flatMap({ CGFloat($0.columnSpace()) }) ?? 1.0
            let numberOfColumn: CGFloat = self.waterfallLayout.flatMap({ CGFloat($0.numberOfColumns()) }) ?? 2.0
            let defaultSize = CGSize(width: 100, height: 100)

            (0 ..< self.collectionView.numberOfItems(inSection: section)).forEach {
                let indexPath = IndexPath(item: $0, section: section)

                let size = self.waterfallLayout?.columnsSize(at: indexPath) ?? defaultSize
                let aspect = CGFloat(size.height) / CGFloat(size.width)

                let width = (environment.container.effectiveContentSize.width - (numberOfColumn - 1) * space) / numberOfColumn
                let height = width * aspect

                let currentColumn = $0 % Int(numberOfColumn)
                let y = layouts[currentColumn] ?? 0.0 + space
                let x = width * CGFloat(currentColumn) + space * (CGFloat(currentColumn) - 1.0)

                let frame = CGRect(x: x, y: y + space, width: width, height: height)
                let item = NSCollectionLayoutGroupCustomItem(frame: frame)
                items.append(item)

                layouts[currentColumn] = frame.maxY
            }
            return items
        }
        return NSCollectionLayoutSection(group: group)
    }
}()

大事なところだけ抜き出しておきました。
フルのサンプルコードは以下のGistに置いています。

fromkk/WaterfallLayout.swift

まとめ

NSCollectionLayoutGroup.customを利用することで柔軟なレイアウトも簡単に構築することができました。
NSCollectionLayoutGroupCustomItemframeの他にもzIndexを指定することができるので、色々と柔軟なレイアウトが作れそうですね。

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