- 投稿日:2020-01-19T22:35:36+09:00
【Swift】UICollectionViewCompositionalLayoutを使って数種類のカスタムレイアウトを実装してみる
はじめに
WWDC2019で新たに発表された、UICollectionViewのレイアウト手法であるUICollectionViewCompositionalLayoutを使っていくつかカスタムレイアウトを作ってみました。
※ iOS13以降の環境にて、今回作成したサンプルを動かせますUICollectionViewCompositionalLayoutとは
詳細については、WWDCのセッションを参考にして頂ければと思いますが、ざっくり言うと、
- iOS13からUICollectionViewCompositionalLayoutが登場したことで、UICollectionViewFlowLayoutやUICollectionViewDelegateFlowLayoutに加えて、UICollectionViewのレイアウトを定義する方法が1つ増えた。
- iOS13からUICollectionViewDiffableDataSourceが登場したことで、UICollectionViewDataSourceを準拠して行なっていたDataSource管理の方法が増えた。(こちらについては本記事では解説していません)
※ UICollectionViewDataSourceのみでDataSource管理を行なった場合でも、UICollectionViewCompositionalLayoutは使用できます。
※ より詳細な情報については、こちらの記事がかなり詳しくまとめてくださっています。
https://qiita.com/shiz/items/a6032543a237bf2e1d19階層構造
UICollectionViewCompositionalLayoutにおける階層構造は、Item、Group、Sectionなどの概念から成り立っています。
実装するレイアウト
今回はUICollectionViewCompositionalLayoutを使用して、3種類のレイアウトを実装してみました。
1. グリッド形式(3×n)
このレイアウトは、赤枠で囲んだ部分のグループを用意し、×nの形式で表示させる属性を持たせたセクションを用意します。
SectionType.swiftprivate func gridSection(collectionViewBounds: CGRect) -> NSCollectionLayoutSection { let itemCount = 3 // 横に並べる数 let lineCount = itemCount - 1 let itemSpacing = CGFloat(1) // セル間のスペース let itemLength = (collectionViewBounds.width - (itemSpacing * CGFloat(lineCount))) / CGFloat(itemCount) // 1つのitemを生成 // .absoluteは固定値で指定する方法 let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(itemLength), heightDimension: .absolute(itemLength))) // itemを3つ横並びにしたグループを生成 // .fractional~は親Viewとの割合 let items = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)), subitem: item, count: itemCount) // グループ内のitem間のスペースを設定 items.interItemSpacing = .fixed(itemSpacing) // 生成したグループ(items)が縦に並んでいくグループを生成(実質これがセクション) let groups = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(itemLength)), subitems: [items]) // 用意したグループを基にセクションを生成 // 基本的にセルの数は意識しない、セルが入る構成(セクション)を用意しておくだけで勝手に流れてく let section = NSCollectionLayoutSection(group: groups) // セクション間のスペースを設定 section.interGroupSpacing = itemSpacing return section }必要なセクション(NSCollectionLayoutSection)を用意したら、それを基にUICollectionViewCompositionalLayoutを生成できます。
UICollectionViewCompositionalLayout(section: 生成したSection)あとは用意したUICollectionViewCompositionalLayoutクラスをCollectionViewに割り当てます。
ViewController.swiftcollectionView.collectionViewLayout = 用意したUICollectionViewCompositionalLayout // 既に構成されているレイアウトを更新する場合は、invalidateLayout()を呼びます。 collectionView.collectionViewLayout.invalidateLayout()2. 異なるサイズのItem併用 (Instagram風)
このレイアウトは、緑枠で囲った各グループを用意して、それらを結合した1つのセクションを用意します。
SectionType.swiftprivate func largeAndSmallSquareSection(collectionViewBounds: CGRect) -> NSCollectionLayoutSection { let itemSpacing = CGFloat(2) // セル間のスペース // 小itemが縦に2つ並んだグループ let itemLength = (collectionViewBounds.width - (itemSpacing * 2)) / 3 let largeItemLength = itemLength * 2 + itemSpacing let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(itemLength), heightDimension: .absolute(itemLength))) let verticalItemTwo = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(itemLength), heightDimension: .absolute(largeItemLength)), subitem: item, count: 2) verticalItemTwo.interItemSpacing = .fixed(itemSpacing) // 大item + 小item*2 のグループ let largeItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(largeItemLength), heightDimension: .absolute(largeItemLength))) let largeItemLeftGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(largeItemLength)), subitems: [largeItem, verticalItemTwo]) largeItemLeftGroup.interItemSpacing = .fixed(itemSpacing) let largeItemRightGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(largeItemLength)), subitems: [verticalItemTwo, largeItem]) largeItemRightGroup.interItemSpacing = .fixed(itemSpacing) // 小ブロックが縦に2つ並んだグループを横に3つ並べたグループ let twoThreeItemGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(largeItemLength)), subitem: verticalItemTwo, count: 3) twoThreeItemGroup.interItemSpacing = .fixed(itemSpacing) // 各グループを縦に並べたグループ let subitems = [largeItemLeftGroup, twoThreeItemGroup, largeItemRightGroup, twoThreeItemGroup] let groupsSpaceCount = CGFloat(subitems.count - 1) let heightDimension = NSCollectionLayoutDimension.absolute(largeItemLength * CGFloat(subitems.count) + (itemSpacing * groupsSpaceCount)) // MEMO: 高さの計算は後に追加するスペース分も足す let groups = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: heightDimension), subitems: subitems) groups.interItemSpacing = .fixed(itemSpacing) let section = NSCollectionLayoutSection(group: groups) section.interGroupSpacing = itemSpacing return section }同様に、セクションを基にUICollectionViewCompositionalLayoutを用意してCollectionViewのレイアウトに割り当てます。
3. 複数Section併用 (Netflix風)
Appleが公式で公開しているサンプルをみた感じ、「ヘッダーを付与する」や「itemを横スクロールさせる」といった実装は、セクション(NSCollectionLayoutSection)を対象に振る舞いを指定する方法が一般的なようなので、上記レイアウトを実装する場合は、「複数のセクションを用意して実行時に渡ってくるindexPath.sectionの値に合わせて適切なセクションを返す」という流れで実装してみました。
SectionType.swift/// 縦長の長方形が1つだけのセクション private func verticalRectangleSection(collectionViewBounds: CGRect) -> NSCollectionLayoutSection { let verticalRectangleHeight = collectionViewBounds.height * 0.7 let verticalRectangleItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))) let verticalRectangleGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(verticalRectangleHeight)), subitem: verticalRectangleItem, count: 1) return NSCollectionLayoutSection(group: verticalRectangleGroup) } /// 縦長の長方形が横スクロールするセクション(ヘッダー付き) private func rectangleHorizonContinuousWithHeaderSection(collectionViewBounds: CGRect) -> NSCollectionLayoutSection { let headerHeight = CGFloat(50) let headerElementKind = "header-element-kind" let insetSpacing = CGFloat(5) let rectangleItemWidth = collectionViewBounds.width * 0.9 / 3 let rectangleItemHeight = rectangleItemWidth * (4/3) let rectangleItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))) let horizonRectangleGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(rectangleItemWidth), heightDimension: .absolute(rectangleItemHeight)), subitem: rectangleItem, count: 1) horizonRectangleGroup.contentInsets = NSDirectionalEdgeInsets(top: insetSpacing, leading: insetSpacing, bottom: insetSpacing, trailing: insetSpacing) let horizonRectangleContinuousSection = NSCollectionLayoutSection(group: horizonRectangleGroup) let sectionHeaderItem = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(headerHeight)), elementKind: headerElementKind, alignment: .top) sectionHeaderItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: insetSpacing, bottom: 0, trailing: insetSpacing) horizonRectangleContinuousSection.boundarySupplementaryItems = [sectionHeaderItem] // セクションに対してヘッダーを付与 horizonRectangleContinuousSection.orthogonalScrollingBehavior = .continuous // セクションに対して横スクロール属性を付与 horizonRectangleContinuousSection.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: insetSpacing, bottom: 0, trailing: insetSpacing) return horizonRectangleContinuousSection } /// 正方形が1つだけのセクション(ヘッダー付き) private func squareWithHeaderSection(collectionViewBounds: CGRect) -> NSCollectionLayoutSection { let itemLength = collectionViewBounds.width let headerHeight = CGFloat(50) let headerInsetSpacing = CGFloat(10) let headerElementKind = "header-element-kind" let squareItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))) let squareGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(itemLength)), subitem: squareItem, count: 1) let squareSection = NSCollectionLayoutSection(group: squareGroup) let sectionHeaderItem = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(headerHeight)), elementKind: headerElementKind, alignment: .top) sectionHeaderItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: headerInsetSpacing, bottom: headerInsetSpacing, trailing: 0) squareSection.boundarySupplementaryItems = [sectionHeaderItem] return squareSection }用意した3種類のセクションをindexPath.sectionに合わせて返します。
※ CollectionView側のセクション数は別途指定しています。(今回は4)各セクションをindexPath.sectionに合わせて返す場合は、UICollectionViewCompositionalLayoutのinit時に以下のイニシャライザを使います。
UICollectionViewCompositionalLayout.hpublic init(sectionProvider: @escaping UICollectionViewCompositionalLayoutSectionProvider) public init(sectionProvider: @escaping UICollectionViewCompositionalLayoutSectionProvider, configuration: UICollectionViewCompositionalLayoutConfiguration)実装例
let layout = UICollectionViewCompositionalLayout { (section, _) -> NSCollectionLayoutSection? in let layoutSection = 渡ってきたsectionに応じたNSCollectionLayoutSectionを返す return layoutSection } return layout上記の流れで、用意した3種類のセクションを4つのsectionに対してそれぞれ適したものを返すことで、レイアウトを組みます。
今回のsectionに対するセクション順は以下。0: 縦長の長方形が1つだけのセクション
1: 縦長の長方形が横スクロールするセクション(ヘッダー付き)
2: 縦長の長方形が横スクロールするセクション(ヘッダー付き)
3: 正方形が1つだけのセクション(ヘッダー付き)※ 「縦長の長方形が1つだけのセクション」は、Netflix風を意識して、他のセルと違うセルクラスを使用しています)
ソースコード
今回作成したサンプルのソースは以下のリポジトリにあります。
https://github.com/ddd503/CompositionalLayouts-Sample
- 投稿日:2020-01-19T18:39:58+09:00
FirebaseのRemoteConfigを使ってキャンペーンのようなモーダルを表示させたい[RemoteConfig応用編]
最初に
このQiita記事は、RemoteConfig初級編の応用編として書かれているため、RemoteConfig初級編がまだの方はこちらからお読みください。
やりたいこと
応用編では、初級編でやったことを用いてLabelと画像の値を変更してモーダルで表示することをします。
完成形の画面はこんな感じだよ!
RemoteConfigの設定
画像を参考にしながら、FirebaseのRemoteConfigの画面から、任意のパラメータキー(card_key)デフォルト値に以下のjsonを入力します。
RemoteConfig{ "title": "企画のタイトル!!!", "image_url": "https://ocadweb.com/wp-content/uploads/2016/03/32.png", "details": "ここには、企画等の詳細情報を記入してください。", "url": "https://picable.co.jp", "identifier": "20200112" }storyboardファイルで必要なパーツを配置しよう。
storyboardファイルを生成して、StoryboardIDに"CardRemoteConfigViewController"と記入します。
デザインは、以下の画像を参考にして配置してみてください。
タイトル、画像、内容のテキスト、アンケート等に遷移させるボタン、閉じるボタンを配置しています。
背景は、黒の透明度60%
画像:今回は、300×200の比率の画像を利用しています。コーディング
はじめに、RemoteConfigからjsonのデータを受け取るEntityを作ります。
CardRemomteConfigEntity.swiftstruct CardRemomteConfigEntity: Codable { let title: String let url: String let details: String let imageURL: String? let identifier: String enum CodingKeys: String, CodingKey { case title case url case details case imageURL = "image_url" case identifier } }RemoteConfigManagerに新しいパラメータキーとjsonを扱う変数を追加します。
RemoteConfigManager.swiftimport FirebaseRemoteConfig class RemoteConfigManager { //以下のキーを追加 private let cardKey = "card_key" //以下の2行を追加 private let jsonDecoder = JSONDecoder() private var defaultValue: [String: NSObject] = [:]RemoteConfigManagerのupdateVariables()関数でCardRemomteConfigEntityを変数にセットします。
RemoteConfigManager.swiftprivate func updateVariables() { //以下のコードを追加 cardRemoteConfig = (try? jsonDecoder.decode(CardRemomteConfigEntity.self, from: remoteConfig[cardKey].dataValue)) }CardRemoteConfigViewController.swiftを新しくファイルを作成して以下のコードを記述してください。
やっていることは、モーダルに表示したいパーツとIBOutletと繋いで、初級編と同じように、RemoteConfigManagerを呼び出して必要に応じてラベルや画像に代入しています。CardRemoteConfigViewController.swiftimport UIKit class CardRemoteConfigViewController: UIViewController { @IBOutlet private weak var titleTextLabel: UILabel! @IBOutlet private weak var bodyTextView: UITextView! @IBOutlet private weak var presentWebButton: UIButton! @IBOutlet private weak var closeButton: UIButton! @IBOutlet private weak var cardImage: UIImageView! @IBOutlet private weak var modalView: UIView! func setup() { modalTransitionStyle = .crossDissolve modalPresentationStyle = .overCurrentContext } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() } override func viewDidLoad() { super.viewDidLoad() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) modalView.layer.cornerRadius = 8 titleTextLabel.text = RemoteConfigManager.shared.cardRemoteConfig?.title bodyTextView.text = RemoteConfigManager.shared.cardRemoteConfig?.details if let url = RemoteConfigManager.shared.cardRemoteConfig?.imageURL?.convertURL { do { let data = try Data(contentsOf: url) cardImage.image = UIImage(data: data) }catch let err { print("Error : \(err.localizedDescription)") } } } @IBAction func closeButton(_ sender: Any) { self.dismiss(animated: true, completion: nil) } @IBAction func presentWebButton(_ sender: Any) { if let openUrl = RemoteConfigManager.shared.cardRemoteConfig?.url.convertURL{ if UIApplication.shared.canOpenURL(openUrl) { UIApplication.shared.open(openUrl) } } } } extension String{ var convertURL: URL? { return URL(string: self) } }ViewControllerには、上のコードで設定したモーダルを表示するコードを追加します。
注意⚠️ viewDidLoad()で記述するとうまく画面を表示させることができません。
タップした時に表示したい場合は、タップアクションの中で呼び出すようにしてください。
今回は、ViewControllerが呼び出されたタイミングで表示したいので、viewDidAppear()の中に記述しています。CardRemoteConfigViewController.swiftoverride func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if RemoteConfigManager.shared.cardRemoteConfig?.title != nil{ let storyboard: UIStoryboard = UIStoryboard.init(name: "CardRemoteConfigViewController", bundle: nil) let cardRemoteConfigViewController = storyboard.instantiateViewController(withIdentifier: "CardRemoteConfigViewController") as! CardRemoteConfigViewController cardRemoteConfigViewController.modalPresentationStyle = .fullScreen cardRemoteConfigViewController.setup() self.present(cardRemoteConfigViewController, animated: false, completion: nil) } }⚠️実装におけるの問題点⚠️
今回の仕組みでは、アプリの初期起動時やRemoteConfigの値変更後のアプリ起動時には、変更した内容は次回起動時以降にUIに反映される仕様です。RemoteConfig変更直後の起動ではデータがうまく反映されません。初回起動時の場合は、データが蓄積されていないため、モーダルを表示させないように今回はさせていただきました。
コメント
今回初めてRemoteConfigを使用したQiita記事をさせていただきました。疑問に思ったことはわからないこと、間違っている箇所がある等があれば以下のコメント欄にてお知らせください。
キャンペーン等のモーダルを非表示にしたい場合は、remoteConfigのパラメーターキーとjsonを削除することで非表示にできます。最後までお読みいただきありがとうございました。
サンプルコードをダウンロードしたい方はGithubをクリックしよう。
- 投稿日:2020-01-19T17:52:00+09:00
FirebaseのRemoteConfigを使ってLabelの値を変更しよう![RemoteConfig初級編]
開発環境
Xcode: Version 11.3
実機iPhone: iOS 13.1
Firebase: Version 6.14.0
FirebaseRemoteConfig: Version 4.4.6やりたいこと
FirebaseのRemoteConfigを使ってLabelの値を変更したい。
初級編では、RemoteConfigを利用してViewControllerのLabelを変更してみようということで以下の画像のように、「Firebase 勉強中!!!」の文字を実際にRemoteConfigを利用して表示させていきます。RemoteConfig応用編
応用編では、RemoteConfigを利用してモーダルの値を変更してモーダルで表示することをやります。
応用編はこちらをクリック!実装の準備
それでは実際に開発してみよう!
今回は、タイトル通りFirebaseを利用するので、XcodeのプロジェクトファイルにFirebaseのライブラリーをインストールしておきましょう!サンプルコードでも、Cocoa Podを利用してFirebaseをインストールする必要があります。Firebaseの設定時に、「GoogleService-Info.plist」をプロジェクトファイルに追加する必要があります。Firebaseの初期導入がわからない方は、以下の記事を参考にしてください。
Firebaseの初期導入はこちらを参考に!RemoteConfigの設定
画像を参考にしながら、FirebaseのRemoteConfigの画面から、任意のパラメータキー(この記事ではtitle_name)とLabelに表示する文字列を入力しよう。
コーディング
RemoteConfigManagerを作ります。
ここでは、先ほど設定したパラメータキーの名前を変数として代入するRemoteConfigManager.swiftimport FirebaseRemoteConfig class RemoteConfigManager { static let shared = RemoteConfigManager() private let remoteConfig = RemoteConfig.remoteConfig() //初期の値 var titleString: String = "まだデータがありません。" //パラメータキー private let titleKey = "title_name"取得したデータを変数に代入
RemoteConfigManager.swiftprivate func updateVariables() { //RemoteConfigから取得したデータを入れる。 titleString = remoteConfig[titleKey].stringValue ?? "" }インスタンス生成時にRemoteConfigからのデータをfetchする処理
RemoteConfigManager.swiftprivate init() { remoteConfig.configSettings = RemoteConfigSettings() updateVariables() //開発環境の場合は0、本番環境(App Storeに配布する場合)では、は3600(1時間)以上に設定してください。 let expirationDuration = 0 remoteConfig.fetch(withExpirationDuration: TimeInterval(expirationDuration)) { [weak self] (status, _) in switch status { case .success: self?.remoteConfig.activate(completionHandler: { error in if let error = error { print("error:\(error)") } self?.updateVariables() }) case .failure: print("error: remote config fetch failure") default: break } } }最後にViewControlleを開いたタイミングで値をLabelに代入
ViewController.swiftclass ViewController: UIViewController { @IBOutlet weak var titileLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() titileLabel.text = RemoteConfigManager.shared.titleString }⚠️実装におけるの問題点⚠️
今回の仕組みでは、アプリの初期起動時やRemoteConfigの値変更後のアプリ起動時には、変更した内容は次回起動時以降にUIに反映される仕様です。
RemoteConfig応用編
応用編では、画像を加えてキャンペーンのようなモーダルを表示させることを紹介します。
応用編はこちらをクリック!サンプルコードをダウンロードしたい方はGithubをクリックしよう!
- 投稿日:2020-01-19T16:35:18+09:00
クリーンアーキテクチャーをざっと理解できる短いコードを書いた
クリーンアーキテクチャーは沢山インターフェースやクラスが登場し、その関係をざっと把握するのが難しいです。
そこですべての要素が入ったなるべく短いコードを書いてみました。XCodeのPlaygroundなので、そのまま実行できます。githubのリポジトリはこちらです。なお、今回はざっと理解することを優先するため、クリーンアーキテクチャーについての詳しい説明は割愛します。参考記事をご覧ください。このコードは、Uncle Bobの書いたClean Architecture 達人に学ぶソフトウェアの構造と設計と、次の記事を参考に書いています。
参考記事:
実装クリーンアーキテクチャ: https://qiita.com/nrslib/items/a5f902c4defc83bd46b8
Laravelで実践クリーンアーキテクチャ: https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22クリーンアーキテクチャーの概要図
引用:Clean Architecture 達人に学ぶソフトウェアの構造と設計概要図と今回のコードとの対応
サンプルコードに現れるインターフェース、クラスをなるべく本家の図と同じ位置に同じ色で配置しています。
この図で、本家と違うのが
SomeView
がUserController
にも依存しているところです。理由は、本家はMVC1を前提としていますが、Webフレームワークなどで使われているMVC2に寄せたほうが理解がしやすいと判断したためです。このあたりは『クリーンアーキテクチャの Presenter が分かりにくいのは MVC 2 じゃないから』という記事に解説があります。コード
実際のコードです。簡単な説明がコメント行に入っています。
クリーンアーキテクチャーのコード// 【Frameworks & Drivers】アプリケーションフレームワークやドライバなど実装の詳細にあたる部分 // DB ORMやDAOなどDBとのやり取りをするAPI。このサンプルでは何もしない class SomeDB { static func executeQuery(sql: String, bindParam: [String]) { // Dummy ここで実際にユーザーを登録する } } // UI UIKitやRailsの表示部分など画面表示をするためのAPI class SomeView { var userController: UserController var viewModel: UserCreateViewModel init(userController: UserController, viewModel: UserCreateViewModel) { self.userController = userController self.viewModel = viewModel self.viewModel.bind { userName in print("登録:" + userName + "さん") } } func start(){ userController.createUser(userName: "test user") } } // 【Interface Adapters】Application Business RulesとFrameworks & Driversの型の相互変換 // Controllers 入力をUserCaseのために変換する(入力のための変換) class UserController { var userCreateUseCase: UserCreateUseCaseInputPort init(userCreateUseCase: UserCreateUseCaseInputPort) { self.userCreateUseCase = userCreateUseCase } func createUser(userName: String) { let input = UserCreateInputData(userName: userName) userCreateUseCase.handle(input: input) } } // GateWays Frameworks & Driversからのデータを抽象化する class UserDataAccess: UserDataAccessInterface { func save(user: UserEntity) { SomeDB.executeQuery( sql: "REPLACE INTO USER (USER_NAME) VALUES (?) ", bindParam: [user.userName] ) } } // Presenters データをViewに適した加工する(出力のための変換) class UserCreatePresenter: UserCreateUseCaseOutputPort { var viewModel: UserCreateViewModel init(viewModel: UserCreateViewModel) { self.viewModel = viewModel } func complete(output: UserCreateOutputData) { let userName = output.userName self.viewModel.update(userName: userName) } } class UserCreateViewModel { typealias CallBackType = (String)->Void var userName: String var callBack: CallBackType? init(userName: String) { self.userName = userName } func bind(callBack: @escaping CallBackType) { self.callBack = callBack } func update(userName: String) { self.userName = userName self.callBack?(userName) } } // 【Application Business Rules】 アプリケーションのビジネスルール // UseCaseと上位層との遣り取りをするためのオブジェクト protocol UserDataAccessInterface { func save(user: UserEntity) } protocol UserCreateUseCaseOutputPort { // Output Boundaryともいう func complete(output: UserCreateOutputData) } struct UserCreateInputData { var userName: String } struct UserCreateOutputData { var userName: String } // Use Cases ユースケースを表す protocol UserCreateUseCaseInputPort { // Input Boundaryともいう func handle(input: UserCreateInputData) } class UserCreateInteractor: UserCreateUseCaseInputPort { var userDataAccess: UserDataAccess var presenter: UserCreateUseCaseOutputPort init(userDataAccess: UserDataAccess, presenter: UserCreateUseCaseOutputPort) { self.userDataAccess = userDataAccess self.presenter = presenter } func handle(input: UserCreateInputData) { let userName = input.userName let user = UserEntity(userName: userName) userDataAccess.save(user: user) let output = UserCreateOutputData(userName: user.userName) presenter.complete(output: output) } } // 【Enterprise Business Rules】 ドメイン層 // Entities ビジネスルールをカプセル化したもの struct UserEntity { var userName: String } // Entry Point このサンプルの実行開始ポイント let viewModel = UserCreateViewModel(userName: "") let userDataAccess = UserDataAccess() let presenter = UserCreatePresenter(viewModel: viewModel) let useCase = UserCreateInteractor(userDataAccess: userDataAccess, presenter: presenter) let userController = UserController(userCreateUseCase: useCase) var ui = SomeView(userController: userController, viewModel: viewModel) ui.start()まとめ
短い、といいながら140行ほどになってしまいました。また、なるべく簡単にするために
userName
だけを持つclass
ばかりとなってしまい、それぞれのclass
の必要性がつかみにくくなってしまったのが残念です。コードと図を書いていて気がついたのは、
UseCase
が依存するインターフェースはすべてUseCase
と同じレイヤーに配置されているということです。このあたりに依存性逆転の原則がしっかり守られていると感じました。
- 投稿日:2020-01-19T15:20:13+09:00
CMSampleBufferのCの関数使わなくてよくなったみたい
これまでCMSampleBufferからImageBufferを取り出す時などは
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)という関数を使っていましたが、iOS13.0から
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public var imageBuffer: CVImageBuffer? { get }というプロパティが生えて、
sampleBuffer.imageBufferでアクセス出来るようになりました!
imageBufferの他、timingInfoやformatDescriptionも同様に取れるようになっていました。
便利ですね!
- 投稿日:2020-01-19T13:30:14+09:00
フライウェイトをSwift5で実装する
※この記事は「全デザインパターンをSwift5で実装する」https://qiita.com/satoru_pripara/items/3aa80dab8e80052796c6 の一部です。
The Flyweight(フライウェイト)
0. フライウェイトの意義
同種のオブジェクトを大量に作らなくてはいけない時、それらの中に共通する不変のデータがある場合、これを複数のオブジェクトから参照するような設計にすると良い。
もし、全オブジェクトが同じデータを直接保持するとなると、生成するオブジェクト数が多い場合はかなりのメモリの無駄につながってしまう。
これがフライウェイトというデザインパターンである。
1. フライウェイトを使わない場合
宇宙船を表すクラス
SpaceShip
のオブジェクトを1000個作る場合を考える。SpaceShip.swiftpublic class SpaceShip { //宇宙船の外見(形およびテクスチャ)を表すプロパティ private let mesh: [Float] private let texture: UIImage? //宇宙船の位置を表すプロパティ private var position: (Float, Float, Float) public init(mesh: [Float], imageNamed name: String, position: (Float, Float, Float)) { self.mesh = mesh self.texture = UIImage(named: name) self.position = position } }Spaceships.playground//作るオブジェクトの個数 let fleetSize = 1000 var ships = [SpaceShip]() var vertices = [Float].init(repeating: 0, count: fleetSize) for _ in 0..<fleetSize { let ship = SpaceShip(mesh: vertices, imageNamed: "SpaceShip", position: ( Float.random(in: 1...100), Float.random(in: 1...100), Float.random(in: 1...100) ) ) ships.append(ship) }for-inループの中でメッシュやテクスチャまで1000回割り当てを行なっている。これらは全
SpaceShip
オブジェクトで共通していて、変わることがない。にも関わらず1000回も生成することはいかにも無駄である。2. フライウェイトを使う場合
全
SpaceShip
オブジェクトに共通する要素をSharedSpaceShipData
クラスとしてくくり出す。SpaceShip.swiftpublic class SharedSpaceShipData { private let mesh: [Float] private let texture: UIImage? public init(mesh: [Float], imageNamed name: String) { self.mesh = mesh self.texture = UIImage(named: name) }
mesh
,texture
プロパティを抜いた形で、SpaceShip
クラスを再定義する。SpaceShip.swiftpublic class SpaceShip { private var intrinsicState: SharedSpaceShipData private var position: (Float, Float, Float) public init(sharedData: SharedSpaceShipData, position: (Float, Float, Float)) { self.intrinsicState = sharedData self.position = position }実際に
SpaceShip
クラスのオブジェクトを1000個生成する際は、まずSharedSpaceShipData
クラスのオブジェクトを一つだけ作った後、全SpaceShip
オブジェクトがこれを参照するようにする。Spaceships.playground//生成するオブジェクトの個数 let fleetSize = 1000 var ships = [SpaceShip]() var vertices = [Float].init(repeating: 0, count: fleetSize) //まず共通するデータを表すオブジェクトを一つだけ生成する。 let sharedState = SharedSpaceShipData(mesh: vertices, imageNamed: "SpaceShip") for _ in 0..<fleetSize { //宇宙船オブジェクトが共通データを参照するようにする let ship = SpaceShip(sharedData: sharedState, position: ( Float.random(in: 1...100), Float.random(in: 1...100), Float.random(in: 1...100) ) ) ships.append(ship) }注意点としては、共有データである
SharedSpaceShipData
は参照型(クラス)として宣言しなくてはならないということである。値型(構造体)として宣言すると、各
SpaceShip
オブジェクトに割り当てる際にコピーされてしまうため、結局Flyweightパターンを用いないのと同じ結果となってしまう。※値型と参照型の違いについてはこちらの記事などを参照
https://qiita.com/chihayafuru/items/f6bb38c4a34d8cf62aa9https://github.com/Satoru-PriChan/FlyweightSpaceShipDemo
参考文献: https://www.amazon.com/Design-Patterns-Swift-implement-Improve-ebook/dp/B07MDD3FQJ
- 投稿日:2020-01-19T12:25:57+09:00
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Mediator~
この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況や
Swiftのコアライブラリやフレームワークで使われているパターンに
着目してデザインパターンを学び直してみた記録です。関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターンMediatorパターン概要
- Mediatorとは「仲介者」という意味です。
- 複数のオブジェクト間で直接やり取りをせずにMediatorを介してやり取りします。
- 各オブジェクトが依存する相手をMediatorだけにすることで、オブジェクト同士が疎結合になり、関連オブジェクトが多い場合には保守性を向上できます。
- GoFのデザインパターンでは振る舞いに関するパターンに分類されます。
使い所
実務的な例としては、UIPageViewControllerの配下にある子ViewController同士の通知が考えられます。
以下のサンプルコードは、赤背景のViewControllerが非表示になる時に、青背景のViewControllerに通知を行う例です。UIPageViewControllerがMediatorの役割を担っています。
メリットは、新たにReceiverとなる子ViewControllerが増えた時、既存の子ViewControllerは変更しなくても済む点です。サンプルコード
Xcode 11.3でシングルページアプリケーションを新規作成し、ViewController.swiftに以下のコードをコピペすれば動作します。
// MARK: - プロトコル protocol Receiver { func receive(message: String) } protocol Sender { func send(message: String) } protocol Mediator: class { var recipients: [Receiver] { get } func send(message: String) } // MARK: - PageViewControllerの子ViewController // 送り手のViewController final class SenderViewController: UIViewController { // Message送信をMediatorに委譲する weak var messageDelegate: Mediator? override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) // 表示されなくなった時にMessageを送信(Mediatorに委譲) messageDelegate?.send(message: "SenderViewController.viewDidDisappear()") } } // 受け手のViewController ※Receiverプロトコルに準拠 final class RecieverViewController: UIViewController, Receiver { func receive(message: String) { print("\(message)を受信しました") } } // MARK: - PageViewController final class ViewController: UIPageViewController { // 送り手のViewController let senderViewController = SenderViewController() // 受け手のViewController let receiverViewController = RecieverViewController() // 子ViewController配列 var controllers = [UIViewController]() override func viewDidLoad() { super.viewDidLoad() // PageViewControllerの子ViewControllerを設定 senderViewController.view.backgroundColor = .red controllers.append(senderViewController) receiverViewController.view.backgroundColor = .blue controllers.append(receiverViewController) setViewControllers([controllers[0]], direction: .forward, animated: false, completion: nil) dataSource = self // SenderViewControllerの委譲先に自分を設定する senderViewController.messageDelegate = self } } // PageViewControllerをMediatorプロトコルに準拠 extension ViewController: Mediator { var recipients: [Receiver] { // 子ViewControllerの中でReceiverプロトコルに準拠しているものを返す return controllers.filter { $0 is Receiver } as! [Receiver] } func send(message: String) { for recipient in recipients { recipient.receive(message: message) } } }
※説明には無関係ですが動かす時にコピペが必要なコード
// UIPageViewControllerDataSource extension ViewController: UIPageViewControllerDataSource { // 右にスワイプ(戻る) func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let index = controllers.firstIndex(of: viewController), index > 0 else { return nil } return controllers[index - 1] } // 左にスワイプ(進む) func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let index = controllers.firstIndex(of: viewController), index < controllers.count - 1 else { return nil } return controllers[index + 1] } func presentationCount(for pageViewController: UIPageViewController) -> Int { return controllers.count } }
- 投稿日:2020-01-19T11:19:19+09:00
iOS, Androidアプリの価格変更(キャンペーン)の仕方
はじめに
以前アプリをリリース(iOS, Mac, Androidのアプリをリリースしてみた
)したのですが、複数の方から100円も払えるかクソが!!といわれましたまだ機能が思ってる完成形には達していないし、やっぱだれかがダウンロードしてくれないとおもしろくないししばらくは無料にすることにしました!!
たまにみるキャンペーンみたいなので期間限定無料をやりたいと思い調べていたのですがやり方があんま出てこなかったので備忘録として書きます。
iOS, Mac版
iOSとMacアプリについてです。やり方はめっちゃ簡単。(だからあんま調べてもでてこないのかも)
- 下記ページを開きます
App Store Connect -> マイ App -> 対象アプリ -> App Store -> 価格および配信状況- 価格表の「価格変更を計画」をクリック
- 価格、開始日、終了日を選択
- 「終了」クリック
- 「保存」クリック
終了日は元の価格に戻る日付のようです。(2月いっぱい無料にしたかったのに間違えた)
Android版
Androidアプリについてです。こっちもめっちゃ簡単。
- 下記ページを開きます
Google Play Console -> 対象アプリ -> ストアでの表示 -> 価格と配布 -> セール- セール名を設定
- セール期間を設定(最大8日間らしいです)
- 価格を設定(最低30%オフらしいです)
- 「販売スケジュールを設定」クリック
一旦セールを行うと次のセール作成は30日間おこなえないようです。
参考:Play Console ヘルプー有料アプリのセールを作成する
さいごに
セールの開始日を当日にすると反映までは多少時間がかかるみたいです。余裕をもってセールを設定しましょう!!(iOSは3時間くらいでAndroidは8時間くらいでした)
今無料なんでだれかダウンロードしてください
- 投稿日:2020-01-19T09:54:40+09:00
GatsbyJSとNetlifyでiOS/AndroidアプリのLPを勢いで作ってみた
経緯
2020年1月にiOSアプリをリリースして、もともとAndroidのアプリがあったので
これは今ならLP作れるのでは...
という浅い考えと、何か新しい方法で試してみたいという欲求から今回は
GatsbyJSとNetlifyを使ってLPを作る事にしました。2020年初のアプリリリースしました?
— slowhand (@wes_ja0927) <a href="https://twitter.com/wes_ja0927/status/1218172126093438976?refsrc=twsrc%5Etfw">January 17, 2020
神社巡ったり好きな方は良ければ使って見てください。iOSアプリです。m(_ )mhttps://t.co/qv2C9dDhu1GatsbyJSとNetlify
- GatsbyJS
- GatsbyJSに関してはこちら参照 GatsbyJSを試してみた
- Netlify
- 静的サイトをホスティングすることができるWebサービス
- 料金に関して
- 今回は無料の枠内で対応
環境構築
今回はこちらのテンプレートを使用してLPを作成したいと思います。
$ gatsby new app-lp https://github.com/anubhavsrivastava/gatsby-starter-newage
早速起動してみます。
$ cd app-lp $ gatsby developブラウザで
http://localhost:8000/
にアクセスして↓のページが表示されればOKです。
LPポイですねw (当たり前)ここまで出来たら、一度Github上にリポジトリを作成しPushします。
今回はPrivateなリポジトリでも可能なのか検証も含めPrivateリポジトリで試しました。
(確認後Publicにしてます)サイトページの内容を修正
基本的な情報
config.js
で設定されているので、ここを適宜修正します。module.exports = { siteTitle: '[アプリ名]', // <title> ☆修正 manifestName: 'app-lp', // ☆修正 manifestShortName: 'Landing', // max 12 characters manifestStartUrl: '/', manifestBackgroundColor: '#EB6101', // ☆修正 manifestThemeColor: '#EB6101', // ☆修正 manifestDisplay: 'standalone', manifestIcon: 'src/assets/img/website-icon.png', // ☆アイコン修正 pathPrefix: `/app-lp/`, // ☆修正 // social ☆必要に応じて修正 socialLinks: [ { icon: 'fa-github', name: 'Github', url: 'https://github.com/xxxx', }, { icon: 'fa-linkedin-in', name: 'Linkedin', url: 'https://linkedin.com/in/xxxx/', }, { icon: 'fa-twitter', name: 'Twitter', url: 'https://twitter.com/xxxx', }, { icon: 'fa-facebook-f', name: 'Facebook', url: 'https://facebook.com/xxxx', }, ], };Footerの修正
まずは簡単な所から、
src/components/Footer.js
のコピーライトの部分を修正しました。import React from 'react'; export default function Footer() { return ( <footer> <div className="container"> <p>© [名前]. All Rights Reserved.</p> </div> </footer> ); }コンテンツ全般の修正
次にコンテンツ全般の修正をガッツリ行いました。主に修正するファイルは
src/pages/index.js
になります。
また、適宜CSSも微調整しました。Netlifyでページ公開
ページを修正したら、早速デプロイして公開していきたいと思います。
Netlifyにログインすると以下のページが表示されます。
早速Githubのレポジトリと連携しようと思うので、「New from site Git」から連携するリポジトリを選択します。
設定はデフォルトのままで「Deploy site」で早速デプロイしてみます。
今回作成したLPは以下に公開してます !
https://ss-map-lp.netlify.com/
※ドメイン変更まで行うと有料になるので、今回はやりませんでした。リポジトリはこちら
https://github.com/Slowhand0309/ss-map-lp
(※Privateでも大丈夫そうだったので、今は公開してます)ものの数時間でデプロイできました 更新があっても後はリポジトリにPushするだけなのでとても便利そうです。
参考になったURL