- 投稿日:2019-12-15T22:35:08+09:00
【Objective-C/Swift】iOS13対応にてUITableViewの「shouldIndentWhileEditingRowAtIndexPath」が機能しなくなった話。
こういう人に向けて発信しています。
・編集モード中に左寄せしていた人
・iOS13対応をしている人
・下記UITableViewDelegateのデリゲートメソッドを使用している人//インテント調整 - (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath{ //チェックマーク分の余白を詰める為にインテントは表示しない。 return NO; }iOS12以前では編集モードで出てくるチェックボタンを消したい場合は
冒頭に貼りましたコードなどで対応する必要がありました。そうすればインデント(左からの余白)が無しになり、
詰められるといった算段です。しかしながら、iOS13でiOSシミュレータ上・実機で確認してみると、
上記コードは全く動作していない事が分かりました。上記iOSシミュレーターは下記のコードで動かしております。
つまり、インデントを調整するデリゲートメソッドも採用しているにも
関わらずインデントがNOになっておりません。#import "ViewController.h" @interface ViewController ()<UITableViewDataSource,UITableViewDelegate> @property (weak, nonatomic) IBOutlet UITableView *tableView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.tableView.delegate = self; self.tableView.dataSource = self; //編集モードを有効にする。 [self.tableView setEditing:YES animated:YES]; self.tableView.allowsMultipleSelectionDuringEditing = true; } //インテント調整 - (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath{ //チェックマーク分の余白を詰める為にインテントは表示しない。 return NO; } //row = 行数を指定するデリゲートメソッド - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ //必ずNSInteger型を返してあげている。 return 30; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ //標準で用意されているTableViewを利用する場合。 NSString *cellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; if (!cell) { cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } cell.textLabel.text = [NSString stringWithFormat:@"このセルは%ld番目のセルになります!", (long)indexPath.row]; return cell; } @end調査結果について
そもそもshouldIndentWhileEditingRowAtIndexPathでTweet検索して、
最新のツイートが2013年とかなのでニッチなデリゲートメソッドです。なのでiOS13とか以前に関連する文献は一切見つかりませんでした。
特に公式で非推奨になるとアナウンスされたコードでも無いので、
不具合動作なのかなとも考えておりますが、
現時点で編集モード中インデントを無しにするというのは難しいと考えております。対応策について
・編集モードは制御出来ないと考え使用しない。
・編集モードを自作する。まずは仕様としては
・汎用性が高いようにセルを複数選択を想定する。
・セルを押下時は背景色を変えて、非選択時には背景色をデフォルトに。上記仕様で考えて実装してみました。
Objective-C/Swift両方とも書いてみました。Objective-C版
#import "ViewController.h" @interface ViewController ()<UITableViewDataSource,UITableViewDelegate> @property (weak, nonatomic) IBOutlet UITableView *tableView; @property (strong,nonatomic) NSMutableArray<NSIndexPath *> *selectedIndexPathArray; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.tableView.delegate = self; self.tableView.dataSource = self; self.selectedIndexPathArray = @[].mutableCopy; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return 30; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ //標準で用意されているTableViewを利用する場合。 NSString *cellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; if (!cell) { cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } cell.textLabel.text = [NSString stringWithFormat:@"このセルは%ld番目のセルになります!", (long)indexPath.row]; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ if([self.selectedIndexPathArray containsObject:indexPath]){ [self.selectedIndexPathArray removeObject:indexPath]; }else{ [self.selectedIndexPathArray addObject:indexPath]; } [self.tableView reloadData]; } /** @brief セル表示前の処理 */ - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{ //セルのテキストが入っている場合は色を青にする if([self.selectedIndexPathArray containsObject:indexPath]){ cell.backgroundColor = [UIColor lightGrayColor]; }else{ cell.backgroundColor = [UIColor clearColor]; } } @endSwift版
import UIKit class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { @IBOutlet weak var tableView: UITableView! var selectedIndexPathArray : NSMutableArray = [] ; override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. self.tableView.dataSource = self; self.tableView.delegate = self; } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 30; } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "Cell") // セルに表示する値を設定する cell.textLabel!.text = indexPath.row.description return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if self.selectedIndexPathArray .contains(indexPath){ self.selectedIndexPathArray.remove(indexPath) }else{ self.selectedIndexPathArray.add(indexPath) } tableView.reloadData() } func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { if self.selectedIndexPathArray .contains(indexPath){ cell.backgroundColor = .blue }else{ cell.backgroundColor = .clear } } }簡単に解説するのであれば、
(1)選択されたNSIndexPathを管理する配列を用意する。
(2)押下時に配列にNSIndexPathを含むか判定し、
含む場合は削除、含まない場合は追加する。
(3)セル表示前にセル自身のindexPathを判定し、
含む場合は選択色、含まない場合は透過色(デフォルト)にする。以上がロジックです。
Objective-C
NSArray *selectedIndexPaths = [self.tableView indexPathsForSelectedRows];
上記のようなコードで最後にTableViewの選択されている全てのセルを
取得すると思いますが、その場合は上記コードの配列をそのまま活用すればOKです。
(押下されたセルのみ、NSIndexPath型の配列なので)
- 投稿日:2019-12-15T21:41:04+09:00
Swift5でDropbox APIを使いDropbox上にある画像を表示
導入まではこちら
Dropbox APIをSwift5で使用する方法バージョン情報
・Swift version 5.0.1
・Xcode version 10.2.1ダウンロードボタンを設置
ViewController.swiftimport UIKit import SwiftyDropbox class ViewController: UIViewController { //イメージビューを追加 let myImageView = UIImageView() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. //省略 //ダウンロードボタンを追加 let downloadButton = UIButton(type: UIButton.ButtonType.system) downloadButton.frame = CGRect(x: 10, y: 220, width: 100, height: 30) downloadButton.setTitle("Download", for: .normal) downloadButton.addTarget(self, action: #selector(self.downloadDropboxFile), for: .touchUpInside) self.view.addSubview(downloadButton) //画像表示エリアの記載 myImageView.frame = CGRect(x: 10, y: 500, width: 200, height: 120) self.view.addSubview(myImageView) }ダウンロード 画面表示処理
ViewController.swift@objc func downloadDropboxFile() { //ダウンロード処理 if let client = DropboxClientsManager.authorizedClient { //ダウンロード先URLを設定 let destination : (URL, HTTPURLResponse) -> URL = { temporaryURL, response in let fileManager = FileManager.default let directoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] let UUID = Foundation.UUID().uuidString var fileName = "" if let suggestedFilename = response.suggestedFilename { fileName = suggestedFilename } let pathComponent = "\(UUID)-\(fileName)" return directoryURL.appendingPathComponent(pathComponent) } //画面描画処理 client.files.download(path: "/logo.png", destination: destination).response { response, error in if let (metadata, url) = response { print("Downloaded file name: \(metadata.name)") do { //urlをData型に変換 let data = try Data(contentsOf: url) //Data型に変換したurlをUIImageに変換 let img = UIImage(data: data) //UIImageをivに変換 let iv:UIImageView = UIImageView(image:img) //変換したivをviewに追加 self.view.addSubview(iv) //表示位置決定 iv.layer.position = CGPoint(x: self.view.bounds.width/2, y: 400.0) } catch let err { print("Error : \(err.localizedDescription)") } } else { print(error!) } } } }画面
参考
- 投稿日:2019-12-15T21:41:04+09:00
Swift 5でDropbox APIを使いDropbox上にある画像を表示
導入まではこちら
Dropbox APIをSwift5で使用する方法バージョン情報
・Swift version 5.0.1
・Xcode version 10.2.1ダウンロードボタンを設置
ViewController.swiftimport UIKit import SwiftyDropbox class ViewController: UIViewController { //イメージビューを追加 let myImageView = UIImageView() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. //省略 //ダウンロードボタンを追加 let downloadButton = UIButton(type: UIButton.ButtonType.system) downloadButton.frame = CGRect(x: 10, y: 220, width: 100, height: 30) downloadButton.setTitle("Download", for: .normal) downloadButton.addTarget(self, action: #selector(self.downloadDropboxFile), for: .touchUpInside) self.view.addSubview(downloadButton) //画像表示エリアの記載 myImageView.frame = CGRect(x: 10, y: 500, width: 200, height: 120) self.view.addSubview(myImageView) }ダウンロード 画面表示処理
ViewController.swift@objc func downloadDropboxFile() { //ダウンロード処理 if let client = DropboxClientsManager.authorizedClient { //ダウンロード先URLを設定 let destination : (URL, HTTPURLResponse) -> URL = { temporaryURL, response in let fileManager = FileManager.default let directoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] let UUID = Foundation.UUID().uuidString var fileName = "" if let suggestedFilename = response.suggestedFilename { fileName = suggestedFilename } let pathComponent = "\(UUID)-\(fileName)" return directoryURL.appendingPathComponent(pathComponent) } //画面描画処理 client.files.download(path: "/logo.png", destination: destination).response { response, error in if let (metadata, url) = response { print("Downloaded file name: \(metadata.name)") do { //urlをData型に変換 let data = try Data(contentsOf: url) //Data型に変換したurlをUIImageに変換 let img = UIImage(data: data) //UIImageをivに変換 let iv:UIImageView = UIImageView(image:img) //変換したivをviewに追加 self.view.addSubview(iv) //表示位置決定 iv.layer.position = CGPoint(x: self.view.bounds.width/2, y: 400.0) } catch let err { print("Error : \(err.localizedDescription)") } } else { print(error!) } } } }画面
参考
- 投稿日:2019-12-15T21:22:33+09:00
Dropbox APIをSwift5で使用する方法
SwiftでDropbox APIを活用する特殊案件にぶち当たって
Swift特有のバージョン毎に微妙に何かが違う現象でハマったので、メモ。
Pods入れてから認証まで。
Pods部分はもっとわかりやすい人の解説へどうぞ。バージョン情報
・Swift version 5.0.1
・Xcode version 10.2.1Xcodeプロジェクトの作成
Single View Applicationのプロジェクトを適当に作成
Cocoapodsの設定
# Uncomment the next line to define a global platform for your project # platform :ios, '8.0' target 'dropCheck' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for dropCheck pod 'SwiftyDropbox' end現バージョンは特に何も指定しなくてOK
Info.plistの設定変更
赤色塗りつぶしは自身のAPIキーを記載
Dropboxアプリの自身のAPIキーはApp Consoleで取得DropboxClientのインスタンス初期化
AppDelegate.swiftimport UIKit import SwiftyDropbox //追加 @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. DropboxClientsManager.setupWithAppKey("自分のAPIキー") //追加 return true }画面にログインするボタンを作成
ViewController.swiftimport UIKit import SwiftyDropbox class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // ログインボタンを追加 let signInButton = UIButton(type: UIButton.ButtonType.system) signInButton.frame = CGRect(x: 10, y: 190, width: 100, height: 30) signInButton.setTitle("Sign In", for: .normal) signInButton.addTarget(self, action: #selector(self.signInDropbox), for: .touchUpInside) self.view.addSubview(signInButton) } @objc func signInDropbox(){ if let _ = DropboxClientsManager.authorizedClient { //既にログイン済みだとクラッシュするのでログアウト DropboxClientsManager.unlinkClients() } DropboxClientsManager.authorizeFromController(UIApplication.shared, controller: self, openURL: { (url: URL) -> Void in UIApplication.shared.openURL(url) }) }認証処理
AppDelegate.swiftfunc application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { if let authResult = DropboxClientsManager.handleRedirectURL(url) { switch authResult { case .success: print("Success! User is logged into Dropbox.") case .cancel: print("Authorization flow was manually canceled by user!") case .error(_, let description): print("Error: \(description)") } } return true一応この3点セットがあればDropbox APIの確認という意味では動く。
動作画面
おわりに
他の言語と違って、
Swiftは調べてもしんどいし、色々な方の解説読みに行くより
結局Gitの英語の説明読みに行ったほうが早く解決する事の方が多い。なんでだろね
そしてそのGitの説明もiOSとかSwiftの修正に追いついていないという現象もついてきて
ダブル、トリプルでハマる…Apple様には逆らえない参考
・公式
http://dropbox.github.io/SwiftyDropbox/api-docs/latest/
https://github.com/dropbox/SwiftyDropbox/issues/94
- 投稿日:2019-12-15T21:22:33+09:00
Dropbox APIをSwiftで使用する方法
SwiftでDropbox APIを活用する特殊案件にぶち当たって
Swift特有のバージョン毎に微妙に何かが違う現象でハマったので、メモ。
Pods入れてから認証まで。
Pods部分はもっとわかりやすい人の解説へどうぞ。バージョン情報
・Swift version 5.0.1
・Xcode version 10.2.1Xcodeプロジェクトの作成
Single View Applicationのプロジェクトを適当に作成
Cocoapodsの設定
# Uncomment the next line to define a global platform for your project # platform :ios, '8.0' target 'dropCheck' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for dropCheck pod 'SwiftyDropbox' end現バージョンは特に何も指定しなくてOK
Info.plistの設定変更
赤色塗りつぶしは自身のAPIキーを記載
Dropboxアプリの自身のAPIキーはApp Consoleで取得DropboxClientのインスタンス初期化
AppDelegate.swiftimport UIKit import SwiftyDropbox //追加 @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. DropboxClientsManager.setupWithAppKey("自分のAPIキー") //追加 return true }画面にログインするボタンを作成
ViewController.swiftimport UIKit import SwiftyDropbox class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // ログインボタンを追加 let signInButton = UIButton(type: UIButton.ButtonType.system) signInButton.frame = CGRect(x: 10, y: 190, width: 100, height: 30) signInButton.setTitle("Sign In", for: .normal) signInButton.addTarget(self, action: #selector(self.signInDropbox), for: .touchUpInside) self.view.addSubview(signInButton) } @objc func signInDropbox(){ if let _ = DropboxClientsManager.authorizedClient { //既にログイン済みだとクラッシュするのでログアウト DropboxClientsManager.unlinkClients() } DropboxClientsManager.authorizeFromController(UIApplication.shared, controller: self, openURL: { (url: URL) -> Void in UIApplication.shared.openURL(url) }) }認証処理
AppDelegate.swiftfunc application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { if let authResult = DropboxClientsManager.handleRedirectURL(url) { switch authResult { case .success: print("Success! User is logged into Dropbox.") case .cancel: print("Authorization flow was manually canceled by user!") case .error(_, let description): print("Error: \(description)") } } return true一応この3点セットがあればDropbox APIの確認という意味では動く。
動作画面
おわりに
他の言語と違って、
Swiftは調べてもしんどいし、色々な方の解説読みに行くより
結局Gitの英語の説明読みに行ったほうが早く解決する事の方が多い。なんでだろね
そしてそのGitの説明もiOSとかSwiftの修正に追いついていないという現象もついてきて
ダブル、トリプルでハマる…Apple様には逆らえない参考
・公式
http://dropbox.github.io/SwiftyDropbox/api-docs/latest/
https://github.com/dropbox/SwiftyDropbox/issues/94
- 投稿日:2019-12-15T21:11:30+09:00
MVVMとMVPパターン
イントロダクション
アプリを作る際に考えることは色々ありますが、その中でも特に重要なのが設計パターンの選定だと思います。
私自身は今までMVVMパターンでの実装が主だったのですが、MVPについて前から興味があったのでサンプルアプリを作りながら両者のメリデメを考察してみました。参考のためiOSアプリ設計パターン入門を読みました。各章の内容が非常に簡潔にわかりやすくまとまっており、読みやすいためかなりオススメな著書です。
MVVMとは
一言で言うとGUIの構造をModel, View, ViewModelの3つに分けたパターンです。
それぞれの役割は以下です。
- Model - 表示に関わらないドメインロジックを持つ
- View - ユーザー操作を受けつけ、ViewModelの状態の変更を監視し画面の更新を行う
- ViewModel - 画面の表示全般に関わるロジックを持ち、Viewに使うデータを保持する
図のように参照関係は一方向で、逆の参照関係はありません。Model, ViewModelの変更はイベントにより通知されます。MVVMパターンの大きな特徴としてはViewの更新をデータバインドによって行うという点です。
手続き的ではなく宣言的にロジックが表現できるため、コンポーネント間を疎結合に保てます。それによって可読性やテスト容易性を向上させます。
このデータバインド機構にRxSwiftやReactiveSwitといったライブラリを利用するのですが、こちらの概念理解にかかるコストが非常に高いというのも特徴の一つです。MVPとは
こちらも一言で言うとGUIの構造をModel, View,Presenterの3つに分けたパターンです。
Modelの役割はMVVMと変わりません。
- Model - 表示に関わらないドメインロジックを持つ
- View - ユーザー操作を受けつけ、Presenterに処理を委譲。Presenterから画面更新の指示を受け付け、画面を更新する
- Presenter - ModelとViewの仲介役。画面表示に関するロジックを持つ
Viewの更新にはPassive ViewとSupervising Controllerの2つの方法がありますが、今回は図のPassive Viewを前提にします。Passive Viewはデータフローが追いやすい反面、簡単な処理もModelが絡んだ際は必ず以下のフローに準拠する必要があります。View → Presenter → Model → Presenter → Viewそのため、コードが冗長になりやすいという面もあります。
しかし、全てのプレゼンテーションロジックをPresenterの中に閉じ込めることができるのでテスト容易性が向上します。MVVMとMVPの比較
まず、MVVMもMVPも共通して言えることはコンポーネントを疎結合にしてテスト容易性や可読性を向上させています。
そもそもなぜそうする必要があるのか。
それはAppleが提唱したCococa MVCパターンにてViewControllerの役割が大きすぎて可読性、テスト容易性が低くなってしまい、いわゆるFat View Controllerになりがちということがあります。
そのため、MVVMやMVPなどのパターンを使い、責務の分離をすることでそれらの課題を解決しています。MVVMとMVPの違いを実感するために冒頭で紹介した著書のサンプルコードを元にMVVMとMVPそれぞれの設計でサンプルアプリを作ってみました。
Githubのユーザーを検索し、ユーザーのリポジトリ一覧を表示するものです。
データは実際のAPI通信は行わず、モックから取得しています。
ユーザー検索 リポジトリを表示 本記事ではsearchBarに文字を入力して対象のユーザーを表示する部分をMVVM, MVPそれぞれのパターンで実装する部分を解説します。リポジトリの表示についてはサンプルコードのURLを最後に記載しますので、興味を持たれた方は見て頂けますと幸いです。
まずはMVPでの実装を見てみます。
Presenterの実装ではまずInput/Outputの処理をプロトコルで宣言しておきます。
ViewControllerから委譲されたInputの処理を行い、OutputにてModelに処理を委譲し、結果を再びViewControllerに伝えます。// SearchUserPresenter protocol SearchUserPresenterInput { var numberOfUsers: Int { get } func user(forRow row: Int) -> User? func didSelectRow(at indexPath: IndexPath) func didTapSearchButton(text: String?) } protocol SearchUserPresenterOutput: AnyObject { func updateUsers(_ users: [User]) func transitionToUserDetail(userName: String) } final class SearchUserPresenter: SearchUserPresenterInput { ... func didTapSearchButton(text: String?) { guard let query = text { return } model.fetchUser(query: query) { [weak self] result in switch result { case .success(let users): self?.users = users DispatchQueue.main.async { self?.view.updateUsers(users) } case .failure: () } } } ... }ViewControllerはOutputプロトコルを実装することで画面を更新します。
// SearchUserViewController.swift extension SearchUserViewController: SearchUserPresenterOutput { func updateUsers(_ users: [User]) { tableView.reloadData() } ... }これによって先ほど説明した以下フローを実現しています。
View → Presenter → Model → Presenter → View次にMVVMでの実装を見てみます。
ViewModelはViewからの入力に反応してstate(Mutable Property)を更新します。
ここではViewからsearch()メソッドが実行(入力)されることによってModelがデータ取得処理を行い、結果をstateにバインドしています。final class SearchUserViewModel { let state = BehaviorRelay<[User]?>(value: nil) private let model: SearchUserModel private let disposeBag = DisposeBag() init(model: SearchUserModel) { self.model = model } func search(query: String) { model.fetch(query: query) .asObservable() .bind(to: state) .disposed(by: disposeBag) } }Viewでは予め監視対象を設定しておき、ViewModelのイベント通知によって画面更新を行います。
final class SearchUserViewController: UIViewController { ... override func viewDidLoad() { super.viewDidLoad() viewModel = SearchUserViewModel(model: SearchUserModel()) tableView.registerCell(type: SearchUserCell.self) viewModel.state.subscribe(onNext: { [unowned self] _ in self.tableView.reloadData() }).disposed(by: disposeBag) } } ... extension SearchUserViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard let state = viewModel.state.value else { return 0 } return state.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueCell(type: SearchUserCell.self, indexPath: indexPath) guard let state = viewModel.state.value else { return cell } cell.configure(user: state[indexPath.row]) return cell } }MVVMとMVPそれぞれで実装してみての所感
まず、最初に感じたのはMVPではプレゼンテーションロジックの実装を行う際に、「ViewとModelそれぞれのことを考えなければならない。」ということが気になりました。
先に説明した通りPresenterは「仲介役」です。
そのため、Presenterを実装する際にはViewとModelそれぞれの顔色を伺いながら実装するということを余儀なくさせられます。
一方で、MVVMはプレゼンテーションロジックを実装する際にはあくまでModelからとってきたデータをどのように整形するのかのみに集中できました。
コードを見てもらえばわかる通り、コード量も段違いにMVPの方が多いです。
これは先に説明した通りコードが冗長になるということを体現しています。まとめ
今回実際にMVVM, MVPパターンでサンプルアプリを実装してみてそれぞれのメリット・デメリットを比較したものは以下でした。
MVVM
メリット
- View, ViewModel, Modelが一方向の参照関係になっているため、疎結合が保て、それぞれの作業に集中できる
- それぞれのコンポーネントで作業分担が明確なため、冗長なコードが生まれず、コード量を比較的少なくできる
デメリット
- RxSwift, ReactiveSwiftなどのライブラリが必須なため、初期設定が手間
- FRPの概念を理解するまでの学習コストが高い
MVP
メリット
- 初期導入に必要なものがない
- 特別な実装が不要なため、すでにMVCなどで実装しているプロジェクトにも導入がしやすい
デメリット
- データフローへの準拠が厳しく、コードが冗長になりやすい
- コード量が増え、実装に手間がかかる
サンプルコード
- 投稿日:2019-12-15T20:57:21+09:00
AWS Kinesis Video StreamsでビデオストリーミングWebRTCを実施
こんにちは フリーランスの永田です。
今年も仕事をしながら、技術を探求して、ARのライブラリー、ビデオ編集のライブラリー、背面前面同時撮影できるカメラのライブラリーなどを作成しました。 こちらに技術を公開しています
今回はSwift愛好会の2019年のアドベントカレンダーに参加させていただきました。
AWS Kinesis Video StreamsでビデオストリーミングWebRTCを実施します。
こちらの順序で展開させていただきます。
環境 サンプルソース やり方 つまづきポイント 挙動環境
macOS Catalina 10.16.2
Xcode Version 11.2.1 (11B500)
AWS マネジメントコンソールに入れること(AWSに登録済み)
AWS Kinesis Video Streamsでシグナリングチャンネルの作成
サンプルソース
このリンク先のアプリとAWS Kinesis Video Streamsを連携させてWebRTCを実現します。
https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-iosやり方
サンプルソースのReadMeを確認願います。
Amazon Cognitoのユーザープール、IDプールの管理設定が必要になります。
Amazon Cognitoの作成方法、サンプルの確認手順はこのリンクに書いています。
AWSKinesisVideoWebRTCDemoApp.xcworkspace
をbuild&Runbuildが通らない場合、サンプルソースにbuildを通すコマンド操作を確認。
記載箇所にAmazon Cognitoで設定した項目を記載します。
Constants.swift
import Foundation import AWSCognitoIdentityProvider // 記入 let CognitoIdentityUserPoolRegion = AWSRegionType.APNortheast1 // 記入 let CognitoIdentityUserPoolId = "Amazon Cognitoのプール ID" // 記入 let CognitoIdentityUserPoolAppClientId = "Amazon Cognitoのアプリクライアント ID" // 記入 let CognitoIdentityUserPoolAppClientSecret = "Amazon Cognitoのアプリクライアントのシークレット" let AWSCognitoUserPoolsSignInProviderKey = "UserPool" // 記入 let CognitoIdentityPoolID = "ap-northeast-1:~~~~~~~~~~~~~ ID プールの管理から作成するID プールID" // 記入 let AWSKinesisVideoEndpoint = "https://kinesisvideo.ap-northeast-1.amazonaws.com" let AWSKinesisVideoKey = "kinesisvideo" let VideoProtocols = ["WSS", "HTTPS"] let ConnectAsMaster = "connect-as-master" let ConnectAsViewer = "connect-as-viewer" let MasterRole = "MASTER" let ViewerRole = "VIEWER" let ClientID = "ConsumerViewer"awsconfiguration.json
{ "Version": "1.0", "CredentialsProvider": { "CognitoIdentity": { "Default": { // 記入 "PoolId": "ap-northeast-1:~~~~~~~~~~~~~ ID プールの管理から作成するID プールID", // 記入 "Region": "ap-northeast-1" } } }, "IdentityManager": { "Default": {} }, "CognitoUserPool": { "Default": { // 記入 "AppClientSecret": "Amazon Cognitoのアプリクライアントのシークレット", // 記入 "AppClientId": "Amazon Cognitoのアプリクライアント ID", // 記入 "PoolId": "Amazon Cognitoのプール ID", // 記入 "Region": "ap-northeast-1" } } }
パスワードを再設定の際、Amazon Cognitoでメール、SMSからパスワードの再設定コードを受け取る必要があります。
- サインインしてConnect画面に遷移
Kinesis Video Streamsのシグナリングチャンネルのチャンネル名
Amazon Cognitoのアプリクライアントのシークレット
リージョン名
を記入して、青い色Button(どちらでも良い)を押下で成功するとConnectedになり、RTC専用のカメラプレビュー画面に遷移します。AWSのライブラリーではカメラもライブラリー内で使用されていて、UIViewを渡す処理になっていますので、カメラの種類をカスタマイズは難しいようです。モバイルで表示しているプログラムです。ライブラリーのRTCMTLVideoViewはUIViewになります。
override func viewDidLoad() { super.viewDidLoad() #if arch(arm64) // Using metal (arm64 only) let localRenderer = RTCMTLVideoView(frame: localVideoView?.frame ?? CGRect.zero) let remoteRenderer = RTCMTLVideoView(frame: view.frame) localRenderer.videoContentMode = .scaleAspectFill remoteRenderer.videoContentMode = .scaleAspectFill #else // Using OpenGLES for the rest let localRenderer = RTCEAGLVideoView(frame: localVideoView?.frame ?? CGRect.zero) let remoteRenderer = RTCEAGLVideoView(frame: view.frame) #endif webRTCClient.startCaptureLocalVideo(renderer: localRenderer) webRTCClient.renderRemoteVideo(to: remoteRenderer) if let localVideoView = self.localVideoView { embedView(localRenderer, into: localVideoView) } embedView(remoteRenderer, into: view) view.sendSubviewToBack(remoteRenderer) }つまづきポイント
ユーザーは管理画面から作成ではなく、アプリのサインアップから作成してください。
管理画面から作成すると、パスワードの再設定をしていない状態になってしまい、別途コマンド操作でパスワードの再設定が必要になりますがAWS CLIから操作する場合に、シークレットキーを外す対応が必要です。
今回のサンプルの手順ではAWS CLIから対応するより、アプリのAPIから対応した方が良さそうです。
https://qiita.com/noobar/items/6615501b035e47792227issuesを投げたところ、デバッグでレスポンスの確認をしてくださいとの事で、確認しましたら、パスワードの再設定的なログがでたので、アプリ側から操作してパスワードを再設定したところ、アプリ認証が通りました。
https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-ios/issues/1挙動
モバイル
シグナリングチャンネルのConnect画面
プレビュー画面
AWS Kinesis Video Streams シグナリングチャンネル画面
Chromeなどで開いて下さい。
CloudWatch メトリクスなどで使用解析もできるので、価格プランなども検討できると思います。
複数端末での挙動
複数端末でのAWSRTCの配信技術 pic.twitter.com/1lIaz3U3WC
— DaisukeNagata (@dbank0208) December 12, 2019アプリケーションを動作させる上で、合計工数は4時間ぐらいで確認できました。
リアルタイム配信が身近なものになったと思いますが、カメラ機能などカスタマイズ性を向上させるのはRTC側を自作する必要があり、Safariだとメディア再生ビューワーが開かなかったので、自作してSafariを開かせる必要が出てきそうです。
関連資料 JSのWeb画面でビデオストリーミングWebRTCを利用する場合
今後も精進して、技術力を高めて、ゆくゆくは技術で還元していきます。
以上、貴重なお時間お読み下さいまして、誠にありがとうございます。
- 投稿日:2019-12-15T20:46:09+09:00
プロジェクト起動したら画面真っ黒[Swift][Info.plist configuration "Default Configuration" for UIWindowSceneSessionRoleApplication contained UISceneDelegateClassName...]
状況
プロジェクトをビルドしたら
Info.plist configuration "Default Configuration" for UIWindowSceneSessionRoleApplication contained UISceneDelegateClassName key, but could not load class with name "TinderSwipeSample.SceneDelegate". [WindowScene] There is no scene delegate set. A scene delegate class must be specified to use a main storyboard file.こんなエラーとともに実機の画面が真っ黒に。。。。
is initial viewcontroller
は全てチェック入れているTARGETS
->Deployment info
->Main Interface
のStoryboard
を変更しても真っ黒なまま原因
SceneDelegate.swift
ファイルを消去してしまっていた
->info.plist
でConfiguration Name
をDefault Configuration
にしている場合は消去してはいけないらしい
->以前は消去しても実行できていたのでverが新しいとこのバグが起きるかもしれません。はっきりとはわかりません
SceneDelegate.swift
はSwiftUIで使うもの->Storyboard
しか使わないから消してもいいやーと思っていました。皆さんも気をつけて。
(エラー文にはscene delegateがないよーみたいなこと書いてくれてるんですがね。。。)参考
- 投稿日:2019-12-15T20:19:28+09:00
うちはサスケが習得するのに1ヶ月かかった千鳥を2日で習得してみた。
初めに
この記事は漫画「NARUTO」に大きく依存していますので「NARUTO」知らない方はごめんなさい。
遡ること二日前、Twitterでニジゲンノモリというところで「NARUTO」のアトラクションがあるということを知りました。
以下ニジゲンノモリURL
http://nijigennomori.com/naruto_shinobizato/
行ってみたいなーと思いましたが、就活とか授業で忙しいし金もないし行けませんでした。
どうしようかなあと思った矢先、ニジゲンノモリのアトラクションの一つでAR使って千鳥とか火遁とかを出してる風な画像が撮れるというものがあることに気づきます。
あ、これ作れば行かなくても実質行ったことになるやん!(謎理論)
そう思い開発が始まりました。
千鳥とは
https://w.atwiki.jp/aniwotawiki/pages/17081.html妥協点
- 人は殺せない
- 発動しても同じ体勢から動くことができない
- なんか千鳥が平べったい
開発スケジュール
1日目
1. Mayaで千鳥っぽい3Dオブジェクトを作る。
2. 断念する。
2日目
1. 絵を描く。
2. コードを書く。
3. 術を発動する。1日目
まずAR上に表示させる千鳥を作り始めました。
3Dモデルを表示させようとMayaで制作しました。
できたもの
僕にはMayaは使いこなせませんでした。
Mayaに詳しい友達に聞いてみたりしましたが作るのが難しいらしく(正しくは面倒くさい)、3Dオブジェクトは断念することにしました。(誰か作って!)
その結果、1日目はほぼコードは書かず3Dオブジェクトも作れず終了。
もっと早く3Dオブジェクト諦めればよかった!!!!!!!2日目
3Dオブジェクトは断念し画像アニメーションで千鳥することにしました。
この時点でかなりニジゲンノモリのARの劣化になってしまったのですが交通費がないと思えばまあこれでもいいと自分に言い聞かせました。
ということで自分で絵を描いてみました。絵は普段描かないのでクッソ時間かかりました。友達に協力してもらったりしてできた画像が下の4つ
こいつらをアニメーションさせることに決定。
下手ですがまあ初心者なので許してください。
ということで、ここまででだいたい1日半くらい終わってしまってます。
ここまで来てやっとコードを書くことができました。viewController.swiftimport UIKit import SceneKit import ARKit class ViewController: UIViewController, ARSCNViewDelegate { @IBOutlet var sceneView: ARSCNView! let count = 0 var chidoriNode1: SCNNode! var chidoriNode2: SCNNode! var chidoriNode3: SCNNode! var chidoriNode4: SCNNode! var rand: Int = 1 override func viewDidLoad() { super.viewDidLoad() // Set the view's delegate sceneView.delegate = self // Show statistics such as fps and timing information sceneView.showsStatistics = true //千鳥をUImageとして取り込みそれぞれノードにして変数に入れる。 chidoriNode1 = (setImageToScene(image: UIImage(named:"Chidori1.png")!)) chidoriNode2 = (setImageToScene(image: UIImage(named:"Chidori2.png")!)) chidoriNode3 = (setImageToScene(image: UIImage(named:"Chidori3.png")!)) chidoriNode4 = (setImageToScene(image: UIImage(named:"Chidori4.png")!)) Timer.scheduledTimer(timeInterval: 0.05, target: self, selector: #selector(activationChidori), userInfo: nil, repeats: true) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // Create a session configuration let configuration = ARWorldTrackingConfiguration() // Run the view's session sceneView.session.run(configuration) } //画像の大きさとか設定するそしてノードにする private func createPhotoNode(_ image: UIImage, position: SCNVector3) -> SCNNode { let node = SCNNode() let scale: CGFloat = 1.2 let geometry = SCNPlane(width: image.size.width * scale / image.size.height, height: scale) geometry.firstMaterial?.diffuse.contents = image node.geometry = geometry node.position = position return node } //ノードを返す private func setImageToScene(image: UIImage) -> SCNNode { var node: SCNNode! if let camera = sceneView.pointOfView { // カメラから見た座標を設定。 let position = SCNVector3(x: 0, y: -0.2, z: -2.0) let convertedPosition = camera.convertPosition(position, to: nil) //大きさとか画像を設定してノードにする。 node = createPhotoNode(image, position: convertedPosition) return node } return node } //0.05秒ごとに千鳥を発動する @objc func activationChidori() { //前回表示された千鳥を消す sceneView.scene.rootNode.enumerateChildNodes{ (existingNode, _) in existingNode.removeFromParentNode() } var newRand = Int.random(in:1...4) if(rand == newRand) { newRand+=1 } rand = newRand //ランダムに千鳥を発動する。 switch rand { case 1: self.sceneView.scene.rootNode.addChildNode(chidoriNode1) case 2: self.sceneView.scene.rootNode.addChildNode(chidoriNode2) case 3: self.sceneView.scene.rootNode.addChildNode(chidoriNode3) case 4: self.sceneView.scene.rootNode.addChildNode(chidoriNode4) case 5: self.sceneView.scene.rootNode.addChildNode(chidoriNode1) default: self.sceneView.scene.rootNode.addChildNode(chidoriNode1) } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // Pause the view's session sceneView.session.pause() } }case分や画像読み込みが長ったらしくて、for文とかでまとめれそうですが、for文の中でUIImageを取得しようとするとなぜか取得結果がnilになってしまうのでシャーナシこんな形になってしまいました。
誰か原因分かる人コメントください。
要は0.05秒ごとにさっきの画像4枚をランダムに表示させてアニメーションっぽくしてます。
この方の記事がとても参考になりました。ありがとうございます。
https://qiita.com/k-boy/items/e18e74e8b341f5d5675d結果
千鳥発動できました。
今日のKIT Developer Advent Calendar 2019では千鳥を体得してみた話を書きました。https://t.co/DEcNsP7TFB#千鳥#NARUTO #AdventCalendar2019 pic.twitter.com/Wo0mGTHzif
— 石田大智 (@Dishida07) December 15, 2019終わりに
雷のクオリティが低いとかいう人は僕に3Dオブジェクト提供してください。.daeファイルに変換してもらえると助かります。
絵とかMayaとか触ってたらコードを書いていたのは実質2時間くらいでした。もう2度とMayaなんて触りたくないです。
まあ2日で千鳥を習得できたので「NARUTO」の世界では火影くらい余裕で慣れてしまうかもしれませんね。補足
- 作者は厨二病ではありません。
- 20歳の大学生が一人で家で千鳥をしてても悲しくなんてありません。
- 投稿日:2019-12-15T20:03:34+09:00
RxSwiftでUXを考慮した(Gmailアプリライクな)詳細画面を実装する
この記事は、Classi Advent Calendar 2019 の15日目の記事です。
こんにちは、ClassiのiOSアプリエンジニアの@yoko-yanです。
Classiは、サーバーサイド、特にRubyエンジニアが多くて、サーバーサイドの記事が多いと思うんですが、アプリも負けずに書いていきたいと思っているので、よろしくお願い致します。
最近は社内でRxSwift勉強会を開催するなど、社内のRxSwiftの啓蒙活動を行っています。そういったこともあり、せっかくなので、RxSwiftで試行錯誤して作った部分を公開できる範囲で記事にしてClassiのアプリも、しっかり作っているぞ的なイメージを持たせられればと思っています。
UX(ユーザーエクスペリエンス)を考慮した詳細画面とは
さて、現在開発に携わっているアプリの中では、いろいろな工夫を施しているんですが、その中でも、UXの体験向上として、工夫した箇所があるんですが、一覧画面と詳細画面という構成の中で、詳細の前後のページにアクセスしやすくしたいので、詳細画面で左右にスワイプすることで、次の詳細データや前のの詳細データを表示する体験があります。
これは、Gmailアプリが実装しているような、各メールの詳細ビューで左右にスワイプすることで、次の前のメールを表示する方法をイメージしてもらえれば、分かりやすいかと思います。
記事の内容について
前述したものを実現すべく、RxSwfitを使って、どんな風に実装したかを、記事にしたいと思います。
今回、実際に動かせるソースをGithubに公開できるとよかったんですが、時間の都合で用意できなかったので
別途、公開できるタイミングがあれば、公開したいなと思うので、今回は、ご了承ください。具体的な実装の解説
アプリのよくある実装として、一覧画面では、一覧データ取得のAPIを叩いて、あらかじめ決められた数(例えば20件など)を取得して一覧画面を作成していると思うので、Gmailのように一覧画面から詳細画面に遷移し、そこでスワイプで前後のデータに遷移できるように実装するには
1. スワイプで遷移した際のデータの切れ目で、ページングのAPIリクエストを実行
2. 追加で取得したデータを一覧ページとスワイプ画面で同期を取るという実装が必要になります。
これを、RxSwiftで実装してみます。
まず、スワイプでのページングを実装するにあたり、UICollectionViewだったり、UIPageViewControllerだったりで、いろいろ実装方法はあるかと思いますが、今回は、UIPageViewControllerで実装しました。
その理由は、すでに詳細画面をUIViewControllerとして実装していたので、それをそのまま使いたく、UIPageViewControllerなら、それを呼び出すだけでよかったのと、スワイプのいらない詳細画面は、直接、詳細画面のUIViewControllerを呼び出すことが出来るようにしたかったからです。概要
めちゃくちゃ雑ですが、ざっくり言うと、こんな感じで、ストリームを共有しています。
PageViewControllerについて
まず、PageViewControllerをどのように実装しているのかを見ていきます。
Item.swiftstruct Item: Decodable { let id: String let name: String }このアプリでは、上記のようなStructを扱うとして、以下のように実装しています。
PageViewController.swiftimport UIKit import RxSwift import RxCocoa class PageViewController: UIPageViewController { private let paginationRequestTrigger: PublishRelay<Void> private var items: [Item] = [] let disposeBag = DisposeBag() init(itemsStream: Observable<[Item]>, paginationRequestTrigger: PublishRelay<Void>, selectedItem: Any) { self.paginationRequestTrigger = paginationRequestTrigger super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) dataSource = self delegate = self let binder = Binder(self) { (vc, items: [Item]) in if vc.items.isEmpty { guard let index = items.firstIndex(where: { $0.id == selectedItem.id }) else { return } vc.items = items vc.setupPageViewController(initialIndex: index) } else if !items.isEmpty { vc.items = items } } itemsStream .filter { !$0.isEmpty } .bind(to: binder) .disposed(by: disposeBag) } required init?(coder aDecoder: NSCoder) { fatalError() } } private extension PageViewController { func setupPageViewController(initialIndex: Int) { setViewControllers([createDetailPageViewController(of: initialIndex) ?? UIViewController()], direction: .forward, animated: true, completion: nil) } func createDetailPageViewController(of index: Int) -> UIViewController? { guard items.indices.contains(index) else { return nil } // index out of range let item = items[index] let vc = DetailViewController() vc.item = item vc.view.tag = index return vc } } extension PageViewController: UIPageViewControllerDataSource { func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { let currentIndex = viewController.view.tag return createDetailPageViewController(of: currentIndex - 1) } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { let currentIndex = viewController.view.tag return createDetailPageViewController(of: currentIndex + 1) } } extension PageViewController: UIPageViewControllerDelegate { func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { guard let nextIndex = pendingViewControllers.first?.view.tag else { return } guard items.count - 1 == nextIndex else { return } paginationRequestTrigger.accept(()) } }このPageViewControllerは、一覧画面で、セル(アイテム)をクリックした際に、インスタンス化されて呼び出されます。
このPageViewControllerでやってるRxSwiftを活用した実装のポイントとしては
- (1) 一覧取得APIを実行しているストリームを購読し、ListViewController(一覧画面)とデータを同期する
- (2) スワイプ時に表示するデータがない場合、ストリームを実行し、追加取得する
の2つです。
(1) 一覧取得APIを実行しているストリームを購読し、ListViewController(一覧画面)とデータを同期する
インスタンス生成時に一覧画面から、一覧取得APIを実行しているストリームと、そのトリガーと、選択されたデータを渡しています。
init(itemsStream: Observable<[Item]>, paginationRequestTrigger: PublishRelay<Void>, selectedItem: Any) {
引数 説明 itemsStream 一覧取得APIを実行し、 itemの配列が流れるストリーム paginationRequestTrigger itemsStreamを現在のページ指定を元に追加データを取得するトリガー selectedItem 一覧画面で選択したセルのデータ 以下の箇所で、ストリームに値が流れてきた際に、必要に応じてDetaiViewControllerを再セットします。
let binder = Binder(self) { (vc, items: [Item]) in if vc.items.isEmpty { guard let index = items.firstIndex(where: { $0.id == selectedItem.id }) else { return } vc.items = items vc.setupPageViewController(initialIndex: index) } else if !items.isEmpty { vc.items = items } } itemsStream .filter { !$0.isEmpty } .bind(to: binder) .disposed(by: disposeBag)(2) スワイプ時に表示するデータがない場合、ストリームを実行し、追加取得する
スワイプで次々にデータを表示する際、DetailViewControllerを生成して表示していますが、元になるデータは、インスタンス生成時に、initで渡されたデータなので、スワイプを続けているとデータの切れ目に辿りつきます。
そのタイミングで、一覧取得APIを実行して、新たにデータを追加取得したいのですが、ここで取得したデータは、一覧でも反映して(同期を取りたい)、一覧画面に戻った時に、同じストリーム実行せずに済ませたいので、initで渡されたトリガーを元に、一覧画面側のストリームを実行し、 (1)の箇所で、結果を反映させます。extension PageViewController: UIPageViewControllerDelegate { func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { guard let nextIndex = pendingViewControllers.first?.view.tag else { return } guard items.count - 1 == nextIndex else { return } paginationRequestTrigger.accept(()) } }このように実装することで、一覧画面と同じストリームや同じパラメータセットのAPIを、わざわざ実行せずに済むようにしました。
ListViewControllerについて
一覧画面を表示するListViewControllerについて、実装を見ていきます。
[補足]
余談ですが、このアプリの実装は、MVVM+RxSwiftで実装しているので、この記事でも、それで実装しているコードで記述します。
また、MVVM+RxSwiftで実装するにあたっては、以下の書籍を参考にさせて頂いています。
RxSwift研究読本3 ViewModel設計パターン入門
この書籍の中のsergdort氏のViewModelパターンを採用しています。ListViewControllerは、以下のように実装しています。
ListViewController.swiftfinal class ListViewController: UIViewController { private let disposeBag = DisposeBag() private let viewModel = ListViewModel() private let dataSource = DataSource() @IBOutlet private weak var tableView: UITableView! private let reloadRequestTrigger = PublishRelay<Void>() private let paginationRequestTrigger = PublishRelay<Void>() private let changeDataTrigger = PublishRelay<Item>() // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() bind() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } // MARK: - Private private func bind() { let input = ListViewModel.Input( viewWillAppearStream: rx.sentMessage(#selector(viewWillAppear(_:))).map { _ in }, viewDidReachBottom: tableView.rx.reachedBottom.asObservable(), reloadRequestTrigger: reloadRequestTrigger.asObservable(), paginationRequestTrigger: paginationRequestTrigger.asObservable(), changeDataTrigger: changeDataTrigger.asObservable() ) let output = viewModel.transform(input: input) output.itemsStream .bind(to: tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) do { let binder = Binder(self) { (vc, error: Error) in // error } output.error .bind(to: binder) .disposed(by: disposeBag) } do { let binder = Binder(self) { (vc, item: Item) in let detailVC = PageViewController(itemsStream: output.itemsStream, paginationRequestTrigger: vc.paginationRequestTrigger, selectedItem: item) vc.navigationController?.pushViewController(detailVC, animated: true) } tableView.rx .modelSelected(Item.self) .bind(to: binder) .disposed(by: disposeBag) } output.loading .bind(onNext: { $0 ? LoadingIndicator.show() : LoadingIndicator.dismiss() }) .disposed(by: disposeBag) } } private class DataSource: NSObject, UITableViewDataSource { typealias Element = [Item] private var items: [Item] = [] func numberOfSections(in tableView: UITableView) -> Int { return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let item = items[indexPath.row] let cell = UITableViewCell(style: .default, reuseIdentifier: "cell") cell.textLabel?.text = item.name return cell } } extension DataSource: RxTableViewDataSourceType { func tableView(_ tableView: UITableView, observedEvent: RxSwift.Event<Element>) { Binder(self) { dataSource, elements in dataSource.items = elements tableView.reloadData() } .on(observedEvent) } } extension DataSource: SectionedViewDataSourceType { func model(at indexPath: IndexPath) throws -> Any { return items[indexPath.row] } }ViewModelの処理の概要は、後述します。
このListViewControllerでやってることの概要としては
- (1) UIイベントに依存せず、任意で発火させたいトリガーを宣言する
- (2) 一覧取得APIを実行の責務は、ViewModelに持たせるため、各トリガーをViewModelへ渡す
- (3) ViewModelで流れたストリームの結果を処理
- (4) tableViewの各イベントをストリームで処理
(1) UIイベントに依存せず、任意で発火させたいトリガーを宣言する
private let reloadRequestTrigger = PublishRelay<Void>() private let paginationRequestTrigger = PublishRelay<Void>() private let changeDataTrigger = PublishRelay<Item>()ボタンクリックなど、UIイベントをトリガーとする場合は、RxCocoaで、イベントをObservable化すればいいですが、そうでなく任意に発火したい場合は、トリガー用のObservableを用意します。
各トリガーの説明は以下になります。
変数 説明 発火場所 reloadRequestTrigger 現在のページ番号を考慮しない初回のAPIリクエストを実行するトリガー Pull to Refresh やエラー時のリトライなど paginationRequestTrigger 現在のページ指定を元に追加データを取得するトリガー スクロールイベントなどに起因せず、任意に追加読み込みをしたい箇所 changeDataTrigger 特定のデータ (Item)だけを入れ替えるトリガー 編集画面から戻ってきた際に、該当するデータだけ反映したい場合など (2) 一覧取得APIを実行の責務は、ViewModelに持たせるため、各トリガーをViewModelへ渡す
各トリガーごとに、異なった処理が必要ですが、それぞのロジックは、ViewController側ではなくViewModelで処理を行います。
これは、ViewModelをテスタブルにして、ViewController側は何も気にせずに処理を実行させたいという意図があります。
現在、アドバイザーとしていろいろ教えて頂いているスーパーなiOSエンジニアの方が、『ViewControllerはアホな方がいい』と言ってました。
この格言、めっちゃ気に入りましたwViewModelへバインドしている箇所です。
let input = ListViewModel.Input( viewWillAppearStream: rx.sentMessage(#selector(viewWillAppear(_:))).map { _ in }, viewDidReachBottom: tableView.rx.reachedBottom.asObservable(), reloadRequestTrigger: reloadRequestTrigger.asObservable(), paginationRequestTrigger: paginationRequestTrigger.asObservable(), changeDataTrigger: changeDataTrigger.asObservable() )ここでのviewWillAppearStreamについては、その名前の通り、UIViewControllerのviewWillAppearに反応してフックしているトリガーです。
ここでのreachedBottomについては、UIScrollViewを拡張して、RxCocoaでスクロールのボトムを検知しているものです。(3) ViewModelで流れたストリームの結果を処理
ViewModelで組んだストリームの結果を、Viewに反映するためのロジックです。
let output = viewModel.transform(input: input) output.itemsStream .bind(to: tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) do { let binder = Binder(self) { (vc, error: Error) in // error } output.error .bind(to: binder) .disposed(by: disposeBag) }リクエストが成功した際のストリームをtableViewのdataSourceへバインドしているのと
リクエストが失敗した際のストリームをエラー処理する為の、Binderへバインドしています。(4) tableViewの各イベントをストリームで処理
これについては、特に目新しいことをやっていることもないので、割愛させて頂きます。
ListViewModelについて
前述させて頂いたように、@yimajoさんの書籍を元に、sergdort氏のViewModelパターンを採用しています。
こちらに関しては、@yimajoさんの書籍を参照頂くか、以下のサイトを参照ください。
ViewModel in RxSwift world - Serg Dort - MediumこのViewModelの責務としては、ItemをAPI経由で取得し、ストリームとしてViewControllerへ返すだけです。
が!、今回の実装の肝となる部分なので、少々細かく説明します。ItemResponse.swiftstruct ItemResponse: Decodable { let items: Observable<[Item]> public let pages: Pages } public struct Pages: Codable { public let currentPage: Int public let nextPage: Int? }APIから返ってくるレスポンスが上記のようだと仮定して、ListViewModelの処理は以下のようになります。
ListViewModel.swiftimport Foundation import RxSwift import RxCocoa import APIKit struct ListViewModel { private let disposeBag = DisposeBag() struct Input { let viewWillAppearStream: Observable<Void> let viewDidReachBottom: Observable<Void> let reloadRequestTrigger: Observable<Void> let paginationRequestTrigger: Observable<Void> let changeItemTrigger: Observable<Item> } struct Output { let loading: Observable<Bool> let itemsStream: Observable<[Item]> let error: Observable<Error> } func transform(input: Input) -> Output { let session = Session.shared let nextPageNumber = PublishRelay<Int?>() let startRequestTrigger = input.viewWillAppearStream.take(1).map { 1 } // 1 is a first page number let itemsRequestTrigger = input.reloadRequestTrigger.map { 1 } // 1 is a first page number let paginationRequestTrigger = PublishRelay.merge( input.viewDidReachBottom.asObservable() .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance), input.paginationRequestTrigger.asObservable() .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance) ) .withLatestFrom(nextPageNumber) .compactMap { $0 } let load = PublishRelay.merge(startRequestTrigger, paginationRequestTrigger, itemsRequestTrigger).share() let sequence = load .flatMapLatest { nextPage -> Observable<Event<ItemResponse>> in return session.rx .send(ItemsRequest(page: nextPage)) .asObservable() .materialize() } .share() let elements = sequence.compactMap { $0.event.element }.share() elements .map { $0.pages.nextPage } .bind(to: nextPageNumber) .disposed(by: disposeBag) let elementsWithPagination = elements .scan([]) { $1.pages.currentPage == 1 ? $1.items : $0 + $1.items } .startWith([]) .share(replay: 1) let elementsWithChanged = input.changeItemTrigger .withLatestFrom(elementsWithPagination) { ($0, $1) } .map { arg -> [Item] in let item = arg.0 var items = arg.1 if let index = items.firstIndex(where: {$0.id == item.id}) { items[index] = item } return items } let itemsStream = Observable .merge(elementsWithPagination, input.reloadRequestTrigger.map { [] }, elementsWithChanged) .share(replay: 1) let loading = PublishRelay.merge(load.map { _ in true }, sequence.map { _ in false }) return Output( loading: loading, itemsStream: itemsStream, error: sequence.compactMap { $0.event.error } ) } }このListViewModelでやってることの概要としては
- (1) 各トリガーに流すデータをページ番号で統一
- (2) 各トリガーをマージして、トリガーが発火したらストリームが実行されるようにする
- (3) APIを実行して、ページ番号を購読
- (4) APIの実行結果を、ストリームの中で保持している前回の結果にマージ
- (5) 部分的に変更があった場合、ストリームの中で保持している結果を元に差し替える
- (6) APIリクエストが必要なトリガーと、APIリクエストが不要となるトリガーをマージして、ストリームを構成する
(1) 各トリガーに流すデータをページ番号で統一
最終的に、ストリームに流れるデータは、ページ番号を起因にして、変化するので、発火する際に流す値は、ページ番号を長す
viewWillAppearやreloadRequest発火時のリクエストについては、ViewControllerからページ番号を渡す必要がないので、ViewModelの中でストリームを直接、書き換えます。let nextPageNumber = PublishRelay<Int?>() let startRequestTrigger = input.viewWillAppearStream.take(1).map { 1 } // 1 is a first page number let itemsRequestTrigger = input.reloadRequestTrigger.map { 1 } // 1 is a first page number追加読み込みによるAPIリクエストは、一覧をスクロールして最下部に到達した場合、任意に読み込み発生させたい(PageViewController内)の両方のパターンがあるので、その二つのトリガーをマージします。
let paginationRequestTrigger = PublishRelay.merge( input.viewDidReachBottom.asObservable() .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance), input.paginationRequestTrigger.asObservable() .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance) ) .withLatestFrom(nextPageNumber) .compactMap { $0 }連打防止の為、throttleで無駄なストリームが流れないように制御しています。
(2) 各トリガーをマージして、トリガーが発火したらストリームが実行されるようにする
ここでは、3パターンあるトリガーのうち、どれかが発火されたらストリームが流れるようにマージします。
let load = PublishRelay.merge(startRequestTrigger, paginationRequestTrigger, itemsRequestTrigger).share()(3) APIを実行して、ページ番号を購読
一覧取得のAPIリクエストを実行しています。
let sequence = load .flatMapLatest { nextPage -> Observable<Event<ItemResponse>> in return session.rx .send(ItemsRequest(page: nextPage)) .asObservable() .materialize() } .share()ここでは、ItemsRequestの中身については、触れません。
単に、APIKitをラッピングしているのみです。RxSwiftのmaterializeで、APIの成功のストリームと失敗のストリームに分けて、成功のストリームが流れたときに、ページ番号を購読しています。
let elements = sequence.compactMap { $0.event.element }.share() elements .map { $0.pages.nextPage } .bind(to: nextPageNumber) .disposed(by: disposeBag)(4) APIの実行結果を、ストリームの中で保持している前回の結果にマージ
(成功の)ストリームの中で、保持しているデータに前回の結果が含まれていれば、それに新しい結果を追加して、それをストリームに流すようにします。
let elementsWithPagination = elements .scan([]) { $1.pages.currentPage == 1 ? $1.items : $0 + $1.items } .startWith([]) .share(replay: 1)これは、APIの結果が、2ページ目、3ページ目で、リクエストごとに、前ページの結果を含んでいない場合のAPIの仕様の場合で、ストリームの外から前回の結果を差し込まずに、時系列で保持している前回の値を効率よく使う為のロジックです。
最初、ストリームの外から差し込んじゃえというようなロジックを書いてたんですが、スーパーエンジニアにダメ出しされました。
今考えると当たり前ですが。汗(5) 部分的に変更があった場合、ストリームの中で保持している結果を元に差し替える
これも (6) と同じく、既にストリームの中に流れている値 (elementsWithPagination) を元に、部分的にデータを差し替えるロジックです。
編集を行った際などに、APIリクエストを伴わなずに、差し替えたいなと思った場合、上流のストリームから流してしまうと、かなり複雑なストリームになってしまうので、API実行後のストリームに対し、差し替えを行っています。let elementsWithChanged = input.changeItemTrigger .withLatestFrom(elementsWithPagination) { ($0, $1) } .map { arg -> [Item] in let item = arg.0 var items = arg.1 if let index = items.firstIndex(where: {$0.id == item.id}) { items[index] = item } return items }(6) APIリクエストが必要なトリガーと不要となるトリガーをマージして、ストリームを構成する
(5) で説明したようにAPI実行後のストリームの値に変化をつけて、その結果をViewControllerへ流したいので、本流のストリームとマージして、両方をトリガーとして、流れるようにしています。
let itemsStream = Observable .merge(elementsWithPagination, input.reloadRequestTrigger.map { [] }, elementsWithChanged) .share(replay: 1)ViewController側でこういった処理を行おうとすると、かなり助長なコードになるのが予想できるので、途中で話した『ViewControllerはアホな方がいい』というのは、非常に納得できるのではないでしょうか。
(補足) ローディングの表示について
ここまで触れてこなかったのですが、ViewController側で、くるくるのローディング表示を行うに当たって、ストリームが流れているかどうかで判断させることで、ViewControllerは、そのBool値を追いかけるだけでよくなります。
let loading = PublishRelay.merge(load.map { _ in true }, sequence.map { _ in false })上流のトリガーが発火したタイミングでtrue、ストリームが完了したタイミングでfalseを流すことで、ローディング表示のオンオフが容易になります。
DetailViewControllerについて
今回の説明では、解説は不要なので割愛させて頂きます。
最終的なストリームのイメージ
最終的に組んだストリームのイメージは、こんな感じになります。
厳密には、ちょっと違うかもしれませんが、Itemsの取得に関するストリームは、全てListViewModelの中のストリームを通って、取得している感じです。まとめ
実は、RxSwiftやリアクティブプログラミングは、Classiに入社する前は、興味はあれど、触ったことがなく、入社してからキャッチアップさせてもらったのですが、さすが教育分野の会社だけあって、社内メンバーの学習意欲の高さや、学習欲に対する理解度は高いです。
オブジェクト指向プログラミングを長くやっていたので、リアクティブプログラミングに切り替えるのは、かなり苦労しましたが、今ではかなり理解度もあがり、オブジェクト指向プログラミングに戻って、助長なプログラムを書いてしまうのが、怖いくらいになりました。
ただ、まだまだリアクティブプログラミングの奥は深く、ただでさえ、正解がないプログラミングの世界で、さらに正解がぼやけてしまう感じで、例えて言うならば、RPGゲームで、どの武器を使ってボスと戦かおうかの選択肢の中に、魔法や属性が加わって、武器の選択だけでは、戦えないといった感じです。
むしろ、リアクティブプログラミングという方法を知ってしまった今は、バル○やパルプン○といった呪文を覚えてしまった感覚にもなり、全てを破壊したり、混乱に陥れる可能性もあるヤバイスキルを身につけてしまった感もあります。
ただ、Appleも、Combineフレームワークを発表したように、時代は、リアクティブプログラミングなど、時系列を考慮したプログラミングを組めるようにならないと、取り残されていくなと感じました。
今回の実装も、複雑な処理は、ほとんど、ViewModelの中にコンパクトに収められていて、RxSwiftを導入していなければ、色々煩雑なロジックを書く必要があったなと、RxSwiftを採用したメリットをかなり感じています。
ここまで来るには、いろんな壁にぶち当たって、その都度、社内のiOSエンジニアと、アドバイスを頂いているスーパーエンジニアのご協力もあって、実装できましたので、リスペクトを兼ねて、解説する記事を書くことにしました。
おかげで、自分自身もかなり、RxSwiftやリアクティブプログラミングの理解が深まり、こういった記事が書けるようになったので、本当に感謝しています。Classiには、こんなアプリエンジニアがいるんだというようなイメージアップに繋がればいいなと思っていますので、是非、今後ともよろしくお願いいたします。
- 投稿日:2019-12-15T14:03:08+09:00
Swift:macのネットワーク上り下りスピードを取得する
Capabilities
のIncoming Connections (Server)
またはOutgoing Connections (Client)
にチェックを入れるclass Network { private var lastUpBytes: Int? private var lastDownBytes: Int? // interval秒ごとのスピード func speed(interval: Int) { let process = Process() process.launchPath = "/usr/bin/env" process.arguments = ["netstat", "-bdI", "en0"] let pipe = Pipe() process.standardOutput = pipe process.launch() let data = pipe.fileHandleForReading.readDataToEndOfFile() guard var commandOutput = String(data: data, encoding: .utf8) else { return } commandOutput = commandOutput.lowercased() let lines = commandOutput.components(separatedBy: .newlines) let stats = lines[1].components(separatedBy: .whitespaces).compactMap({ (str) -> String? in return str.isEmpty ? nil : str }) let upBytes = Int(stats[9]) ?? 0 let downBytes = Int(stats[6]) ?? 0 var upSpeed: Int = 0 var downSpeed: Int = 0 if lastDownBytes != nil && lastUpBytes != nil { upSpeed = (upBytes - lastUpBytes!) / interval downSpeed = (downBytes - lastDownBytes!) / interval } lastUpBytes = upBytes lastDownBytes = downBytes Swift.print("Upload: \(convert(byte: Double(upSpeed)))") Swift.print("Download: \(convert(byte: Double(downSpeed)))") } private func convert(byte: Double) -> String { let KB: Double = 1024 let MB: Double = pow(KB, 2) let GB: Double = pow(KB, 3) var str = "" if GB <= byte { str = String((10 * byte / GB).rounded() / 10) + " GB/s" } else if MB <= byte { str = String((10 * byte / MB).rounded() / 10) + " MB/s" } else { str = String((10 * byte / KB).rounded() / 10) + " KB/s" } return str } }
- 投稿日:2019-12-15T13:10:28+09:00
iOS13.3 @Publishedでの値更新からsinkが呼ばれなくなった?(ミス解決)
iOS13.3では@Publishedでの値更新でsinkが呼ばれない?
起きたこと
iOS13.3に更新したところ、SwiftUIのEnvironmentObject上の@Publishedの更新通知が受け取れなくなりちょっとハマりました。結局の所、もともと更新通知を受け取る処理を書き間違えていたことが原因でした。
修正前
let _ = self.model.$text.receive(on: DispatchQueue.main) .sink { (value) in print("receive") self.text = value }修正後
var task:AnyCancellable? = nil ... self.task = self.model.$text.receive(on: DispatchQueue.main) .sink { (value) in print("receive") self.text = value }きちんと戻り値のAnyCancellableを保持していないことが問題でした。考えてみれば納得なのですが、iOS13.2までは動いていたので見落としていました。戒めの意味を込めて記録しておきます。
- 投稿日:2019-12-15T01:27:29+09:00
パラメタライズドテストのメリット解説(Swift)
はじめに
本記事は Swift Advent Calendar 2019 の14日目の記事です。
パラメタライズドテストのメリットについて解説します。注意
本記事は 「YUMEMI.swift #5 ~Free Talk~」のLTで使用した資料 を元にしています。
LT時は MarkdownをVimで表示した ので、よかったら併せて見てください。用語
本記事で使う用語を説明します。
用語 意味 テストメソッド 自動テストのメソッド テストコード 自動テストのコード プロダクトコード 通常のソースコード。テストコードの対義語として使う あるメソッドのテストケースを考える
ここから本題です。
エンジニアが働けるかどうかを判定するメソッドを用意しました。
やる気が100以上でRed Bullを持っている場合のみ働けます。func canWork(motivation: Int, hasRedBull: Bool) -> Bool { if motivation < 100 { return false } if !hasRedBull { return false } return true }こちらのメソッドのテストケースは全部で何パターン必要でしょうか?
あるメソッドのテストケース数
私が考える正解は、以下の4パターンです。
motivation hasRedBull return value 99 true false 99 false false 100 true true 100 false false
motivation
を境界値分析すると、99
と100
が境界値とわかります。
さらに「100未満」と「100以上」で同値分割できるので、99
と100
の2通りのみテストすれば十分です。
hasRedBull
はBool型なのでtrue
とfalse
の2通りしかありません。2通り × 2通り = 4パターン です。
手動でテストする場合
上記のメソッドを 手動で テストする場合、エクセルなどを使って一覧表に○×を付けるのが一般的だと思います。
先ほどの表に「No」「判定」「日付」「担当」列を追加しました。
No motivation hasRedBull return value 判定 日付 担当 1 99 true false OK 2019/12/14 ウホーイ 2 99 false false OK 2019/12/14 ウホーイ 3 100 true true NG 2019/12/14 ウホーイ 4 100 false false OK 2019/12/14 ウホーイ これで十分であり、表になっているのでパターンが網羅できているかもわかりやすいです。
自動でテストする場合
上記のメソッドを 自動で テストする場合はどうでしょうか?
最も簡単な方法として、1ケース1テストメソッドとして実装することが考えられます。1ケース1テストメソッドとして実装func test_canWork_motivation_99_hasRedBull_true() { let logic = LogicSample() XCTAssertFalse(logic.canWork(motivation: 99, hasRedBull: true)) } func test_canWork_motivation_99_hasRedBull_false() { let logic = LogicSample() XCTAssertFalse(logic.canWork(motivation: 99, hasRedBull: false)) } func test_canWork_motivation_100_hasRedBull_true() { let logic = LogicSample() XCTAssertTrue(logic.canWork(motivation: 100, hasRedBull: true)) } func test_canWork_motivation_100_hasRedBull_false() { let logic = LogicSample() XCTAssertFalse(logic.canWork(motivation: 100, hasRedBull: false)) }テストケースごとにテストメソッドを実装することがあるかもしれませんが、実は以下のデメリットがあります。
網羅できているかわかりづらい
エクセルのようにテストケースが一覧になっていないので、網羅できているかわかりづらいです。同じような処理で読みづらい
canWork()
メソッドに渡す引数以外は同じ処理なので、どこが違うか注力して読み比べる必要があります。
同じ処理を繰り返すのはDRY原則にも反しています。メソッド名に悩む
テストメソッドの命名はメソッド数が増えるほど難しくなり、testCanWork2()
のように末尾に連番を振ることがあるかもしれません。
今回は引数と値をそのままメソッド名に付けましたが、適切ではない気がします。テストコードがわかりづらいとどうなるか
テストコードの可読性が低い場合、 テストコードが保守されなくなる 可能性が高まります。
テストコードはプロダクトコードに比べて可読性が無視されがちですが、それだとテストが失敗しても直せなくなり、テストが成功しなくてもPRを通すようになります。自動テストはうまく使うとプロダクトコードを安全に破壊できるので、もったいないです。
どうやったらテストコードがわかりやすくなる?
先ほどのデメリットにも上げた通り、 自動テストでもエクセルのように一覧になっていたら わかりやすくなると思いませんか?
自動テストでテストケースを一覧にする// | motivation | hasRedBull | return value | // | ---------: | :--------- | :----------- | // | 99 | true | false | // | 99 | false | false | // | 100 | true | true | // | 100 | false | false | // // ↓ // let testCases: [(motivation: Int, hasRedBull: Bool, expect: Bool)] = [ ( 99, true, false), ( 99, false, false), (100, true, true ), (100, false, false) ]どうでしょうか?
テストケースをタプルで表して配列に入れることで、エクセルのように一覧で見れるようになりました。タプルはtypealiasを使って別名を付けることで、型のように扱えます。
タプルに別名を付けるtypealias TestCase = (motivation: Int, hasRedBull: Bool, expect: Bool) let testCases: [TestCase] = [ ( 99, true, false), ( 99, false, false), (100, true, true ), (100, false, false) ]ちょっとした工夫ですが、より可読性が上がりました。
構造体を定義するより簡単に使えるので、型を使い捨てる場合には便利です。あとは作成した配列をループさせ、1つずつアサーションすればOKです。
テストメソッドの全体は以下の通りです。工夫したテストメソッドの全体func test_canWork() { typealias TestCase = (line: UInt, motivation: Int, hasRedBull: Bool, expect: Bool) let testCases: [TestCase] = [ (#line, 99, true, false), (#line, 99, false, false), (#line, 100, true, true ), (#line, 100, false, false) ] for (line, motivation, hasRedBull, expect) in testCases { let logic = LogicSample() let result = logic.canWork(motivation: motivation, hasRedBull: hasRedBull) XCTAssertEqual(result, expect, line: line) } }
#line
はソースコードの行番号を表し、これを渡すことでテストの失敗時に対象の要素でエラーになってくれます。
ただ、エラーメッセージに変数名が出力されないため、若干わかりづらいです。
いちいち行番号を渡すのも冗長なので、このあたりをいい感じに吸収してくれるライブラリを作りたいです。パラメタライズドテスト
引数のみを切り替えて実行するテストを パラメタライズドテスト といいます。
言語によって呼び方が異なることがあります。
言語 名前 Go テーブル駆動テスト Java (JUnit) パラメータ化テスト 他言語では一般的なのに、なぜかSwiftではあまり知られていません。
以下のようにメリットも多く、簡単に導入できるのでオススメです。
- テストパターンが網羅できているかひと目でわかる
- 1メソッドで済むので処理が重複せず、命名にも困らない
- テストケースを追加・変更・削除しやすい
1行変更するのみおまけ
JavaのテストフレームワークであるSpockには「Data Tables」という機能があり、より直感的にテストケースを記述できます。
Spockによるテストclass MathSpec extends Specification { def "maximum of two numbers"(int a, int b, int c) { expect: Math.max(a, b) == c where: a | b | c 1 | 3 | 3 7 | 4 | 7 0 | 0 | 0 } }Swiftでは、独自演算子とFunction Builderを使って実現できそうな気もします。
強い方教えてください…。おわりに
パラメタライズドテストのメリットが少しでも伝わっていると嬉しいです?
以上、 Swift Advent Calendar 2019 の14日目の記事でした。
15日目は @freddi_ さんの記事です。参考リンク
- Swift で ParameterizedTest をやってみた話/swift-parameterized-test - Speaker Deck
私がパラメタライズドテストを知ったきっかけのスライドです
より詳細に解説されているので、ぜひ読んでください- yumemi-swift-5-sample/LogicSampleTests.swift at master · uhooi/yumemi-swift-5-sample
本記事のサンプルコードです- https://twitter.com/lovee/status/1200595966782406656?s=20
タプルに別名を付けるのは、Twitterでアドバイスを頂きました
- 投稿日:2019-12-15T00:58:37+09:00
SwiftPMとSlackKitを使ってSlackBotを作った話
この記事は Willgate Advent Calendar 2019 の15日目の記事です。
昨日は @ryota_hnk さんの
OKE(Oracle Container Engine for Kubernetes) を Rancher で監視する
でした。
本日は、ウィルゲートでPHPやJavaScript、CSSで開発している@N06ARです!
よろしくお願いします!はじめに
私は普段の業務でSwiftを書くことがありませんが、最近で昔作ったiOSアプリを
Swift1
からSwift5
にジャンプアップデートする機会がありました。
久しぶりのSwiftが楽しかったので、SlackKitを使いSlackBotを作ってみました。
そこでこの記事では、SlackKitを使ったSlackBotの導入をご紹介します!ここで書かれていること
この記事では次の3点について書いています。
- SlackKitの導入方法
- SlackKitを使ったSlackでのメッセージの送受信
- SlackKitで作ったSlackBotを使ってみての感想x
この3点で、私が失敗や上手く行かなかったところは(
)を書いています。
もし、この記事を読んで作ろう!と思った方は、参考にしてみてください!開発環境
開発環境は、次のバージョンで開発しました。
バージョン OS macOS Catalina Swift 5.1 SlackKit 4.5.0 Xcode 11 0.事前準備
コードを書き始める前に、事前に必要なものを準備します。
0-1.SlackでAPI Tokenの発行
はじめにSlackからAPI Tokenを取得してきます。
このURLからAPI Tokenを発行します。Botの名前を入力し、botを追加するボタンを押すと次の画像が一部として表示される画面が表示されます。
画像のところにAPI Tokenがあるのでメモしておいてください。
(画像のTokenはダミーです)0-2.(オプション)LibreSSLのインストール
SlackKitの依存パッケージの中にSwiftNIOがあり、LibreSSLが必要になります。
インストールしていなければ1-5.ビルドでビルドに失敗するのでインストールします。()
$ brew install libressl1.SwiftPMの設定
SwiftPMを使いSwiftパッケージディレクトリ(プロジェクトフォルダ)を作成し、
SlackKit
を使えるようにします。1-1.SlackKitの実行パッケージディレクトリの作成
次のコマンドを実行して、SlackBotを作るディレクトリを作成し、実行パッケージディレクトリを作成します。
swift package init --type executable
を実行した時に特に入力を求められませんが、問題ありません。$ mkdir 【プロジェクト名】 $ cd 【プロジェクト名】 $ swift package init --type executable1-2.(オプション)実行確認
次のコマンドを実行した後
Hello, world!
と表示されたら準備完了です!$ swift build $ ./.build/debug/【プロジェクト名】1-3.SwiftPMで使用するパッケージにSlackKitを追加
次に
Package.swift
を編集します。
Package.swift
をエディタで開きdependencies
の箇所に今回使用するパッケージのSlackKit
を追加します。Package.swiftimport PackageDescription let package = Package( name: "【プロジェクト名】", dependencies: [ .package(url: "https://github.com/pvzig/SlackKit.git", .upToNextMinor(from: "4.5.0")) ] )1-4. (オプション)テストもしたい場合
テストもしたい場合は、
targets
にもSlackKitのパッケージを追加する必要があります。
SlackKitの追加を忘れると1-5.ビルドでビルドに失敗します。()
Package.swiftimport PackageDescription let package = Package( name: "【プロジェクト名】", dependencies: [ .package(url: "https://github.com/pvzig/SlackKit.git", .upToNextMinor(from: "4.5.0")) ], targets: [ .target( name: "【プロジェクト名】", dependencies: ["SlackKit"]), .testTarget( name: "【プロジェクト名】Tests", dependencies: ["【プロジェクト名】","SlackKit"]), ] )1-5.ビルド
Package.swiftを編集した後に、
swift build
をするとSlackKitを取得し使えるようになります。$ swift build
1-6.(オプション)XcodeやAppCodeを使う場合
XcodeやAppCodeを使いたい感じると思います。その時は次のコマンドを実行して
.xcodeproj
を生成してください。$ swift package generate-xcodeproj
ただし、Package.swiftにパッケージを追加変更する度に、
.xcodeproj
を再生成する必要があります。()
AppCodeで発生しましたが、プロジェクト内のファイル名を変更したときビルドエラーになりました。
その時も再生成してあげると良さそうです。()
2.SlackBotのコードを書く
ここから実際にSlackKitを使ってSlackbotのコードを書いていきます。
2-1.main.swiftの編集
まずはじめに、今回SwiftPMで作成した のエントリーポイントの
main.swift
を編集します。
今回は、SlackBotの機能をmain.swift
に書かずSlackBot
クラスを作成し分離します。
SlackBot
のインスタンス生成時に、0-1.SlackでAPI Tokenの発行 で準備したAPI Tokenを引数に入れます。Sources/【プロジェクト名】/main.swiftimport Foundation SlackBot(apiToken: "xoxb-XXXXXXXXXXXXXXXXXXXX") RunLoop.main.run()2-3.SlackBotのクラスを作成
次に、
SlackBot
クラス用のファイルを作成し、コードを作成していきます。
コンストラクタ(init
)で、API Tokenを受け取れるようにして、
SlackKitでSlack Web API
とSlack Real Time Messaging API
使えるようにするためにAPI Token
を渡します。
API Token
を渡すには、addWebAPIAccessWithToken
とaddRTMBotWithAPIToken
を使用します。Sources/【プロジェクト名】/SlackBot.swiftimport Foundation import SlackKit class SlackBot { /// SlackBotのKit let slackkit = SlackKit() init(apiToken: String) { slackkit.addWebAPIAccessWithToken(apiToken) slackkit.addRTMBotWithAPIToken(apiToken) } }2-4.メッセージを受け取る
次にSlackからメッセージを受け取ります。
メッセージは、SlackKitのnotificationForEvent
を使い受け取ります。
受け取ったメッセージの内容は、クロージャのevent.message
に入っており、
event.message
には発言したユーザIDやチャンネルID、スレッドID、メッセージの文章等が入っています。Sources/【プロジェクト名】/SlackBot.swiftimport Foundation import SlackKit class Slackbot { /// SlackBotのKit let slackkit = SlackKit() init(apiToken: String) { slackkit.addWebAPIAccessWithToken(apiToken) slackkit.addRTMBotWithAPIToken(apiToken) slackkit.notificationForEvent(.message) { [weak self] (event, client) in // ここに聞いたメッセージを受信したときのメッセージを書く! } } }【余談】SlackKitのサンプルコードのように書くときの注意
サンプルコードのこの箇所のように、実装した場合でAPI Tokenを渡すと
listen
メソッド実行時に、client
クラスが生成されないためかnil
のためguard
構文でreturn
されるので何もせずに終わります。()
Sources/【プロジェクト名】/SlackBot.swiftimport Foundation import SlackKit class SlackBot { /// SlackBotのKit let slackkit = SlackKit() init(apiToken: String) { slackkit.addWebAPIAccessWithToken(apiToken) slackkit.addRTMBotWithAPIToken(apiToken) slackkit.notificationForEvent(.message) { [weak self] (event, client) in self?.listen(client?.client, message: event.message) } } private func listen(_ client: Client?, message: Message?) { guard let client = client, let message = message, let text = message.text, let channel = message.channel else { return } } }2-5.メッセージを送信する
メッセージを受け取れるようになったので、今度はメッセージを送れるようにします。
今回は、全てのメッセージに大してこんにちは
と返事をするメッセージを送信する処理を書きます。
メッセージの送信には、SlackKit
のWebAPI
クラスのsendMessage
メソッドを使用します。
sendMessage
メソッドの引数には、送信するチャンネル
、表示するユーザネーム
、アイコン画像のURL
、メッセージ
を入れてください。Sources/【プロジェクト名】/SlackBot.swiftimport Foundation import SlackKit class NanaChangbot { /// SlackBotのKit let slackkit = SlackKit() init(apiToken: String) { slackkit.addWebAPIAccessWithToken(apiToken) slackkit.addRTMBotWithAPIToken(apiToken) slackkit.notificationForEvent(.message) { [weak self] (event, client) in // ここに聞いたメッセージを受信したときのメッセージを書く! slackkit.webAPI?.sendMessage( channel: message.channel, text: "こんにちは", username: "【botの名前】", iconURL: "【アイコン画像のURL】", success: nil, failure: { (error) in print("メッセージ送信失敗:\(error)") } ) } } }これでbotが入ったチャンネルか、DMでメッセージを送った時に
こんにちは
と返事するようになりました!
メッセージ以外にもAttachmentクラスを使いAttachmentをつけることもできます。【余談】アイコン画像とBotの名前がない場合
実のところ
表示するユーザネーム
とアイコン画像のURL
は、必須ではないので引数に入れず送信することができます。
仮に引数なしで送信をした場合は、次の画像の結果になります。
アイコン画像がbotの画像
になり、名前がbot
になります。()
そのため、名前と画像のURLを渡たしてあげる必要があります。2-6.起動!!
最後ビルドして起動します。
起動には、デバックモードの起動とリリースモードの起動があります。2-6-1.デバックモードで起動!
ビルドは今まで通り次のコマンドを入力してください。
$ swift build
最後に次のコマンドで起動します!
コマンド実行するとシェルの入力はできなくなるので、nohup
を使いバックグラウンド実行すると良いです。.build/debug/【プロジェクト名】2-6-2.リリースモードで起動!
ビルドコマンドは今までとは少し異なります。
$ swift build -c release最後に次のコマンドで起動します!
.build/release/【プロジェクト名】3.macOS以外で起動したい!
SlackBotを動かすのに
Linux
やサービスを使って動かしたいことがあると思います。
そこで、Heroku
とDocker
で動作させる方法をご紹介します。3-1.Herokuにデプロイしたい場合
Herokuにデプロイする場合は、
HerokuCLI
を使用します。
そのため、次のコマンドでHerokuCLIをインストールしてください。brew tap heroku/brew && brew install heroku3-1-1.Heroku用設定ファイルの追加
はじめに、Heroku用の設定ファイルを追加します。
次のコマンドを実行して、.swift-version
とProcfile
を作成してください。$ echo 5.1 > .swift-version $ echo slackbot: .swift-bin/【プロジェクト名】 > ProcfileHerokuでデプロイするために、heroku-buildpack-swiftを使用します。
heroku-buildpack-swiftでは、ビルドしたときのディレクトリ名は.build
ではなく.swift-bin
なので気をつけてください。()
3-1-2.Herokuの設定
次にHerokuCLIでアカウントにログインし、Herokuにアプリケーションを作成します。
アプリケーション作成時に、heroku-buildpack-swiftをbuildpackとして追加します。$ heroku login $ heroku create --buildpack kyle/swift 【プロジェクト名】3-1-3.デプロイ
Herokuに、コードを
push
すればheroku側でデプロイが開始されます。$ git push heroku master
起動するには、Herokuの画面から
slackbot
のDynos
をオンにします。3-2.Dockerのコンテナにデプロイしたい場合
次は、
Docker
でSlackBotを起動する方法をご紹介します。3-2-1.Dockerfileを書く
まず、プロジェクトのディレクトリに次の
Dockerfile
を追加してください。
(他のディレクトリにDockerfile
を置く場合はCOPYの第1引数を修正してください。)
libssl-dev
をインストールしていますが、これはSwiftNIOで必要となるためです。
libssl-dev
をインストールしないとbuildできません。()
FROM swift:5.1 RUN chmod -R o+r /usr/lib/swift/CoreFoundation && \ apt-get update && \ apt-get install -y libssl-dev COPY . /【プロジェクト名】 WORKDIR /【プロジェクト名】 RUN swift build -c release CMD [ "/【プロジェクト名】/.build/release/【プロジェクト名】"]3-2-2.コンテナのビルド
次のコマンドを実行し、Dockerfileを元にビルドします。
docker build . -t slackbot3-2-3.起動!
最後に、次のコマンドを実行してコンテナを立ち上げてください。
docker run --detach -t slackbotこれで、DockerでSlackBotの起動できます。
4.SwiftとSwiftPM、SlackKitのよかったところ、困ったところ
今回SwiftでSlackBot作りました、作っていて次のよかったところと困ったところがありました。
良かったところ
- macOSなら開発環境がすぐにできる
- sendMessegeメソッド
macOSならSwiftの開発環境が簡単にできる
さすが開発元がAppleなだけあり、さえXcodeをインストールすればSwiftの開発環境はおおよそ揃えられました。
SlackKitでは追加でライブラリを一つインストールする必要がありましたが
また、Xcode11からはSwiftPMのパッケージを管理できるようになりました。sendMessegeメソッド
余談でも書きましたが、SlackKitのWebAPIを使うとSlackBotの名前とアイコン画像を変更することができます。
細かい仕様を調ベていないので詳細は分かりませんが、昔Node.js
のBotKit
でSlackBotを作った時はなかった仕様でした。
これを逆手にとると、画像のようにメッセージごとに名前とアイコン画像を切り替えることができます。
これが不具合なのか、いつ修正されるか分かりませんが、アイコンを切り替えて感情豊かなBotを作れそうです。困ったところ
- Swiftのパッケージの大半がiOSのパッケージ
- Herokuやbitbucket-pipelineなどがSwiftに公式で対応していない
- 記事などの知見が少なく古い
Swiftのパッケージの大半がiOSのパッケージ
Googleカレンダーを使った機能を実装したいときに、使えそうなパッケージがないか調べるとiOSでしか使えないパッケージしかない等のことが多かったです。
使ったことがないので正確なことは言えませんが、Vaporのパッケージは色々揃っているので使うと楽になりそうです。Herokuやbitbucket-pipelineなどがSwiftに公式で対応していない
AttrasianやHerokuなどのWebサイトを見てみるとわかりますが、自動デプロイに公式で対応してません。
そのため、Docker
で対処するか、すでにあるライブラリやパッケージ等を頼りに対応する必要がありました。記事などの知見が少なく古い
良くありがちなことですが、今では使用できないものが書かれている記事が多かったです。
主にSlackKitを導入するにあたり、資料が古くSwiftやSwiftPMのバージョンアップにより上手く動作しないことがありました。
特にlibressl
関連とHeroku
、Docker
は、問題が解決するより、SlackBotの作成時間の方が短かったです。今回作ったSlackBot
今回作ったSlackBot私の家にいる なな(猫)を模したBotをつくりました。
ななちゃんBotは、私の分報チャンネルに生息しています。Botができること
ななちゃんbotには、次の機能を実装しました。
- メッセージにあるキーワード(ななちゃん)があるとリアクションする
- 勤怠の打刻をリマインドとリンクの表示
- GoogleMapでランチまたは、居酒屋を検索したURLの表示
- ユーザを祝ったり応援する機能
残念ながら二つ目の勤怠の打刻の通知以外はあまり使われていませんが、要望や個人的に欲しかった機能を実装しました。
SlackBotを分報に入れて気がついたこと
よかったこと
Botにキャラクターを持たせること
今回たまたま思いつきでしたが、家のなな(猫)をBotにしたことは正解でした。
ななちゃんBotの最初期は、個人のSlackチームで作っていたことや、SlackKitが手探りだったこと、環境が出来上がっていなかったこともありバグが多かったです。
それに、確認できる環境がSlack上のため、本番で動作確認をしている状態でした。この時はバグを発見したら極力すぐに直すようにしていました。
ですが、このバグに関してSlackでは次のような反応があり、バグがあっても良いのではという意見がありましたこの件について社内の同僚から、ご紹介いただいたこの記事に次のことが書かれていました。
バグは極力直さない
バカな子ほど可愛いという諺がある
怒られにくくするには愛着がわきやすい Bot を作るのが大切ということです読んでみて確かにと思いました。
記事の通り愛着が沸きやすいマスコットとして作った方が周囲から可愛がられ、今後社内で広く使うには良い方法だと感じました。
また、Botで遊んでいるのを見ていると開発のモチベーションも上がるため、偶然でしたが猫のBotとして作ったのは正解だったと思いました。失敗したこと
デプロイ方法を見つけるまでローカルで起動していた
今回HerokuとDockerでの起動方法をご紹介しましたが、先月ではまだできないでいて、
最初は私のローカル(MacBook Pro)で動作させていていました。
ただ、ローカルで動作していると、Sleepするとき
や会議の移動でネットワークが一時的に切断されたとき
に、オフラインになり使えなくなる状態ができ不便に思うことが増えました。
また、その度に再起動していたのですが、何度もやっていると流石に面倒でした。
そのためMac以外で常時起動できる方法を探しHerokuとDockerに行き着きました。最後に
今回SlackBotをSwiftで作ることで、Swiftのverison1から久しぶりにがっつりコードをかきました。
Version1のころと比較して、結構文法が変わっていましたが変わらないところは変わっていなかったのですんなり使えるようになりました。
また今回初めて技術記事を書きましたが、こんなに文章を書くのが難しいとは思いませんでした。
普段から情報を発信している方々の凄さに驚きました。
最後になりますが、もしこの記事の内容で誤り等ありましたら、お教えいただけますと幸いです。参考・参照文献
- 【SwiftPM】SwiftでもSlack Botを作りたい
- SlackKit
- Building Slack Bots In Swift
- heroku-buildpack-swift
- Update to Swift 5.0.1 breaks builds on Ubuntu 18/04
- Slack で役に立たない Bot を運用するときの知見とか
明日は@8845musignさんです!
よろしくお願いします!
- 投稿日:2019-12-15T00:15:14+09:00
忙しい人のためのCore Haptics
はじめに
iOS10からUIKitにはUIFeedbackGeneratorというAPIが追加されており、簡単にHapticFeedbackを実装することができました。
iOS13が公開され、UIFeedbackGeneratorに比べてカスタマイズできるHaptic Feedbackを作成できるようになったのですが、UIFeedbackGeneratorがあるUIKitにではなく、新しくCore HapticsというFrameworkが追加されました。
この記事はCore Hapticsとはどういう物なのか、実装方法などを簡単にまとめてみました。
HapticFeedbackとは
UISwitchでON/OFFを切り替えたり、UISliderでSliderを端までスワイプしたり、UIDatePickerでスクロールした時などの特定のアクションを行うと起きる振動のことで、たくさんの場所に組み込まれており、いくつかの種類が存在します
Core Hapticsとは
対応機種
iPhone8以降の全機種で使用することができます。
UIFeedBackGeneratorとCore Haptics
UIFeedBackGeneratorはNotification,Impact,Selectionなど、アクションを起こしたユーザーに対してFeedBackを行うことにより、UXを向上させることが目的とされています。
対して、Core Hapticsは自分でHapticFeedBackを内容、使うタイミングなどを自由に設定したい場合に使用します。
他のAPI(Core Animation, AVAudioEngine)との連動も可能なことや、Core HapticsはAudioAPIであり、今までAppleが実装していた音と連動して使用することなど(例:UIDatePickerのスクロール)も可能なので、UIFeedBackGeneratorに比べて豊かな表現を行うことができます。WWDCでは特に活用できるジャンルとして、ゲーム、ARなどが挙げられていました・
コード
それでは実際に実装しながら見ていきたいと思います。
モジュールのインポート
import CoreHapticsHapticEngine作成
var hapticEngine: CHHapticEngine? do { hapticEngine = try CHHapticEngine() try hapticEngine?.start() } catch { print("Error \(error)") }・CHHapticEngineには
capabilitiesForHardware
というメソッドが存在しており、HapticEngineを作成する前にデバイスが実行されているデバイス環境でHapticsFeedBackがサポートされているか確認することもできます。guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }・そのほかにも
stoppedHandler
,resetHandler
が用意されており、アプリケーション以外の動作でエンジンが終了される時などに使用できるコールバックも用意されています。var stoppedHandler: CHHapticEngine.StoppedHandler { get set } var resetHandler: CHHapticEngine.ResetHandler { get set }HapticEventParameterを元にHapticEventを作成
再生するコンテンツを作成します
//EventParameterを作成 let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1) let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 1) //EventParameter,EventType,Time,からEventを作成します。 let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 0)EventParameterを作成
・Eventで使われるParameterを作成します、ParameterTypeとしては音の強さ、明瞭さなど複数の種類が存在します。
EventParameter,EventType,Time,からEventを作成します。
・先ほど作成したEventParameter,EventType,Time,からEventを作成、eventTypeとしては単発で短い動き、継続的、音とHapticFeedBackを連動させるなどがあります。
再生
do { //先ほど作ったEventからPatternを作成 let hapticPattern = try CHHapticPattern(events: [event], parameters: []) //PatternのPlayerを作成 let hapticPlayer = try hapticEngine?.makePlayer(with: hapticPattern) try hapticPlayer?.start(atTime: CHHapticTimeImmediate) } catch let error{ print("Error: \(error)") }・イベントを1つのPatternとしてラップして、そのPatternに対するPlayerを作成します。
これがCoreHapticsの基本的なコードになります
Dynamic Parameter
UIFeedBackGeneratorとの違いの1つとして、Event Parameterの値を増減することや複数の動きを組み合わせる事が可能です。
・上記のCHHapticEventは単純な振動のみですが、下記のようにするとrelativeTimeを元にタイムラインを設定することにより、複数イベントを作成して、パターンとして扱うこともできます。
//単発のEvent let short1 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0) let short2 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0.2) let short3 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0.4) let short4 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0.6) let short5 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0.8) let short6 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 1.0) //継続的なEvent let long1 = CHHapticEvent(eventType: .hapticContinuous, parameters: [], relativeTime: 1.2, duration: 0.5) let long2 = CHHapticEvent(eventType: .hapticContinuous, parameters: [], relativeTime: 1.8, duration: 0.5) let long3 = CHHapticEvent(eventType: .hapticContinuous, parameters: [], relativeTime: 2.4, duration: 0.5)最後に
今回紹介したのはCore Hapticsの簡単な部分にすぎません、他のPatternの作成方法として、APHP(Apple Haptic Audio Pattern)と呼ばれるファイルを用いてコードからHapticPatternを分離させる方法や、CHHapticParameterCurveといった再生中に値を増減させるなどより自由度の高い作成方法もありますので、気になった方はWWDCの動画やDocumentなどをご覧になってみてください。
ありがとうございました。
参考リンク
- 投稿日:2019-12-15T00:01:03+09:00
Swiftは / 2 より * 0.5 のほうが速いのか
この記事は、CAM Advent Calendar 15日目の記事です
前回は @matsuhei さんによる、 レガシーコードに対する解析ツール(重複コード編)でした。概要
以前、Swiftのコードを誰かに見せてたときに以下のように指摘されました。
ここ、
/ 2
になっとるけど* 0.5
のほうが速いで確かに、基本的には割り算よりも掛け算のほうがパフォーマンスがいいです。直に頭で計算するとき、僕も掛け算のほうが計算しやすいです。計算理由にもよりますが、Swiftもその例から漏れないはずです。
ただ、iOSでは本番用のipaを作成するときにはリリースビルド(本番)が行われます。
最適化の側面から見て、コードが一緒になってないかな?と思ったので、検証してみました。SIL
注目すべきはSILです。
SILはSwift Intermediate Language
の略で、実行できるバイナリーに落とし込むために必要な中間言語です。
Swift CompilerはLLVM IRに落とし込む前にSILを生成します。
SILは主に2種類、raw SIL
とcanonical SIL
があります。
raw SIL
は、AST(abstract syntax tree)から解析しやすいように変換された直後のものです。
canonical SIL
は、raw SIL
を最適化された状態の SILです。検証1
sample1.swiftlet aaa: Double = 200 let result: Double = aaa * 0.5sample2.swiftlet aaa: Double = 200 let result: Double = aaa / 2実行コード
$ swiftc -emit-sil -O sample1.swift > sample1.sil $ swiftc -emit-sil -O sample2.swift > sample2.sil
-emit-sil
でcanonical SIL
が生成されます。raw SIL
を生成するには、-emit-silgen
と書けばよいです。
-O
が付いていますが、これは canonical SIL からまた最適化が施された状態のものを生成するために必要なオプションで、ついてない場合は、-Onone
と同等です。結果
sample1.silsil_stage canonical import Builtin import Swift import SwiftShims @_hasStorage @_hasInitialValue let aaa: Double { get } @_hasStorage @_hasInitialValue let result: Double { get } // aaa sil_global hidden [let] @$s7sample53aaaSdvp : $Double // result sil_global hidden [let] @$s7sample56resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample53aaaSdvp // id: %2 %3 = global_addr @$s7sample53aaaSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 alloc_global @$s7sample56resultSdvp // id: %7 %8 = global_addr @$s7sample56resultSdvp : $*Double // user: %11 %9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10 %10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11 store %10 to %8 : $*Double // id: %11 %12 = integer_literal $Builtin.Int32, 0 // user: %13 %13 = struct $Int32 (%12 : $Builtin.Int32) // user: %14 return %13 : $Int32 // id: %14 } // end sil function 'main'sample2.silsil_stage canonical import Builtin import Swift import SwiftShims @_hasStorage @_hasInitialValue let aaa: Double { get } @_hasStorage @_hasInitialValue let result: Double { get } // aaa sil_global hidden [let] @$s7sample63aaaSdvp : $Double // result sil_global hidden [let] @$s7sample66resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample63aaaSdvp // id: %2 %3 = global_addr @$s7sample63aaaSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 alloc_global @$s7sample66resultSdvp // id: %7 %8 = global_addr @$s7sample66resultSdvp : $*Double // user: %11 %9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10 %10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11 store %10 to %8 : $*Double // id: %11 %12 = integer_literal $Builtin.Int32, 0 // user: %13 %13 = struct $Int32 (%12 : $Builtin.Int32) // user: %14 return %13 : $Int32 // id: %14 } // end sil function 'main'Diff
@@ -9,21 +9,21 @@ @_hasStorage @_hasInitialValue let result: Double { get } // aaa -sil_global hidden [let] @$s7sample53aaaSdvp : $Double +sil_global hidden [let] @$s7sample63aaaSdvp : $Double // result -sil_global hidden [let] @$s7sample56resultSdvp : $Double +sil_global hidden [let] @$s7sample66resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): - alloc_global @$s7sample53aaaSdvp // id: %2 - %3 = global_addr @$s7sample53aaaSdvp : $*Double // user: %6 + alloc_global @$s7sample63aaaSdvp // id: %2 + %3 = global_addr @$s7sample63aaaSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 - alloc_global @$s7sample56resultSdvp // id: %7 - %8 = global_addr @$s7sample56resultSdvp : $*Double // user: %11 + alloc_global @$s7sample66resultSdvp // id: %7 + %8 = global_addr @$s7sample66resultSdvp : $*Double // user: %11 %9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10 %10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11 store %10 to %8 : $*Double // id: %11
float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100
これ、200 / 2
した100
を直接 result に突っ込んでますね……。
最適化された結果、SIL時点で実行時に計算されるのではなく結果のみを返されるようになりました。
リテラル値で計算されるから良いように最適化されたのかなと思ったので、検証コードを変更してみます。検証2
sample3.swiftfunc test(_ value: Double) -> Double { return value * 0.5 } let result: Double = test(200.0)sample4.swiftfunc test(_ value: Double) -> Double { return value / 2 } let result: Double = test(200.0)結果
sample3.silsil_stage canonical import Builtin import Swift import SwiftShims func test(_ value: Double) -> Double @_hasStorage @_hasInitialValue let result: Double { get } // result sil_global hidden [let] @$s7sample36resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample36resultSdvp // id: %2 %3 = global_addr @$s7sample36resultSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 %7 = integer_literal $Builtin.Int32, 0 // user: %8 %8 = struct $Int32 (%7 : $Builtin.Int32) // user: %9 return %8 : $Int32 // id: %9 } // end sil function 'main' // test(_:) sil hidden @$s7sample34testyS2dF : $@convention(thin) (Double) -> Double { // %0 // users: %3, %1 bb0(%0 : $Double): debug_value %0 : $Double, let, name "value", argno 1 // id: %1 %2 = float_literal $Builtin.FPIEEE64, 0x3FE0000000000000 // 0.5 // user: %4 %3 = struct_extract %0 : $Double, #Double._value // user: %4 %4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 return %5 : $Double // id: %6 } // end sil function '$s7sample34testyS2dF'sample4.silsil_stage canonical import Builtin import Swift import SwiftShims func test(_ value: Double) -> Double @_hasStorage @_hasInitialValue let result: Double { get } // result sil_global hidden [let] @$s7sample46resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample46resultSdvp // id: %2 %3 = global_addr @$s7sample46resultSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 %7 = integer_literal $Builtin.Int32, 0 // user: %8 %8 = struct $Int32 (%7 : $Builtin.Int32) // user: %9 return %8 : $Int32 // id: %9 } // end sil function 'main' // test(_:) sil hidden @$s7sample44testyS2dF : $@convention(thin) (Double) -> Double { // %0 // users: %3, %1 bb0(%0 : $Double): debug_value %0 : $Double, let, name "value", argno 1 // id: %1 %2 = float_literal $Builtin.FPIEEE64, 0x4000000000000000 // 2 // user: %4 %3 = struct_extract %0 : $Double, #Double._value // user: %4 %4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 return %5 : $Double // id: %6 } // end sil function '$s7sample44testyS2dF'Diff
@@ -9,13 +9,13 @@ @_hasStorage @_hasInitialValue let result: Double { get } // result -sil_global hidden [let] @$s7sample36resultSdvp : $Double +sil_global hidden [let] @$s7sample46resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): - alloc_global @$s7sample36resultSdvp // id: %2 - %3 = global_addr @$s7sample36resultSdvp : $*Double // user: %6 + alloc_global @$s7sample46resultSdvp // id: %2 + %3 = global_addr @$s7sample46resultSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 @@ -25,16 +25,16 @@ } // end sil function 'main' // test(_:) -sil hidden @$s7sample34testyS2dF : $@convention(thin) (Double) -> Double { +sil hidden @$s7sample44testyS2dF : $@convention(thin) (Double) -> Double { // %0 // users: %3, %1 bb0(%0 : $Double): debug_value %0 : $Double, let, name "value", argno 1 // id: %1 - %2 = float_literal $Builtin.FPIEEE64, 0x3FE0000000000000 // 0.5 // user: %4 + %2 = float_literal $Builtin.FPIEEE64, 0x4000000000000000 // 2 // user: %4 %3 = struct_extract %0 : $Double, #Double._value // user: %4 - %4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 + %4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 return %5 : $Double // id: %6 -} // end sil function '$s7sample34testyS2dF' +} // end sil function '$s7sample44testyS2dF'%4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5割り算と掛け算がちゃんと別れています。
/ 2
を自動的に* 0.5
に変換される、なんてことはないようです。
ちなみに、test(:)
に@inlinable
を追加したら、検証1と同じ計算した結果の100の値が保持されました。展開されて、最適化が施されたようです。まとめ
- リテラル値同士の計算は 最適化されて計算した結果のみ保持する
これを色々調べて思ったのは、ただ早いから
* 0.5
を選ぶのは軽率かなと思いました。可読性の問題もあったり、上の最適化されて結果同じ場合もあったりするので、そこらへんを正しく見極めて書いていくことが大事だと感じました。次は16日目、@keitatata による redis レプリケーションとシャーディング です。お楽しみに。
参考資料
以下の資料は今回の記事を書く上で非常に参考になりました。ありがとうございます。
http://llvm.org/devmtg/2015-10/slides/GroffLattner-SILHighLevelIR.pdf
https://blog.waft.me/2018/01/09/swift-sil-1/
https://github.com/apple/swift/blob/master/docs/SIL.rst
https://qiita.com/Kuniwak/items/cbf6b88db249838895b5
- 投稿日:2019-12-15T00:01:03+09:00
最適化から見る、Swiftの / 2 と * 0.5
この記事は、CAM Advent Calendar 15日目の記事です
前回は @matsuhei さんによる、 レガシーコードに対する解析ツール(重複コード編)でした。概要
以前、Swiftのコードを誰かに見せてたときに以下のように指摘されました。
ここ、
/ 2
になっとるけど* 0.5
のほうが速いで確かに、基本的には割り算よりも掛け算のほうがパフォーマンスがいいです。直に頭で計算するとき、僕も掛け算のほうが計算しやすいです。計算理由にもよりますが、Swiftもその例から漏れないはずです。
ただ、iOSでは本番用のipaを作成するときにはリリースビルド(本番)が行われます。
最適化の側面から見て、コードが一緒になってないかな?と思ったので、検証してみました。SIL
注目すべきはSILです。
SILはSwift Intermediate Language
の略で、実行できるバイナリーに落とし込むために必要な中間言語です。
Swift CompilerはLLVM IRに落とし込む前にSILを生成します。
SILは主に2種類、raw SIL
とcanonical SIL
があります。
raw SIL
は、AST(abstract syntax tree)から解析しやすいように変換された直後のものです。
canonical SIL
は、raw SIL
を最適化された状態の SILです。検証1
sample1.swiftlet aaa: Double = 200 let result: Double = aaa * 0.5sample2.swiftlet aaa: Double = 200 let result: Double = aaa / 2実行コード
$ swiftc -emit-sil -O sample1.swift > sample1.sil $ swiftc -emit-sil -O sample2.swift > sample2.sil
-emit-sil
でcanonical SIL
が生成されます。raw SIL
を生成するには、-emit-silgen
と書けばよいです。
-O
が付いていますが、これは canonical SIL からまた最適化が施された状態のものを生成するために必要なオプションで、ついてない場合は、-Onone
と同等です。結果
sample1.silsil_stage canonical import Builtin import Swift import SwiftShims @_hasStorage @_hasInitialValue let aaa: Double { get } @_hasStorage @_hasInitialValue let result: Double { get } // aaa sil_global hidden [let] @$s7sample53aaaSdvp : $Double // result sil_global hidden [let] @$s7sample56resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample53aaaSdvp // id: %2 %3 = global_addr @$s7sample53aaaSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 alloc_global @$s7sample56resultSdvp // id: %7 %8 = global_addr @$s7sample56resultSdvp : $*Double // user: %11 %9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10 %10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11 store %10 to %8 : $*Double // id: %11 %12 = integer_literal $Builtin.Int32, 0 // user: %13 %13 = struct $Int32 (%12 : $Builtin.Int32) // user: %14 return %13 : $Int32 // id: %14 } // end sil function 'main'sample2.silsil_stage canonical import Builtin import Swift import SwiftShims @_hasStorage @_hasInitialValue let aaa: Double { get } @_hasStorage @_hasInitialValue let result: Double { get } // aaa sil_global hidden [let] @$s7sample63aaaSdvp : $Double // result sil_global hidden [let] @$s7sample66resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample63aaaSdvp // id: %2 %3 = global_addr @$s7sample63aaaSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 alloc_global @$s7sample66resultSdvp // id: %7 %8 = global_addr @$s7sample66resultSdvp : $*Double // user: %11 %9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10 %10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11 store %10 to %8 : $*Double // id: %11 %12 = integer_literal $Builtin.Int32, 0 // user: %13 %13 = struct $Int32 (%12 : $Builtin.Int32) // user: %14 return %13 : $Int32 // id: %14 } // end sil function 'main'Diff
@@ -9,21 +9,21 @@ @_hasStorage @_hasInitialValue let result: Double { get } // aaa -sil_global hidden [let] @$s7sample53aaaSdvp : $Double +sil_global hidden [let] @$s7sample63aaaSdvp : $Double // result -sil_global hidden [let] @$s7sample56resultSdvp : $Double +sil_global hidden [let] @$s7sample66resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): - alloc_global @$s7sample53aaaSdvp // id: %2 - %3 = global_addr @$s7sample53aaaSdvp : $*Double // user: %6 + alloc_global @$s7sample63aaaSdvp // id: %2 + %3 = global_addr @$s7sample63aaaSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 - alloc_global @$s7sample56resultSdvp // id: %7 - %8 = global_addr @$s7sample56resultSdvp : $*Double // user: %11 + alloc_global @$s7sample66resultSdvp // id: %7 + %8 = global_addr @$s7sample66resultSdvp : $*Double // user: %11 %9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10 %10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11 store %10 to %8 : $*Double // id: %11
float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100
これ、200 / 2
した100
を直接 result に突っ込んでますね……。
最適化された結果、SIL時点で実行時に計算されるのではなく結果のみを返されるようになりました。
リテラル値で計算されるから良いように最適化されたのかなと思ったので、検証コードを変更してみます。検証2
sample3.swiftfunc test(_ value: Double) -> Double { return value * 0.5 } let result: Double = test(200.0)sample4.swiftfunc test(_ value: Double) -> Double { return value / 2 } let result: Double = test(200.0)結果
sample3.silsil_stage canonical import Builtin import Swift import SwiftShims func test(_ value: Double) -> Double @_hasStorage @_hasInitialValue let result: Double { get } // result sil_global hidden [let] @$s7sample36resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample36resultSdvp // id: %2 %3 = global_addr @$s7sample36resultSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 %7 = integer_literal $Builtin.Int32, 0 // user: %8 %8 = struct $Int32 (%7 : $Builtin.Int32) // user: %9 return %8 : $Int32 // id: %9 } // end sil function 'main' // test(_:) sil hidden @$s7sample34testyS2dF : $@convention(thin) (Double) -> Double { // %0 // users: %3, %1 bb0(%0 : $Double): debug_value %0 : $Double, let, name "value", argno 1 // id: %1 %2 = float_literal $Builtin.FPIEEE64, 0x3FE0000000000000 // 0.5 // user: %4 %3 = struct_extract %0 : $Double, #Double._value // user: %4 %4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 return %5 : $Double // id: %6 } // end sil function '$s7sample34testyS2dF'sample4.silsil_stage canonical import Builtin import Swift import SwiftShims func test(_ value: Double) -> Double @_hasStorage @_hasInitialValue let result: Double { get } // result sil_global hidden [let] @$s7sample46resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample46resultSdvp // id: %2 %3 = global_addr @$s7sample46resultSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 %7 = integer_literal $Builtin.Int32, 0 // user: %8 %8 = struct $Int32 (%7 : $Builtin.Int32) // user: %9 return %8 : $Int32 // id: %9 } // end sil function 'main' // test(_:) sil hidden @$s7sample44testyS2dF : $@convention(thin) (Double) -> Double { // %0 // users: %3, %1 bb0(%0 : $Double): debug_value %0 : $Double, let, name "value", argno 1 // id: %1 %2 = float_literal $Builtin.FPIEEE64, 0x4000000000000000 // 2 // user: %4 %3 = struct_extract %0 : $Double, #Double._value // user: %4 %4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 return %5 : $Double // id: %6 } // end sil function '$s7sample44testyS2dF'Diff
@@ -9,13 +9,13 @@ @_hasStorage @_hasInitialValue let result: Double { get } // result -sil_global hidden [let] @$s7sample36resultSdvp : $Double +sil_global hidden [let] @$s7sample46resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): - alloc_global @$s7sample36resultSdvp // id: %2 - %3 = global_addr @$s7sample36resultSdvp : $*Double // user: %6 + alloc_global @$s7sample46resultSdvp // id: %2 + %3 = global_addr @$s7sample46resultSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 @@ -25,16 +25,16 @@ } // end sil function 'main' // test(_:) -sil hidden @$s7sample34testyS2dF : $@convention(thin) (Double) -> Double { +sil hidden @$s7sample44testyS2dF : $@convention(thin) (Double) -> Double { // %0 // users: %3, %1 bb0(%0 : $Double): debug_value %0 : $Double, let, name "value", argno 1 // id: %1 - %2 = float_literal $Builtin.FPIEEE64, 0x3FE0000000000000 // 0.5 // user: %4 + %2 = float_literal $Builtin.FPIEEE64, 0x4000000000000000 // 2 // user: %4 %3 = struct_extract %0 : $Double, #Double._value // user: %4 - %4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 + %4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 return %5 : $Double // id: %6 -} // end sil function '$s7sample34testyS2dF' +} // end sil function '$s7sample44testyS2dF'%4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5割り算と掛け算がちゃんと別れています。
/ 2
を自動的に* 0.5
に変換される、なんてことはないようです。
ちなみに、test(:)
に@inlinable
を追加したら、検証1と同じ計算した結果の100の値が保持されました。展開されて、最適化が施されたようです。まとめ
- リテラル値同士の計算は 最適化されて計算した結果のみ保持する
これを色々調べて思ったのは、ただ早いから
* 0.5
を選ぶのは軽率かなと思いました。可読性の問題もあったり、上の最適化されて結果同じ場合もあったりするので、そこらへんを正しく見極めて書いていくことが大事だと感じました。次は16日目、@keitatata による redis レプリケーションとシャーディング です。お楽しみに。
参考資料
以下の資料は今回の記事を書く上で非常に参考になりました。ありがとうございます。
http://llvm.org/devmtg/2015-10/slides/GroffLattner-SILHighLevelIR.pdf
https://blog.waft.me/2018/01/09/swift-sil-1/
https://github.com/apple/swift/blob/master/docs/SIL.rst
https://qiita.com/Kuniwak/items/cbf6b88db249838895b5