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

[swift5]端末毎のビュー崩れを防ぐ方法(コードで指定)

はじめに

今回はiPhoneの端末サイズによってビューが崩れる現象を修正する実装方法についてアウトプットします。具体的な方法はいくつかありますが、今回はコードで要素の位置を指定する方法です。

環境

swift5
Xcode 11.6

要素の位置を指定

基本文法: 'CGRect(x:値, y:値, width:値, height:値)'

基本的にはHTMLのボックモデルと考え方が似ており、左上を基準に要素を動かします。
x軸は右に動かせば+、y軸は下に動かせば+となる。

widthとheightは指定した要素の横縦のサイズを指定する。
液晶全体の幅に合わせたい場合は、'view.frame.size.width(height)'と指定すれば画面いっぱいに要素を配置してくれる。

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

Swiftでインスタグラムへのシェア導線をつけてみる

はじめに

今回はアプリからインスタグラムのフィードへのシェア導線をつける実装です。詳しくはこちらに書いてあります?‍?

インスタグラムに遷移できるようにする

まずは、アプリからインスタグラムの Custom URL Scheme が利用できるように info.plist の LSApplicationQueriesSchemesinstagram://を追加していきましょう。

画像を保存して LocalIdentifier を取得する

    func saveImage() {
        UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
    }

    @objc private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
        let fetchOptions: PHFetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]

        let fetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
        if (fetchResult.firstObject != nil) {
            guard let lastAsset = fetchResult.lastObject else {
                return
            }
            let localIdentifier = lastAsset.localIdentifier
        }
    }

LocalIdentifier を使って Custom URL Scheme でインスタグラムに遷移する

    @objc private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
        let fetchOptions: PHFetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]

        let fetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
        if (fetchResult.firstObject != nil) {
            guard let lastAsset = fetchResult.lastObject, let urlScheme = URL(string: "instagram://library?LocalIdentifier=\(lastAsset.localIdentifier)") else {
                return
            }
            if UIApplication.shared.canOpenURL(urlScheme) {
                UIApplication.shared.open(urlScheme)
            }
        }
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Xcode Behaviors】Xcodeのイベントに合わせて動作を設定する

開発効率を高める便利機能

Xcodeの設定にBehaviorsという項目があるのをご存知でしょうか?
この項目から、Build/Testing/Runningなどの各イベント発生時の挙動を設定することができます。

例えば次のGIFのように、RunningのPausesイベント(デバッグ中のエラー発生やブレイクポイント到達)発生時に、別のタブを開いて該当部分を表示するといったことが可能です。
xcode-behaviors.gif
デフォルトの設定では、エラーが発生するたびに編集中のファイルからエラー発生箇所に移動してしまいますが、別タブで開く設定をしておけば、タブを移動するだけで編集中の状態に戻れるので、ストレスが軽減します。

デバッグ中にエラーが発生したときやブレイクポイントに達したときに別のタブを開く

behaviors.png
Behaviors > Running > Pauses から以下の項目を設定します。

  • Show tab named "Debug" in "active window"
  • "Show" navigator "Debug navigator"
  • "Show" debugger with "Variables & Console View"
  • "Hide" inspectors

ナビゲーターに"Debug navigator"に選択することで、スタックトレースが表示されます。
デバッガーに"Variables & Console View"を選択することで、変数の内容を表示するビューとコンソールが表示されます。
ちなみにタブ名とインスペクターを隠す設定は任意です。

ビルドに失敗したときに問題箇所を表示するナビゲーターを開く

スクリーンショット 2020-07-26 20.13.58.png
Behaviors > Build > Fails から以下の項目を設定します。

  • "Show" navigator "Issue navigator"

この項目はデフォルトで設定されているべきでは?と思うのですが、なぜかデフォルトではないようです。

テストに失敗したときに問題箇所を表示するナビゲーターを開く

ビルドのときと同様です。
Behaviors > Testing > Failsから以下の項目を設定します。

  • "Show" navigator "Test navigator"

参考

公式ドキュメント(あまり情報がない)
- Customizing Your Workflow
- Configure actions for events

タブをさらに使いこなしたい方はこちらが詳しいです
XcodeのBehaviorsを設定してデバッグ時にウインドウを自動で切り替える

他の設定項目についてはこちら
Xcodeのオススメ初期セットアップ

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

【Xcode Behaviors】Xcodeのイベントに合わせて動作を設定する - 開発効率をUPする便利機能

開発効率をUPする便利機能

Xcodeの設定にBehaviorsという項目があるのをご存知でしょうか?
この項目から、Build/Testing/Runningなどの各イベント発生時の挙動を設定することができます。

例えば次の画像のように、RunningのPausesイベント(デバッグ中のエラー発生やブレイクポイント到達)発生時に、別のタブを開いて該当部分を表示するといったことが可能です。
before-running.pngrunning-fails.png
デフォルトの設定では、エラーが発生するたびに編集中のファイルからエラー発生箇所に移動してしまいますが、別タブで開く設定をしておけば、タブを移動するだけで編集中の状態に戻れるので、ストレスが軽減します。

デバッグ中にエラーが発生したときやブレイクポイントに達したときに別のタブを開く

behaviors.png
Behaviors > Running > Pauses から以下の項目を設定します。

  • Show tab named "Debug" in "active window"
  • "Show" navigator "Debug navigator"
  • "Show" debugger with "Variables & Console View"
  • "Hide" inspectors

ナビゲーターに"Debug navigator"に選択することで、スタックトレースが表示されます。
デバッガーに"Variables & Console View"を選択することで、変数の内容を表示するビューとコンソールが表示されます。
ちなみにタブ名とインスペクターを隠す設定は任意です。

ビルドに失敗したときに問題箇所を表示するナビゲーターを開く

スクリーンショット 2020-07-26 20.13.58.png
Behaviors > Build > Fails から以下の項目を設定します。

  • "Show" navigator "Issue navigator"

この項目はデフォルトで設定されているべきでは?と思うのですが、なぜかデフォルトではないようです。

テストに失敗したときに問題箇所を表示するナビゲーターを開く

testing.png
ビルドのときと同様です。
Behaviors > Testing > Failsから以下の項目を設定します。

  • "Show" navigator "Test navigator"

参考

公式ドキュメント(あまり情報がない)
- Customizing Your Workflow
- Configure actions for events

タブをさらに使いこなしたい方はこちらが詳しいです
XcodeのBehaviorsを設定してデバッグ時にウインドウを自動で切り替える

他の設定項目についてはこちら
Xcodeのオススメ初期セットアップ

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

[iOS]APIKit + Combine + SwiftUIでGitHubリポジトリ検索

iOS13に出てきたSwiftUI・Combineと、ライブラリのAPIKitを使い、通信部分の実装手法を紹介したいと思います。
今回は、GitHub APIを使用します。

実装のポイント

  • できるだけシンプル:冗長なコードはなるべく書かない
  • 保守性がある:読みやすいコード
  • 再利用性がある:別の異なるAPIでも少しの変更で利用が可能

開発環境・使用するライブラリなど

開発環境

  • OS:10.15.6
  • Xcode:10.6
  • iOS:iOS13以降(iOS14では確認していません)

使用するライブラリ

APIKit
今回は、APIKitを拡張し、CombineのPublisherでAPIを叩いた結果を取得します。

使用するフレームワーク

  • SwiftUI
  • Combine

また、Swift標準のDecodableを使い、JSONをパースします。

使用する考え方

  • リポジトリパターン:APIやDBにアクセスするための定義を抽象化

実装

全体像

APIKit.png

  • ContentView:UI表示
  • GitHubSearchModel:ロジックを管理するクラス
  • GitHubRepository:APIへのアクセス方法を定義

API基本部分

API部分のクラス相関図は以下のようになります。

APIKit-2.jpg

GitHubRepository.swiftについては、中身は以下のようになっています。

GitHubRepository.swift
import Foundation
import APIKit

class GitHubRepository {

    //GitHubレスポンス用のデコーダー
    static let decoder: GitHubDecoder = .init()

    //リポジトリを検索
    struct SearchRepositories: GitHubRequestProtocol {
        //https://developer.github.com/v3/search

        //検索クエリ
        let query: String

        let method: HTTPMethod = .get
        let path: String = "/search/repositories"
        var decoder: JSONDecoder {
            return GitHubRepository.decoder
        }

        var params: [String: Any] {
            return [
                "q": query
            ]
        }

        typealias Response = SearchResponse

    }

    private init() { }
}

GitHubRepositoryはリポジトリなので、APIアクセスに必要な情報だけを定義(抽象化)しています。
SearchResponseはDecodableに準拠しています。
GitHubRequestProtocol.swiftGitHubDecoder.swiftAPIDataParser.swiftはソースをご確認ください。

ここまでで、GitHub APIを叩くための準備ができました。

Modelの実装

GitHub APIを使い、情報を取ってくるロジック(モデル)を実装します。
今回はViewとしてSwiftUIを使用するので、ロジックはObservableObjectを継承したクラスにします。

GitHubSearchModel.swift
/// 検索モデル
class GitHubSearchModel: ObservableObject {

    @Published var items: [SearchItem] = [] //検索結果
・・・・
    /// 検索を行う
    func search() {
        debugPrint("search")

        // 1)APIリクエストを作成
        let request = GitHubRepository.SearchRepositories(query: searchText)

        // 2)CombineのPublisherを作成し、通信処理を行う
        self.requestCancellable = request.publisher //Publisherに変換(この時点で通信処理を行なっている)
            .receive(on: DispatchQueue.main) //メインスレッドで受け取る
            .sink(receiveCompletion: { result in
・・・・
            }, receiveValue: { [weak self] response in
                // 3)結果をitemsに保存
                self?.items = response.items
            })
        // 4)itemsの更新はCombineを通して通知される

    }

1) APIリクエストを前述のリポジトリから作成します。
2) 通信処理を行います。
3) 結果をitemsに格納します
4) itemsの変更はCombineによりUIに通知されます。

UI

ContentView
こちらをご確認ください。

実際に使ってみた

apikit.gif

ソースコード

今回のソースコードはこちらに置いてあります。
https://github.com/usk-lab/APIKitCombine

再利用性について

今回はGitHub APIを使用しました。
GitHubRepository, GitHubRequestProtocol, GitHubDecoderを少し変えることで他のAPIサービスでも利用できます。

参考

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

ラブホテル検索アプリを作った話

こんにちは!久しぶりにQiita記事を書きます!
今回は現在地から近い・安いラブホテル検索アプリを開発したので、紹介&実装の一部を今後振り返れるよう書き記します!!
https://apps.apple.com/jp/app/ラブホテル簡単検索-ホテルイキタイ/id1524262826
是非、覗いて見てください…笑

6.5incSS1.png
6.5incSS2.png
6.5incSS3.png

作った理由

  • 検索時の情報過多を解消するアプリを作りたいと思った。 (ex.グルメアプリなど多くの情報を入力してそれに適したものを表示してくれるが、その初めの入力作業が億劫に感じた。)
  • Pinterestのような、まずレコメンドを表示する。探索と検索両方を合わせ持つアプリを他の分野で作りたいと思った。
  • モバイルに適応している(UI、UX)ラブホテル検索サービスがあまりないと感じたから。

上記の点よりラブホテルの検索アプリを作ろうと決めました!!

GAS(Google Apps Script)とFireStore連携

アプリを作るにあたってまず、都内のラブホテルの情報をGoogleスプレッドシートに書き込み、書き込んだ情報をFireStoreにコレクションとして追加しました。
スクリーンショット 2020-07-26 15.59.39.png
スクリーンショット 2020-07-26 16.01.10.png
ここでは2つのスプレッドシートを作りました。
一つ目はホテルの簡易な情報、二つ目は詳細な情報です。
これらをFireStoreに追加します。下記の記事を参考にさせていただきました。詳しい手順はこちらをみていただければと思います。
https://medium.com/@m_coder/バックエンドの知識がない自分がfirestore-googleappsscriptで簡易dbを構築した話-def690bb3c4d

const sheet = SpreadsheetApp.getActiveSheet();
const data  = sheet.getDataRange().getValues();

var START_ROW = 1
var START_COL = 1

function getSheetValue(sheet) {
  // 1行目(フィールド)の値を取得
  var fieldRange = sheet.getRange(1, 1, 1, sheet.getLastColumn());
  var fieldRowValues = fieldRange.getValues();
  var fields = fieldRowValues[0];
  var rowValues = [];
  var jsonArray = [];

  var range = sheet.getRange(START_ROW, START_COL, sheet.getLastRow() - START_ROW + 1, sheet.getLastColumn())
  var values = range.getValues();

  for(var i = 0; i < values.length; i++) {
    // 1行のデータをpush
    var rowValue = values[i];
    var json = new Object();
    for(var j = 0; j < fields.length; j++) {
      json[fields[j]] = rowValue[j]
    }
    jsonArray.push(json)
  }

  return jsonArray;
}

function firestoreCertification() {
  var certification = {
    "email": "{client_emailの値}", 
    "key": "-----BEGIN PRIVATE KEY-----\n{private_keyの値}\n-----END PRIVATE KEY-----\n",
    "projectId": "{project_idの値}"
  }
  return certification;
}

function createFirestoreDocuments() {
  var arrayData = getSheetValue(sheet);
  arrayData.forEach(createDocument)
}

function createDocument(item) {
  var certArray = firestoreCertification();
  var firestore = FirestoreApp.getFirestore(certArray.email, certArray.key, certArray.projectId);
   firestore.createDocument('HotelList',item);
}

これでcreateFirestoreDocuments関数を実行することでFireStoreに書き込みが行われます。
スクリーンショット 2020-07-26 16.31.31.png
スプレッドシートに記入していた内容をドキュメントとして書き込むことができました!
新たにスプレッドシートに追加で書き込んでFireStoreにドキュメントを追加したい場合は

//新たに追加し始める行
//ex.新たに10行目からスプレッドシートに書き込んだ場合
var START_ROW = 10

上記のコードの場合START_ROWの値を変えることで可能です。

CloudFunctionsで全体に書き込みしたい

上記のスプレッドシートの画像を見ていただけると分かるのですが、2枚目のホテルの詳細が書いてあるスプレッドシートのフィールドにhotelIdがあると思います。
このhotelIdはHotelListコレクションのドキュメントIDを値としています。
※HotelListはスプレッドシート画像1枚目のデータをFireStoreに書き込んだコレクションです。
スクリーンショット 2020-07-26 16.51.19.png

上記の画像のように該当するhotelIdをクエリして詳細ページに遷移しています。

HotelDetailModel.swift
class HotelDetailModel {
    var items = [HotelDetailItem]()
    weak var delegate: HotelDetailModelDelegate?

    func fetchHotelDetail(with hotelId: String) {
        let query = Firestore.firestore().collection("HotelDetail").whereField("hotelId", isEqualTo: hotelId)
        query.getDocuments { [weak self] (snapshot, error) in
            guard let `self` = self else { return }
            if let snapshot = snapshot {
                self.items = snapshot.documents.map {HotelDetailItem(from: $0.data(), docId: $0.documentID)}
            }
            self.delegate?.didFetchHotelDetail(with: error)
        }
    }
}

※一部抜粋
前置き大変長くなって申し訳ありません。とにかくHotelDetailそれぞれのドキュメントに該当するhotelIdをCloudFunctionsでまとめて追加したいという話です…笑

TypeScriptで実装

導入はこちらを参考にしました。
https://qiita.com/star__hoshi/items/7dcf5970d28a7ff239fb
以下hotelIdを追加するために書いたコードです。

index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin'

admin.initializeApp(functions.config().firebase)
const db = admin.firestore()
type DocumentReference = admin.firestore.DocumentReference

interface HotelItem {
    readonly address: String
    readonly area: String
    readonly imageUrl: String
    readonly name: String
    readonly restMin: number
    readonly stayMin: number
    ref: DocumentReference
}

interface HotelDetail {
    readonly address: String
    readonly name: String
    readonly access: String
    readonly callNumber: number
    readonly hotelUrl: String
    readonly roomNum: number
    readonly imageUrlList: String
    readonly restPlan: String
    readonly stayPlan: String
    hotelId: String
}

  export const requestHotelIdGain = functions.region('asia-northeast1').https.onRequest(async (req, res) => {
    try {
      const result = await fetchHotelId()
      return res.status(200).send(result)
    } catch (error) {
      return res.status(400).send(`Failed ${error}`)
    }
  })

async function fetchHotelId() {

    const hotelListQuery =  db.collection('HotelList')
    const hotelDetailQuery =  db.collection('HotelDetail')
    const hotelListSnap = await hotelListQuery.get()
    const hotelDetailSnap = await hotelDetailQuery.get()
    const batch = db.batch()

    hotelListSnap.docs.forEach(doc => {
        const hotelItem = doc.data() as HotelItem
        hotelDetailSnap.docs.forEach(detailDoc => {
            const hotelDetail = detailDoc.data() as HotelDetail

            if (hotelItem.name === hotelDetail.name) { 
                batch.update(detailDoc.ref,{hotelId: doc.id})
            }
        })
    })
    await batch.commit()
    return new Promise<string>(resolve => resolve(`yeahhh`))

}

やったことはHotelListHotelDetailのコレクションを取ってきて、
それぞれforEachで一つ一つのドキュメントにif (hotelItem.name === hotelDetail.name)で同じホテルの名前であったときに、一致したHotelDetailのドキュメントに一致したHotelListのdocumentIdを書き込んでいます。
batch.update(detailDoc.ref,{hotelId: doc.id})
スクリーンショット 2020-07-26 17.26.11.png
無事hotelIdフィールドに該当するdocumentIdが追加されました!!

最後に

最後まで見ていただきありがとうございます!!
間違っている点ありましたらご指摘していただけると幸いです!
他にも苦戦した点も後々書こうかと思います!
正直、今回のアプリは使う場面も限られており且つ、多くの人に使ってもらえるかは…ですが、今後も自分で作っていきたいなと思いました!!

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

【iOSアプリ開発】Macのloalhostに実機(iOS)から接続する方法

iOSアプリ開発をしていて、サーバサイドとの連携機能を作成するにあたって、
Macのlocalhostに実機(iOS)から接続する方法を調べました。

はじめに

Mac上に立てたlocalhostに対して実機(iOS)のアプリから接続する方法と、
iOSシュミレータから接続する方法になります。

対象バージョン

  • Xcode ver.11.6
  • Simulater ver.13.6
  • iOS ver 13.5.1

http接続できるように設定

  1. はじめにhttp接続できるようにInfo.plistに設定を追加します。
    image.png
  2. App Transport Security SettingのAllow Arbitrary LoadsをYESにします。
    image.png
    (もしApp Transport Security Settingが表示されていなければ追加すれば大丈夫です。)

iOSシュミレータから接続する方法

iOSシュミレータから接続するだけならHTTPリクエストするURLはhttp://localhost (ポート)で大丈夫です。

iOS実機から接続する方法

iOS実機から接続するにはiOSシュミレータと同じhttp://localhost (ポート)では接続できません。
localhostの部分をMacのIPアドレスにする必要があります。
MacのIPアドレスの確認方法は以下になります。(すみません、言語設定が英語になっています。)
1. Macの設定
image.png
2. ネットワーク
image.png
3. 接続されているネットワークのIPアドレス(赤い部分がMacのIPアドレスになります。)
image.png

まとめ

意外に簡単に接続することができました。
これでサーバサイドのプログラムをサーバにアップしなくても連携を確認することができます。

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

Swiftで楽天レシピAPIを表示させてみた。

やること

Swiftで楽天APIをデバッグエリア表示させる。

完成形

スクリーンショット 2020-07-27 17.58.36.png

APIからデータを取得する

今回使ったAPI
楽天レシピカテゴリ別ランキングAPI
(楽天IDが必須になります)

{
 result: [
  {
   foodImageUrl:
  "https://image.space.rakuten.co.jp/d/strg/ctrl/3/34d4ce95b8674c8fb6c8f08b5115464a9f180c31.17.2.3.2.jpg",
   recipeDescription: "小鉢がもう1品ほしいなっていう時に簡単でオススメです。",
   recipePublishday: "2011/08/22 19:04:07",
   shop: 0,
   pickup: 1,
   recipeId: 1200002403,
   nickname: "JIMA88",
   smallImageUrl: 
  "https://image.space.rakuten.co.jp/d/strg/ctrl/3/34d4ce95b8674c8fb6c8f08b5115464a9 f180c31.17.2.3.2.jpg?thum=55",
  recipeMaterial: [
    "きゅうり",
    "ごま油",
    "すりごま",
    "鶏ガラスープのもと",
    "ビニール袋"
   ],
   recipeIndication: "5分以内",
   recipeCost: "100円以下",
   rank: "1",
   recipeUrl: "https://recipe.rakuten.co.jp/recipe/1200002403/",
   mediumImageUrl: 
  "https://image.space.rakuten.co.jp/d/strg/ctrl/3/34d4ce95b8674c8fb6c8f08b5115464a9f180c31.17.2.3.2.jpg?thum=54",
   recipeTitle: "1分で!うまうま胡麻キュウリ"
 }

Codableを使ってJSONを変換させる

参考にした本
参考にした動画
参考にした記事

今回は上記の楽天レシピAPIから記事のフードイメージurlとレシピタイトルを取得しました。

struct ResultList: Codable {

    let result: [User]

struct User: Codable {
    let foodImageUrl :String
    let recipeTitle  :String
}
}

APIをURLSessionを使って叩く

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
      getRApi()
    }

     private func getRApi(){
            guard let url = URL(string: "ここに楽天APIのURLを") else {return}

            let task = URLSession.shared.dataTask(with: url) { (data, response, err)in
                if let err = err {
                    print("情報の取得に失敗しました。:", err)
                    return
                }
                if let data = data{
                    do{
                        let resultList = try JSONDecoder().decode(ResultList.self, from: data)
                        print("json: ", resultList)
                    }catch(let err){
                         print("情報の取得に失敗しました。:", err)
                    }
                }
            }
            task.resume()
        }
    }

実際にデバッグエリア

スクリーンショット 2020-07-26 14.34.38.png

無事表示されました。
次はtableviewに表示させてみたいと思います!

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

超簡単にViewの背景をぼかす方法【iOS開発】

背景をぼかしたい!!

背景をぼかしたいなーと思った時にはUIVisualEffectViewを使うと思うんですけど、ぼかす処理を毎回書くのがめんどくさい!!
というわけで超簡単に背景ぼかしができるextensionを作りましょう

extensionを書く!

新しいファイルを作ってこれを書きましょう!

addVisualEffect.swift
import UIKit

extension UIView {
    func addVisualEffect() {
        //スクリーンサイズ取得!
        let width = UIScreen.main.bounds.size.width
        let height = UIScreen.main.bounds.size.height

        //ブラーエフェクト生成!
        let blurEffect = UIBlurEffect(style: .light)

        //ブラーエフェクトからエフェクトビューを生成!
        let visualEffectView = UIVisualEffectView(effect: blurEffect)

        //エフェクトビューのサイズ指定!
        visualEffectView.frame = CGRect(x: 0, y: 0, width: width, height: height)

        //もとのViewにaddSubView!!
        self.addSubview(visualEffectView)

        //重ね順を一番下に!!
        self.sendSubviewToBack(visualEffectView)
    }
}

ポイントはサイズをしっかり合わせるところと重ね順を一番下にすることですかね!

あ、そういえばブラーエフェクトには種類があるんですよ
(ここに詳しく書いてありました! -> https://dev.classmethod.jp/references/ios8-uivisualeffectview/
extraLightとlightとdartの3種類です!
だから引数を指定してあげればもっと使い勝手良くなる気がします
僕はlightしか使わないのでこれでいきます!

実際に使う!

ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.addVisualEffect()
    }

たったこれだけで背景をぼかすことができるんです!!便利!!

最後に

メモ程度の記事に最後までつきあってくれてありがとうございます
是非使ってみてください!!

環境:Xcode11.6(11E708)

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

【SwiftUI】VectorArithmetic を自作して AnimatablePair の型パラメータ地獄から解脱する

はじめに

モチベーション

SwiftUI でアニメーション可能なシェイプを作成する場合は animatableData を実装する必要があります。

アニメーション可能なパラメータが1つの場合は次のような実装をします。

public struct HogeShape: Shape {
    public var foo: CGFloat

    public var animatableData: CGFloat {
        get { foo }
        set { foo = newValue }
    }

    public func path(in rect: CGRect) -> Path {
        // foo をパラメータにした Path を作る
    }
}

animatableData の型は必ずしも CGFloat である必要はなく、VectorArithmetic に適合していれば OK です。次の型が既知で適合しています。

  • CGFloat
  • Double
  • Float
  • AnimatablePair
  • EmptyAnimatableData

AnimatablePair を利用することで、次のようにパラメータを2つ指定することができます。

public struct FugaShape: Shape {
    public var foo: CGFloat
    public var bar: CGFloat

    public var animatableData: AnimatablePair<CGFloat, CGFloat> {
        get {
            AnimatablePair(foo, bar)
        }
        set {
            foo = newValue.first
            bar = newValue.second
        }
    }

    public func path(in rect: CGRect) -> Path {
        // foo, bar をパラメータにした Path を作る
    }
}

AnimatablePair の型パラメータには、任意の VectorArithmetic を指定することが可能なので、型パラメータに AnimatablePair を指定することができます。

これを利用してパラメータを3つにすることができます。

public struct PiyoShape: Shape {
    public var foo: CGFloat
    public var bar: CGFloat
    public var baz: CGFloat

    public var animatableData: AnimatablePair<CGFloat, AnimatablePair<CGFloat, CGFloat>> {
        get {
            .init(foo, .init(bar, baz))
        }
        set {
            foo = newValue.first
            bar = newValue.second.first
            baz = newValue.second.second
        }
    }

    public func path(in rect: CGRect) -> Path {
        // foo, bar, baz をパラメータにした Path を作る
    }
}

AnimatablePair の型パラメータに AnimatablePair を設定していくことで、いくらでもアニメーション可能なプロパティを追加することができますが、型パラメータがネストされていくので可読性が落ちていきます。パラメータを5つにすると次のようになります。

var animatableData: AnimatablePair<CGFloat, AnimatablePair<CGFloat, AnimatablePair<CGFloat, AnimatablePair<CGFloat, CGFloat>>>> {
        get {
            .init(prop0, .init(prop1, .init(prop2, .init(prop3, prop4))))
        }
        set {
            prop0 = newValue.first
            prop1 = newValue.second.first
            prop2 = newValue.second.second.first
            prop3 = newValue.second.second.second.first
            prop4 = newValue.second.second.second.second
        }
    }

プロパティが5つあることが、現実の解決すべき問題として多いか少ないかについては何とも言えないところですが、単純なシェイプでも柔軟にアニメーションをつけようとすると、プロパティの数はそれなりに必要になってくると思います。

たとえば、角を丸くした長方形のシェイプで考えてみます。
角丸の半径に加えて、各四隅の角丸の有無を自由に制御できるようにすると、単純なシェイプでも5つのプロパティが必要になります。
これらが自由に制御できれば、ビューのマスク処理にアニメーションをつけることも容易になります。

本記事では、VectorArithmetic に適合した AnimatableValues を自作することで、上記のネストを次のようなシンプルな実装にします。

public var animatableData: AnimatableValues {
    get {
        .init(prop0, prop1, prop2, prop3, prop4)
    }
    set {
        let values: [CGFloat] = newValue.values()
        prop0 = values[0]
        prop1 = values[1]
        prop2 = values[2]
        prop3 = values[3]
        prop4 = values[4]
    }
}

環境

  • Xcode 11.6 (11E708)
  • Swift 5.2.4

VectorArithmetic を実装する

アニメーション可能なプロパティをフラットに複数保持するために Array で値を管理します。

public struct AnimatableValues: VectorArithmetic {
    private var values: [Double]
}

あとは、VectorArithmetic に適合するために必要な実装をします。

必要最低限の実装

AnimatableValuesVectorArithmetic に適合するためには、ベクトル空間の公理系を満たす必要があり、以下のように実装します。

zero

static var zero: Self { get }

ゼロ元を返す必要があります。ゼロ元はどのような AnimatableValues と加算してもプロパティを変更しないように実装します。

+ 演算子

static func + (lhs: Self, rhs: Self) -> Self

各要素ごとに加算したオブジェクトを返す必要があります。

- 演算子

static func - (lhs: Self, rhs: Self) -> Self

各要素ごとに減算したオブジェクトを返す必要があります。

scale(by:)

mutating func scale(by rhs: Double)

各要素ごとに rhs 倍したオブジェクトを返す必要があります。

magnitudeSquared

var magnitudeSquared: Double { get }

各要素の二乗した和を返します。

実装例

上記を満たすような実装例です。

固定長のリストであれば .zero[0, 0, ..., 0] のような実装が望ましいところですが、Array で保持している都合で実現できないので、空のリスト [] で代替しておき、+, - 演算を実行するタイミングでゼロ埋めしているのが実装のポイントです。これで公理系を満たすことができます。

import SwiftUI
import enum Accelerate.vDSP

public struct AnimatableValues: VectorArithmetic, Hashable {
    private var values: [Double]

    public init(values: [Double]) {
        self.values = values
    }

    public init<F: BinaryFloatingPoint>(values: [F]) {
        self.init(values: values.map(Double.init))
    }

    public init<F: BinaryFloatingPoint>(_ values: F...) {
        self.init(values: values.map(Double.init))
    }

    public func values<F: BinaryFloatingPoint>(_ type: F.Type = F.self) -> [F] {
        values.map(F.init)
    }
}

// MARK: - VectorArithmetic
public extension AnimatableValues {
    mutating func scale(by rhs: Double) {
        values = vDSP.multiply(rhs, values)
    }

    var magnitudeSquared: Double {
        vDSP.sum(vDSP.multiply(values, values))
    }
}

// MARK: - AdditiveArithmetic
public extension AnimatableValues {
    static var zero: Self {
        .init(values: [])
    }

    static func + (lhs: Self, rhs: Self) -> Self {
        .init(values: operate(vDSP.add, lhs, rhs))
    }

    static func - (lhs: Self, rhs: Self) -> Self {
        .init(values: operate(vDSP.subtract, lhs, rhs))
    }

    private static func operate(_ operation: ([Double], [Double]) -> [Double], _ lhs: Self, _ rhs: Self) -> [Double] {
        let count = max(lhs.values.count, rhs.values.count)
        let lhs = lhs.values + Array(repeating: 0, count: count - lhs.values.count)
        let rhs = rhs.values + Array(repeating: 0, count: count - rhs.values.count)
        return operation(lhs, rhs)
    }
}

この実装によって、animatableData は次のように簡潔に記述できるようになります。

public var animatableData: AnimatableValues {
    get {
        .init(prop0, prop1, prop2, prop3, prop4)
    }
    set {
        let values: [CGFloat] = newValue.values()
        prop0 = values[0]
        prop1 = values[1]
        prop2 = values[2]
        prop3 = values[3]
        prop4 = values[4]
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dateを扱うときは12時間表記設定を考慮しよう。

要約

  • カスタムフォーマットの日時文字列からDate変換時にnilが返却される。
  • シミュレータでは発生しない、実機で発生する
  • 国内のみのサービスでもDateFormatterにはLocaleは必ず設定し利用する。
  • 可能であれば実機利用のテストのタイミングに組み込む

現象

文字列からDate変換するときに、端末の設定が12時間表記になっていると必ずnilが発生。
シミュレータでは発生しないので、忘れがち。

問題になるパターン

変換文字列 24時間 12時間 シミュレータ
時刻を含む文字列 成功 失敗(nil) 成功
時刻を含まない文字列 成功 成功 成功

参考 問題になるコード

let dateFormmater = DateFormatter()
dateFormmater.dateFormat = "yyyy-MM-dd HH:mm:ss"

/// 12時間表記の際に date == nil となる.
let date = dateFormmater.date(from: "2020-03-02 10:00:00")

対処方法

Formatter設定時Localeを設定する

実機でもdateFormatterにはLocaleが設定されているものの、localeを改めて設定する必要がある。

let dateFormmater = DateFormatter()
dateFormmater.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormmater.locale = Locale(identifier: "en_US_POSIX")

/// 12時間表記の際でもnilとならない。
let date = dateFormmater.date(from: "2020-03-02 10:00:00")

忘れがちなのでExtenstionにしてしまう.

状況によって固定値も含めてしまうとよい。

public extension DateFormatter {

    static var standard: DateFormatter {
        let standard = DateFormatter()
        standard.dateFormat = "yyyy-MM-dd HH:mm:ss"
        standard.locale = Locale(identifier: "en_US_POSIX")
        return standard
    }
}

コードで強力に制約をするのであれば、DateFormatterをwrapするのが良さそう。

おまけ

海外対応時のメッセージの受信時刻表示で必要なこと

  • 時刻データのタイムゾーンとクライアントのタイムゾーンを考慮する
  • 時刻表記のフォーマットを考慮する(ここでは触れていない)
let dateFormmater = DateFormatter()
dateFormmater.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormmater.locale = Locale(identifier: "en_US_POSIX")
/// 時刻データのタイムゾーンでDateへ変換.
dateFormatter.timeZone = TimeZone(abbreviation: "JST")
let date = dateFormmater.date(from: "2020-03-02 10:00:00")

/// 端末のタイムゾーンで文字列に変換.
dateFormatter.timeZone = TimeZone.current
let dateString = dateFormatter.string(from: date!)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

<Swift>RxDataSourcesでヘッダ付きのUICollectionViewを実装

完成イメージ

パターン1(全てのセクションで同じ名前、同じデータ形式を表示する場合)

TodolistDetailViewModel.swift
import Foundation
import RxSwift
import RxCocoa
import RxDataSources

struct SectionOfTodolist {
    // headerはSectionModelTypeに設定されない。
    // 任意の文字列を設定しておき、ヘッダーセルを作るときに参照するために定義。
    // 参照の仕方:dataSource.sectionModels[indexPath.section].header
    var header: String
    var items: [Item]
}

extension SectionOfTodolist: SectionModelType {
    typealias Item = Todo
    init(original: SectionOfTodolist, items: [SectionOfTodolist.Item]) {
        self = original
        self.items = items
    }
}

class TodolistDetailViewModel {
    let items = BehaviorRelay<[SectionOfTodolist]>(value: [])

    func updateItems() {
        let todolist = getTodolist()
        let sections = convertToSectionModels(srcArray: todolist.todoArray)
        items.accept(sections)
    }

    private func getTodolist() -> Todolist {
        // ダミーデータ
        var todoArray: [Todo] = []
        todoArray.append(Todo(title: "テストTODO1", expireDate: Date()))
        todoArray.append(Todo(title: "テストTODO2", expireDate: Date()))
        todoArray.append(Todo(title: "テストTODO3", expireDate: Date()))
        let ret = Todolist(name: "ダミーTODOリスト", color: .blue, todoArray: todoArray)
        return ret
    }

    private func convertToSectionModels(srcArray: [Todo]) -> [SectionOfTodolist] {
        let section =
            SectionOfTodolist(header: "test header"
                , items: srcArray)
        let section2 =
                    SectionOfTodolist(header: "test header2"
                        , items: srcArray)
        return [section, section2]
    }
}
TodolistDetailViewController.swift
import UIKit
import RxSwift
import RxDataSources

class TodolistDetailViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!

    private lazy var dataSource = RxCollectionViewSectionedReloadDataSource<SectionOfTodolist>(
    configureCell: configureCell
    ,configureSupplementaryView: titleForHeaderInSection)

    // カスタムセルを設定
    private lazy var configureCell: RxCollectionViewSectionedReloadDataSource<SectionOfTodolist>.ConfigureCell = { [weak self] (_, collectionView, indexPath, todo) in
        guard let strongSelf = self else { return UICollectionViewCell() }
            return strongSelf.todolistCell(indexPath: indexPath, todo: todo)
    }

    // セクションヘッダータイトルを設定
    private lazy var titleForHeaderInSection: RxCollectionViewSectionedReloadDataSource<SectionOfTodolist>.ConfigureSupplementaryView = { [weak self] (dataSource, collectionView, kind, indexPath) in
        guard let strongSelf = self else { return UICollectionReusableView() }
        return strongSelf.headerCell(indexPath: indexPath, kind: kind)
    }

    private var viewModel: TodolistDetailViewModel!

    private let todolistId: Int

    private let disposeBag = DisposeBag()

    init(todolistId: Int) {
        self.todolistId = todolistId
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupViewController()
        setupCollectionView()
        setupViewModel()
    }
}

extension TodolistDetailViewController {
    private func setupViewController() {
        self.navigationItem.title = "ダミーTODOリスト"
        self.view.backgroundColor = .systemBlue
    }

    private func setupCollectionView() {
        collectionView.contentInset.top = TodolistDetailCollectionViewCell.cellMargin
        collectionView.register(UINib(nibName: "TodolistDetailCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "TodolistDetailCollectionViewCell")
        collectionView.register(
            UICollectionReusableView.self
            , forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader
            , withReuseIdentifier: "Section")
        collectionView.rx.setDelegate(self).disposed(by: disposeBag)

        // ヘッダサイズを指定
        let flowLayout = UICollectionViewFlowLayout()
        flowLayout.headerReferenceSize = CGSize(width: self.collectionView.frame.width, height: 40)
        collectionView.collectionViewLayout = flowLayout

        // タップ時のイベント
        collectionView.rx.itemSelected
            .map { [weak self] indexPath -> Todo? in
                return self?.dataSource[indexPath]
            }
            .subscribe(onNext: { [weak self] item in
                guard let item = item else { return }
                    self?.presentTodoDetaillViewController()
            })
            .disposed(by: disposeBag)
    }

    private func setupViewModel() {
        viewModel = TodolistDetailViewModel()
        viewModel.items
            .asDriver()
            .drive(collectionView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)
        viewModel.updateItems()
    }

    private func todolistCell(indexPath: IndexPath, todo: Todo) -> UICollectionViewCell {
        if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TodolistDetailCollectionViewCell", for: indexPath ) as? TodolistDetailCollectionViewCell{
            cell.update(todoTitle: todo.title, todoExpireDate: todo.expireDate)

            // チェックボックスタップ時
            let checkButtonObservable = cell.checkButton.rx.tap.asObservable()
            checkButtonObservable.subscribe(
                onNext: { [weak self] in
                    cell.isCheck = !cell.isCheck
                    let image = cell.isCheck ? UIImage(systemName: "checkmark.square") : UIImage(systemName: "square")
                    cell.checkButton.setImage(image, for: .normal)
                }
            ).disposed(by: cell.disposeBag)

            return cell
        }
        return UICollectionViewCell()
    }

    private func headerCell(indexPath: IndexPath, kind: String) -> UICollectionReusableView {
        let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Section", for: indexPath)
        headerView.backgroundColor = .yellow
        let label = UILabel(frame: CGRect(x:0, y:0, width:self.collectionView.frame.width, height:20))
        label.textColor = UIColor.black
        label.textAlignment = .center
        label.text = dataSource.sectionModels[indexPath.section].header
        headerView.addSubview(label)
        return headerView
    }

    private func presentTodoDetaillViewController() {
        let todoId = 0
        let vc = TodoViewController(todoId: todoId)
        self.navigationController?.pushViewController(vc, animated: true)
    }
}

extension TodolistDetailViewController: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        let item = dataSource[section]
            return UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let item = dataSource[indexPath]
            let width = collectionView.bounds.width - (TodolistDetailCollectionViewCell.cellMargin * 2)
            return CGSize(width: width, height: TodolistDetailCollectionViewCell.cellHeight)
    }
}

■参考にしたサイト
http://m.hangge.com/news/cache/detail_2005.html
https://www.jianshu.com/p/8b9cb8d57edd

パターン2(セルに表示するデータ形式をセクションによって変える場合)

※例:(パターン1からの差分のみ記載)

TodolistDetailViewModel.swift
extension SectionOfTodolist: SectionModelType {
    // <変更>
    typealias Item = TodolistItem
    ...省略
}

// <追加>
enum TodolistItem {
    case todo(todo: Todo)
    case memo(memo: Memo)
}

class TodolistDetailViewModel {
    ...省略
    func updateItems() {
        let todolist = getTodolist()
        // <変更>
        let sections1 = convertTodoToSectionModels(srcArray: todolist.todoArray)
        let sections2 = convertMemoToSectionModels(srcArray: memolist.memoArray)
        items.accept([sections1, sections2])
    }

    // <追加>
    private func convertTodoToSectionModel(srcArray: [Todo]) -> SectionOfTodolist {
        let items = srcArray.map {
            TodolistItem.todo(todo: $0)
        }
        let section =
            SectionOfTodolist(header: "TODOカテゴリー"
                , items: items)

        return section
    }

    // <追加>
    private func convertMemoToSectionModel(srcArray: [Memo]) -> SectionOfTodolist {
        let items = srcArray.map {
            TodolistItem.memo(memo: $0)
        }
        let section =
            SectionOfTodolist(header: "MEMOカテゴリー"
                , items: items)
        return section
    }
}
TodolistDetailViewController.swift
class TodolistDetailViewController: UIViewController {
...省略...
    // カスタムセルを設定
    private lazy var configureCell: RxCollectionViewSectionedReloadDataSource<SectionOfTodolist>.ConfigureCell = { [weak self] (_, collectionView, indexPath, item) in
        guard let strongSelf = self else { return UICollectionViewCell() }
        switch item {
        // <変更>
        case .todo(let todo):
            return strongSelf.todolistCell(indexPath: indexPath, todo: todo)
        case .memo(let memo):
            return strongSelf.todolistCell(indexPath: indexPath, todo: memo)
        }
    }
...省略
    private func setupCollectionView() {
...省略
        // タップ時のイベント
        collectionView.rx.itemSelected
            // <変更>
            .map { [weak self] indexPath -> TodolistItem? in
                return self?.dataSource[indexPath]
            }
            .subscribe(onNext: { [weak self] item in
                guard let item = item else { return }
                switch item {
                // <変更>
                case .todo(let todo):
                    self?.presentTodoDetaillViewController()
                case .memo(let memo):
                    self?.presentTodoDetaillViewController()
                }
            })
            .disposed(by: disposeBag)
    }
...省略
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let item = dataSource[indexPath]
        switch item {
        // <変更>
        case .todo:
            let width = collectionView.bounds.width - (TodolistDetailCollectionViewCell.cellMargin * 2)
            return CGSize(width: width, height: TodolistDetailCollectionViewCell.cellHeight)
        case .memo:
            let width = collectionView.bounds.width - (TodolistDetailCollectionViewCell.cellMargin * 2)
            return CGSize(width: width, height: TodolistDetailCollectionViewCell.cellHeight)
        }
    }
}

パターン3(セルに表示するデータ形式をセクションによって変える + セクションによってヘッダのタイトルを変える場合)

※例(パターン2からの差分のみ記載)

TodolistDetailViewModel.swift
struct SectionOfTodolist {
    // 変更
    var model: TodolistSection
    var items: [Item]
}

// 追加
enum TodolistSection {
    case category(name: String)
}

class TodolistDetailViewModel {
    ...省略
    private func convertTodoToSectionModel(srcArray: [Todo]) -> SectionOfTodolist {
        let items = srcArray.map {
            TodolistItem.todo(todo: $0)
        }
        // <変更>
        let section = SectionOfTodolist(model: .category(name: "TODOカテゴリー"), items: items)
        return section
    }

    private func convertMemoToSectionModel(srcArray: [Memo]) -> SectionOfTodolist {
        let items = srcArray.map {
            TodolistItem.memo(memo: $0)
        }
        // <変更>
        let section = SectionOfTodolist(model: .category(name: "MEMOカテゴリー"), items: items)
        return section
    }
}
TodolistDetailViewController.swift
class TodolistDetailViewController: UIViewController {
...省略...
    // セクションヘッダータイトルを設定
    private lazy var titleForHeaderInSection: RxCollectionViewSectionedReloadDataSource<SectionOfTodolist>.ConfigureSupplementaryView = { [weak self] (dataSource, collectionView, kind, indexPath) in

        guard let strongSelf = self else { return UICollectionReusableView() }
        // <変更>
        let sectionOfTodolist = dataSource[indexPath.section]
        switch sectionOfTodolist.model {
        case .category:
            return strongSelf.headerCell(indexPath: indexPath, kind: kind)
        }
    }

    private func headerCell(indexPath: IndexPath, kind: String) -> UICollectionReusableView {

        let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath)
        headerView.backgroundColor = .yellow
        let label = UILabel(frame: CGRect(x:0, y:0, width:self.collectionView.frame.width, height:20))
        label.textColor = UIColor.black
        label.textAlignment = .center

        // <変更>
        let sectionOfTodolist = dataSource[indexPath.section]
        switch sectionOfTodolist.model {
        case .category(let name):
            label.text = name
        }
        headerView.addSubview(label)
        return headerView
    }
}

extension TodolistDetailViewController: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        // <変更>
        let sectionOfTodolist = dataSource[section]
        switch sectionOfTodolist.model {
        case .category:
            return UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
        }
    }
}

パターン4(パターン3をSectionModelでやる場合)

※例(パターン3からの差分のみ記載)

TodolistDetailViewModel.swift
// 削除
//struct SectionOfTodolist {
//    var header: String
//    var items: [Item]
//}
//
//extension SectionOfTodolist: SectionModelType {
//    typealias Item = TodolistItem
//
//    init(original: SectionOfTodolist, items: [SectionOfTodolist.Item]) {
//        self = original
//        self.items = items
//    }
//}

// 追加
typealias SectionOfTodolist = SectionModel<TodolistSection, TodolistItem>

補足:SectionModelTypeとSectionModelの違い

RxDataSources/TodolistDetailViewModel.swift
public protocol SectionModelType {
    associatedtype Item

    var items: [Item] { get }

    init(original: Self, items: [Item])
}
RxDataSources/SectionModel.swift
public struct SectionModel<Section, ItemType> {
    public var model: Section
    public var items: [Item]

    public init(model: Section, items: [Item]) {
        self.model = model
        self.items = items
    }
}

extension SectionModel
    : SectionModelType {
    public typealias Identity = Section
    public typealias Item = ItemType

    public var identity: Section {
        return model
    }
}

extension SectionModel
    : CustomStringConvertible {

    public var description: String {
        return "\(self.model) > \(items)"
    }
}

extension SectionModel {
    public init(original: SectionModel<Section, Item>, items: [Item]) {
        self.model = original.model
        self.items = items
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む