- 投稿日:2020-07-28T21:47:04+09:00
【Swift】テーブルビューのスクロールを滑らかにする方法【KingFisher】
Qiita初投稿です!
iOSアプリ開発をしていると、UITableViewをかなり高頻度で使います。
今日はこのUITableViewを、より滑らかにスクロールできるように改善する方法を紹介します。画像のキャッシュで滑らかに
スクロールがガクつく原因は色々考えられますが、セルに表示する画像のダウンロードに時間がかかって処理が重くなっているケースが多いです。
今回、同じ様に画像表示でアプリが重くなってしまった際にKingFisherというライブラリを使うことで、超簡単に画像をキャッシュしてサクサク動くようになりました。
UITableViewなどのアプリ内で、複数の画像を使う方にはオススメのライブラリです。インストール
Cocoapodsを使いました。
pod 'Kingfisher'
とPodfileに入れてinstallします。使い方
ViewController.swiftlet url = URL(string: "https://example.com/image.png") imageView.kf.setImage(with: url)上記のコードで画像のキャッシュを行うことができ、アプリがサクサク動くようになりました。
画像を初めて表示する際はURLからダウンロード、それ以降はキャッシュから表示するのでダウンロードを待たずに表示することが可能になります。
まとめ
画像のキャッシュと聞くとかなりハードな実装のイメージがありましたが、実際に行ってみると便利なライブラリもあり、短時間で行うことができました。
テーブルビューが滑らかに動かないと悩んでいる方は是非参考にしてみて下さい。
参考
- 投稿日:2020-07-28T20:34:18+09:00
[swift5]tableViewの基礎文法
投稿の経緯
現在独学でiOS開発を学習中。
学習教材はUdemyで人気の高かった【iOS13対応】未経験者がiPhoneアプリ開発者になるための全て iOS Boot Campを使用。(サイトは下記URL)
https://www.udemy.com/course/ios13_swift5_iphone_ios_boot_camp/学習内容をアウトプットします!
tableViewとは
要約するとリスト型のパーツ。tableViewControllerはリスト型のコントローラーということ。
tableViewのデリゲート宣言
UITableViewDelegate
,UITableViewDataSource
の2つが必要。記述するとエラーが発生するが、それはtableViewの実装に必要なメソッドが
不足しているというエラーで、Xcodeの補完に沿ってメソッドを作成すれば解消される。tableViewの構築に必要なメソッド
①セルのセクションを決めるメソッド
ViewController.swiftfunc numberOfSections(in tableView: UITableView) -> Int { }②セルの数を決めるメソッド
ViewController.swiftfunc tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { }③セルを構築する際に呼ばれるメソッド
ViewController.swiftfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { }①〜③は上から順に読み込まれる。
④セルの高さを決めるメソッド
ViewController.swiftfunc tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { }⑤セルがタップされた時に呼ばれるメソッド
ViewController.swiftfunc tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { }備考
tableViewの更新方法▼
ViewController.swifttableView.reloadData()セルのハイライトを消す方法▼
ViewController.swiftcell.selectionStyle = .none
- 投稿日:2020-07-28T19:27:38+09:00
ゲームアプリ制作におけるベジェ曲線の可能性
はじめに
アプリの絵がぼやける理由
アプリを遊んでいて「絵がぼやけてる」と感じたことはありませんか?
解像度の大きな端末に乗り変えたら、お気に入りのアプリの表示がボケボケで、切ない気分になった人は少なからずいるのではないでしょうか?これは、アプリの画像素材のほとんどがペイント形式で作成されているからです。
ペイント形式とは画素を並びで保持する形式なのですが、拡大や回転による品質の劣化が起こります。例えば、1280x720 のキャラクタの立ち絵がペイント形式で用意されていて、アプリが画面全体に表示させたとします。
解像度 1280x720 の端末であればクッキリと表示されますが、2560x1440 の端末だと画素が引き延ばされて画像がぼやけてしまうことになります。アプリの文字がぼやけない理由
さて、こうも思ったことはありませんか?
「文字だけやけにきれいに見える」
これは、文字がドロー形式で表示されているからです。
ドロー形式とは、データとして座標や色情報などを保持しておき、計算で絵を生成する形式のことです。先の例の場合、1280x720 の端末でも 2560x1440 の端末でも、解像度に合わせた素材が実行時に生成されるため、文字がぼやけずきれいに表示されるというわけです。
絵も文字と同じ形式にしたらどうか
ペイント形式のデータが端末によってぼやけてしまうのは、ゲーム制作者にとって悩ましい問題です。
一般的な対策として、低解像度の端末向けに小さな画像、高解像度の端末向けに大な画像を用意しておき、端末の性能に応じて切り替えて利用するというものがあります。ただし、この対応はデータの管理や作成が煩雑になりますし、スマホの性能の向上とイタチごっこになる側面があります。
そこで素朴な疑問です。
アプリの絵素材も文字と同じく、ドロー形式で作成してみたらどうでしょうか?
ものは試しです。ドロー形式でゲーム素材を作成する場合、どんなことができるか可能性を探ってみましょう。ベジェ曲線
ドロー形式の基本要素が「ベジェ曲線」です。
ベジェ曲線とは「座標+アルファ」で曲線を表現する手法です。アンカーポイント
ベジェ曲線では座標のことを「アンカーポイント」と呼びます。
このアンカーポイントを複数置き、順番につなげることでパス(線)を表現します。アンカーポイント0→1、1→2、2→3、3→0の順番でパスが引かれ、菱形が描画されました。
方向線
さて、アンカーポイントだけだとまっすぐなパスしか表現できないので、パスを曲げるための「+アルファ」が必要です。
パスを曲げるには、アンカーポイントに「方向線」と呼ばれる2種類のベクトルを付与します。*誘導ベクトル*
1つ目の方向線は、そのアンカーポイントから「出発するパスを誘導する」効果を持ち、パスの引き始めほど強く影響します。
ここでは「誘導ベクトル」と呼びます。↓ 図3:アンカーポイントに誘導ベクトルを追加(4本の青い矢印)
それぞれのアンカーポイントから伸びる誘導ベクトル(青い線)に沿って、パスの描き始めが曲げられました。
*誘引ベクトル*
2つ目の方向線は、そのアンカーポイントに「到着するパスを誘引する」効果を持ち、パスの終わり際ほど強く影響します。
ここでは「誘引ベクトル」と呼びます。↓ 図4:アンカーポイントに誘引ベクトルを追加(4本の赤い矢印)
誘導ベクトルとは逆に、アンカーポイントの誘引ベクトル(赤い矢印)に引っ張られて、パスの終わり際が曲げられました。
誘導ベクトルと誘引ベクトルは、1つのアンカーポイントに両方指定することもできます。
アンカーポイントと方向線を工夫することで、自由自在にパスを描けるのがベジェ曲線の強みです。
では、ベジェ曲線を土台とした描画環境を実装し、アプリ上で表示できる絵素材を作成してみましょう。
素材作りの流れ
素材(画素)の生成はベジェ曲線で行うわけですが、それ以外のところは一般的な 2D 素材作成の流れを想定します。
いろいろなゲームで採用されている Live2D っぽく、パーツ別に素材を作成して組み合わせ、1枚の絵としてのバリエーションを出すことを目標としましょう。下絵の作成
パーツわけした下絵を作成してエディタに取り込みます。
(※Live2D のサンプルを真似してミクさんを作成してみます)↓ 図7:パスを作成する際の下絵(パーツ単位で素材を作るのでバラバラ)
パーツ素材の作成
エディタ上で下絵をトレースしていきます。
パーツ素材の組み合わせ
バラバラに作成したパーツを組み合わせることで、絵を構築していきます。
サンプルアプリ
作成した素材の表示を確認するためのサンプルアプリが GitHub へアップロードしてあります。
iOS / Android それぞれのプロジェクトがあるので、興味のある方はビルドして実行してみてください。
サンプルアプリで確認できる表現
ベジェ曲線的に実装した表現手法として、サンプルアプリで確認できる内容は下記となります。
移動値補正パラメータ
アンカーポイントは移動値(風やキャラクタの動き)からの影響されやすさを保持します。
髪や布などのアンカーポイントにこの補正値を高めに適用することで、キャラクタの動き(移動)に沿った変化をさせることが可能です。
(※サンプルプアプリの「SWY」と「MOV」ボタンの機能です)↓ 図13:移動値による髪などのなびき(移動方向を目で追います)
体型補正パラメータ
アンカーポイントは体型パラメータからの影響されやすさを保持します。
各アンカーポイントにこの補正値を適用することで、体を細くしたり、背をのばしたりすることが可能です。
(※サンプルプアプリの「H」と「V」ボタンの機能です)パーツ毎のサイズパラメータ
パーツはサイズパラメータからの影響されやさを保持します。
各部位のサイズをまばらに変え、パーツ毎に個性の表現が可能です。
(※サンプルプアプリの「S」ボタンの機能です)↓ 図16:サイズパラメータによる変化(ツインテールを大きめに補正)
パーツ毎の回転パラメータ
パーツは回転パラメータからの影響されやすさを保持します。
腕や肘などの稼働領域の影響されやすさとなり、絵によって受け入れられる角度を制限することで、関節があらぬ方向へ曲がらないように制限することが可能です。
(※サンプルプアプリの「T」ボタンの機能)擬似的な傾け
絵全体を擬似的に傾けることで、画像に左右/上下に傾いた印象を与えることが可能です。
(※サンプルプアプリの「LR」と「UD」ボタンの機能)ストロークの切り替え
パスの描画ブラシを変えることで、ストロークの差し替えが可能です。
(※サンプルプアプリの「STR」ボタンの機能)パレットの切り替え
厳密にはベジェ曲線の機能というわけではありませんが、処理の単純化と容量を浮かせるために画像をパレット形式で管理しているため、パレット差し替えが可能です。
(※サンプルアプリの「COL」ボタンの機能)素材の差し替え
こちらも厳密にはベジェ曲線の機能というわけではありませんが、ベジェ曲線(ドロー形式データ)の場合、ストロークや塗りの表現を統一できるので、素材の差し替えの自由度を高められます。
(※サンプルアプリの「COSTUME」ボタンの機能)課題
今回作成したベジェ曲線環境ですが、ゲーム制作へ導入するには大きな課題が2つあります。
描画速度
とにかく画像の生成が遅いです(※端末の性能と解像度のバランスにもよりますが、1回の画像生成に数十ミリ秒かかっています)。
特に、大きなサイズでリアルタイムにアニメさせようとすると、処理落ちがひどいことになります。描画速度の改善が課題となります。
一方で、1枚絵ジェネレータとして、アプリ起動時にキーフレーム別に画像を生成しておき、その後、静的に参照する使い方であれば問題なく導入できそうです。
データ作成用のエディタ
今回、サンプルデータの作成はデバッグ画面レベルの簡易エディタで行いました。
が、現段階では、とても人様に提供できるレベルではありません(使いづらい&動作が安定しない等)。ゲーム制作においては、大量のデータをいかに効率よく作成できるかが重要です。
エディタの良し悪しで工数は大きく変わってくるので、使いやすいエディタの準備が課題となります。最後に
さて、ベジェ曲線によるゲーム素材作りを試行錯誤してみましたが、一番手応えを感じたのがストローク描画の部分です。
例えば、ツインテールだけを20%ぐらいのサイズまで縮小した場合でも、アンカーポイントの座標がかわるだけで描かれる線の太さはかわらず、ほかのパーツと共存させても違和感がありませんでした(※ペイント形式データの場合、一部のパーツを20%に縮小しようものなら、主線等が潰れて絵的なニュアンスが変化してしまい、他のパーツとの間で見た目に違和感がでます)。
パーツ毎に派手な拡大&回転をしてしまっても他のパーツと馴染んでくれるのは、動的に線を描くベジェ曲線ならではの強みだと思います。さらに、ストローク描画の際、1画素単位をプログラム側で管理できるので、そこで色々な表現を加えることも可能そうです。
例えば、細密画のように主線に沿ってタッチをつけたり、テクスチャブラシのような表現が考えられます。アンカーポイントの補正を含め、ベジェ曲線によるゲーム作成には十分な可能性がありそうです。
少ない素材&手間によりお手軽に表現のバリエーションがだせるよう、模索していこうと思います。おまけ
GitHub の ReadMe に掲載している、大きめな動作例の画像です。
よろしければご覧くださいませ(※一枚一枚のサイズが大きいので別窓で開きます)。
- 投稿日:2020-07-28T18:28:03+09:00
jpeg画像を元にiOSの画像を@x2/@x3を用意する方法
<背景を透明にし、不要な背景部分をカットし、画像サイズを変える>
背景を透過にする->pngに変換される
https://qiita.com/na1412/items/90a9641d9644e1bfbb49トリミング(不要な背景を削除)
http://www.mac-beginner.com/77.html画像サイズを変える(解像度も変えられる)
https://www.fusenhonpo.com/submit/popup_mac.html
->@x2と@x3が必要なので、@x3の「幅」を3/2の長さで指定してやれば良い反転はプレビュー.appで行う。
- 投稿日:2020-07-28T18:28:03+09:00
画像サイズについて
<Navigation Drawerに表示するヘッダの画像サイズについて>
Navigation Drawerの幅は240〜320dpにすべき
https://qiita.com/nein37/items/6c57fe5ec94a7914872c<画像サイズについて>
Androidにおいて、一般的なスマートフォンの横幅は320-384dp
xxxhdpi 640dpi以下で作成
xxhdpi 480dpi以下で作成
xhdpi 320dpi以下で作成
hdpi 240dpi以下で作成
mdpi 160dpi以下で作成iOSにおいて、320pxはiPhone 3GS非Retinaの画面幅いっぱいのサイズ
->Retinaはそこから倍数だったが、途中から中途半端になってきた
(iPhone 4 ~ 5sまでは Retinaで = 640)
https://qiita.com/Yuta/items/98b9ea2739718b9184de
https://backapp.co.jp/blog/11573/#Android_dp_iOS_Retina
https://www.webtech.co.jp/blog/optpix_labs/6915/<例:320dpの画像を作成依頼するとき>
幅320dpで指定。高さはお好み1dp=
mdpi:1px
hdpi:1.5px
xhdpi:2px
xxhdpi:3px
xxxhdpi:4pxなので、横幅320dpの画像が欲しい場合は以下で作成する
320px x 高さ任意
->【Android】mdpiで使用【iOS】非Retina端末で使用
480px x 高さ任意
->【Android】hdpiで使用
640px x 高さ任意
->【Android】xhdpiで使用【iOS】@2xで使用
960px x 高さ任意
->【Android】xxhdpiで使用【iOS】@3xで使用
1280px x 高さ任意
->【Android】xxxhdpiで使用<背景を透明にし、不要な背景部分をカットし、画像サイズを変える>
背景を透過にする->pngに変換される
https://qiita.com/na1412/items/90a9641d9644e1bfbb49トリミング(不要な背景を削除)
http://www.mac-beginner.com/77.html画像サイズを変える(解像度も変えられる)
https://www.fusenhonpo.com/submit/popup_mac.html
->@x2と@x3が必要なので、@x3の「幅」を3/2の長さで指定してやれば良い反転はプレビュー.appで行う。
- 投稿日:2020-07-28T17:39:06+09:00
iOS: gRPC(Protocol Buffers) + ReactiveSwiftのサンプル
サンプルの説明
- .protoファイルをcocoapodsを利用してビルドする方法です。(grpc-swiftではありません。)
- 生成されたObjective-Cのコードを、Swiftから利用します。
- gRPCでAPIにアクセスする処理をReactiveSwiftのSignalProducer, Signalを返すようにラップしています。
サンプルコードのリポジトリ
https://github.com/yusuke-imagawa/iOS_gRPC_ReactiveSwift_sample
.protoファイル, gRPC, ReactiveSwiftの連携部分だけを実装しています。
UIは実装していません。使い方
- pod installを実行。
cocoapodsで.protoファイルから、コードを生成するための設定
.protoファイル
user.proto
report.proto
push_notify.proto
commons.proto
chat.proto
calling.proto
block.proto
account.protoPodfile
# Uncomment the next line to define a global platform for your project platform :ios, '13.2' target 'TalkingSns' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for Talking pod 'RealmSwift', '4.4.0' # GRPC_Client pod 'RemoteClient', path: './RemoteClient' pod 'ReactiveSwift', '~> 6.1' pod 'ReactiveCocoa', '~> 10.1' pod 'CocoaLumberjack/Swift' target 'TalkingSnsTests' do inherit! :search_paths # Pods for testing end target 'TalkingSnsUITests' do # Pods for testing end endRemoteClient/RemoteClient.podspec
Pod::Spec.new do |s| s.name = "RemoteClient" # .protoファイルの更新時に、versionを変更して pod install する。 s.version = "0.0.21" s.license = "New BSD" s.authors = { 'imagawa' => 'test@example.com' } s.homepage = "http://example.com" s.summary = "grpc client" s.source = { :git => 'https://github.com/yusuke-imagawa/talking-ios.git' } s.ios.deployment_target = "7.1" s.osx.deployment_target = "10.9" # Base directory where the .proto files are. src = "./proto" # Run protoc with the Objective-C and gRPC plugins to generate protocol messages and gRPC clients. s.dependency "!ProtoCompiler-gRPCPlugin", "~> 1.0" # Pods directory corresponding to this app's Podfile, relative to the location of this podspec. pods_root = '../Pods' # Path where Cocoapods downloads protoc and the gRPC plugin. protoc_dir = "#{pods_root}/!ProtoCompiler" protoc = "#{protoc_dir}/protoc" plugin = "#{pods_root}/!ProtoCompiler-gRPCPlugin/grpc_objective_c_plugin" # Directory where the generated files will be placed. # dir = "#{pods_root}/#{s.name}" s.prepare_command = <<-CMD #{protoc} \ --plugin=protoc-gen-grpc=#{plugin} \ --objc_out="./src" \ --grpc_out="./src" \ -I #{src} \ -I #{protoc_dir} \ #{src}/*.proto CMD # Files generated by protoc s.subspec "Messages" do |ms| ms.source_files = "src/*.pbobjc.{h,m}", "src/**/*.pbobjc.{h,m}" ms.header_mappings_dir = '.' ms.requires_arc = false # The generated files depend on the protobuf runtime. ms.dependency "Protobuf" end # Files generated by the gRPC plugin s.subspec "Services" do |ss| ss.source_files = "src/*.pbrpc.{h,m}", "src/**/*.pbrpc.{h,m}" ss.header_mappings_dir = '.' ss.requires_arc = true # The generated files depend on the gRPC runtime, and on the files generated by protoc. ss.dependency "gRPC-ProtoRPC" ss.dependency "#{s.name}/Messages" end s.pod_target_xcconfig = { # This is needed by all pods that depend on Protobuf: 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1', # This is needed by all pods that depend on gRPC-RxLibrary: 'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES', } end各APIのクライアント
PushNotifyApiClient.swift
AccountApiClient.swift
BlockApiClient.swift
CallingApiClient.swift
ChatApiClient.swift
ReportApiClient.swift
UsersApiService.swift例:ReportApiClient
import RemoteClient import ReactiveSwift class ReportApiClient { static let shared = ReportApiClient() private init() {} private var service: ReportService? { return GrpcServiceLoader.shared.getReportService() } func reportUser(toUserId: Int) -> SignalProducer<Bool, Error> { return SignalProducer<Bool, Error> { (observer, lifetime) in let request = PostReportRequest() request.toUserId = Int64(toUserId) self.service?.rpcToPostReport(with: request) { (response: GPBEmpty?, error: Error?) in if let error = error { observer.send(error: error) return } observer.send(value: true) observer.sendCompleted() }.startWithHeaders() } } }request時にユーザー認証用のheaderを設定
- この処理は必須ではないです。
- ユーザーの認証情報の設定箇所を共通化するため、独自に実装しています。
extension GRPCProtoCall { func startWithHeaders() { if let userId = CurrentUserService.getCurrentUserId(), let apiToken = CurrentUserService.getApiToken() { requestHeaders.addEntries(from: ["user_id":String(userId)]) requestHeaders.addEntries(from: ["api_token":apiToken]) } start() } }request時に startWithHeaders() を呼び出す。
BlockApiClient.swiftfunc block(toUserId: Int) -> SignalProducer<Bool, Error> { return SignalProducer<Bool, Error> { (observer, lifetime) in let request = BlockRequest() request.toUserId = Int64(toUserId) self.service?.rpcToBlock(with: request) { (response: GPBEmpty?, error: Error?) in if let error = error { observer.send(error: error) return } observer.send(value: true) observer.sendCompleted() }.startWithHeaders() } }参考記事
・Xcode(Swift) + CocoaPodsでgRPCのClientを生成してServiceを呼び出すまでの流れ
SwiftでGRPCを動かす方法と基本的な挙動について
公式のサンプル
- 投稿日:2020-07-28T12:28:28+09:00
iOS Simulator でタイムゾーンを変更して実行する方法
タイムゾーンを越えたテストなど、iOS Simulator のタイムゾーンを変更したいときってありますよね?
さっとググると、MacOSのタイムゾーンを変更しろ!なんて情報がありますけど、MacOS の他のアプリケーションにも影響しそうで積極的にはやりたくありません。
別の方法はあるのでしょうか?
いろいろと調べたところ、結論からいうと、ビルド時の環境変数 TZ を変更することで、ビルドしたアプリ限定でタイムゾーンを変更できるようです。
正確には Simulator のタイムゾーンは変更されないのですが、僕が確認したかった範囲ではこれで十分だったので有用と思いましてまとめました。手順
まずは、xcode 左上のアプリ名のところを選択して、「Edit Scheme...」を選びます。
次に、、下図のように選択します。
③ のところには、Name に
TZ
、Value にタイムゾーンを示す文字列を入力します。Value の指定は、tz database にある
Asia/Tokyo
といった文字列や、JST, PST のような略語文字列でも扱ってくれるようです。タイムゾーン指定の標準については詳しくないのですが、tz database から選ぶのが無難と思います。確認方法と注意点
まずは注意点。この方法だと、正確には 、実行しているアプリ内でのみタイムゾーンが変化し、iOS Simulator 内のタイムゾーンは変化しません。
なので、iOS 上部のステータスバーの時刻は変わりません。ですが、アプリ内の時刻は指定したタイムゾーンで処理されています。確認した方法は次のとおりです。
まずは、なにも指定無し(= MacOS 側のシステム設定)です。システム設定は、
Asia/Tokyo
です。適当なところに以下のコードをコピペします。
let today = Date.today() let df = DateFormatter() df.dateFormat = "hh:mm Z" df.timeZone = TimeZone.current print(today) print(df.timeZone!) print(df.string(from: today))実行すると、以下のようにコンソールに表示されます。日本時間で正しく表示されています。
2020-07-28 03:19:29 +0000 Asia/Tokyo (current) 12:19 +0900次に、
TZ
にAmerica/Los_Angeles
を設定して実行してみます。2020-07-28 03:19:55 +0000 America/Los_Angeles (current) 08:19 -0700適切にタイムゾーンが変更されていることを確認できました。
同様に、ちょっとマイナーな
IRST
(イラン標準時)を4文字略称で設定してみます。2020-07-28 03:20:36 +0000 Asia/Tehran (current) 07:50 +0430こちらも確認することができました。
参考文献
https://stackoverflow.com/questions/1699671/how-to-change-time-and-timezone-in-iphone-simulator
※ この記事は、xcode 11.4.1 で確認しています。
- 投稿日:2020-07-28T12:04:25+09:00
UnityのiOSビルド時にBuild SettingsやBuild Phases等の項目を自動追加する方法
実装環境
- Unity 2019.2.21f1
- Xcode 11.6
参考サイト
方法
参考サイト記載のサンプルプログラムをダウンロードし、'Assets/Editor'フォルダ下に設置
Users/UserOnPostBuild.cs
内のEditProj
メソッドを適宜書き換えるPostXcodeBuild.csprivate static void EditProj(string pathToBuiltProject) { // 項目追加に必要な情報を取得(必須) var projPath = pathToBuiltProject + "/Unity-iPhone.xcodeproj/project.pbxproj"; var pbxProj = new Users.Custom.PBXProject(); pbxProj.ReadFromFile(projPath); var targetGuid = pbxProj.TargetGuidByName("Unity-iPhone"); // BuildPhasesの追加サンプル // 不要なら削除 pbxProj.AppendShellScriptBuildPhase(targetGuid,"Run Script copy_test.sh","/bin/sh","./../Assets/Editor/copy_test.sh"); // BuildSettingsの追加処理サンプル // 不要なら削除 pbxProj.SetBuildProperty(targetGuid, "IPHONEOS_DEPLOYMENT_TARGET", "9.0"); // Frameworkの追加処理サンプル // 不要なら削除 pbxProj.AddFrameworkToProject(targetGuid, "CoreBluetooth.framework", true); // embedded frameworkの追加サンプル // 不要なら削除 var defaultLocationInProj = "Frameworks/Plugins/iOS/EmbeddedFramework/"; var relativeCoreFrameworkPath = ""; string[] commonFrameworkNames=new string[]{"EmbeddedFramework1","EmbeddedFramework2"}; foreach (var frameworkNameTemp in commonFrameworkNames) { var frameworkName = frameworkNameTemp+".framework"; relativeCoreFrameworkPath = Path.Combine(defaultLocationInProj, frameworkName); AddDynamicFrameworks (ref pbxProj,targetGuid,relativeCoreFrameworkPath); } // .dylibの追加サンプル // 不要なら削除 pbxProj.AddFileToBuild(targetGuid, pbxProj.AddFile("usr/lib/libresolv.dylib", "Frameworks/libresolv.dylib", Users.Custom.PBXSourceTree.Sdk)); // 追加処理を書き込む(必須) pbxProj.WriteToFile(projPath); }
- 投稿日:2020-07-28T00:33:16+09:00
[Swift] var-forパターンを避けよう
はじめに
var-forパターンは既存の言葉ではありません。
このアンチパターンよく見かけるので自分でvar-forパターンと勝手に命名しました。
コードレビューとかで「これvar-forパターンだよね」という感じで使えるかもしれません。本記事の内容はSwiftの初心者向けです。
var-forパターンとは
以下のような一時変数varとfor文(forEach、while、repeat-whileなども含む)を利用したロジックです。
例題)1から10までの整数を3倍して、6の倍数のみの配列を生成
var result = [Int]() for number in 1...10 { let tripleNumber = number * 3 if tripleNumber % 6 == 0 { result += [tripleNumber] } }よくあるのが、配列から特定の条件を満たした別の配列を作る時です。
var-forパターンの置き換え
たいていのvar-forパターンは以下のようにmapやcompactMap、filter、reduceなどの高階関数で置き換えられます。
※高階関数がわからない方向けの記事:イメージで理解するSwiftの高階関数(filter, map, reduce, compactMap, flatMap)let result = (1...10) .map { $0 * 3 } .filter { $0 % 6 == 0 }var-forパターンのデメリット
可読性が低い
読み手の頭の中を想像して、var-forパターンのロジックを日本語にしてみます。
var result = [Int]() // 要素がInt型のresultという名前の配列を定義し、空で初期化する for number in 1...10 { // 1から10までループさせ let tripleNumber = number * 3 // numberを3倍したtripleNumberという名前の定数を用意する if tripleNumber % 6 == 0 { // もしもtripleNumberが6で割り切れたら、 result += [tripleNumber] // resultにtripleNumberを追加する } // tripleNumberが6で割り切れなければ何もしない }次に、高階関数を利用したパターンです。
let result = (1...10) // 1から10までの整数を .map { $0 * 3 } // 3倍して、 .filter { $0 % 6 == 0 } // 6の倍数のみにした定数resultを定義上記を比較してみます。
var-forパターン
要素がInt型のresultという名前の配列を定義し、空で初期化する、1から10までループさせ、numberを3倍したtripleNumberという名前の定数を用意する、もしもtripleNumberが6で割り切れたら、resultにtripleNumberを追加する、tripleNumberが6で割り切れなければ何もしない
高階関数パターン
1から10までの整数を3倍して、6の倍数のみにした定数resultを定義
どちらがわかりやすいでしょうか?
後者の方が例題そのものの性質をよく表現しており、より人間が読みやすく、宣言的なコーディングスタイルになっています。
(プログラミングパラダイムでいうと、前者は命令型プログラミング、後者は宣言型プログラミングと分類できます)修正がしにくい
例題にこんな仕様変更があったらどう修正するでしょうか?
1から10までの整数を3倍して、6の倍数のみにした先頭要素2つの配列を生成
var-forパターンの場合、このような修正が思いつきますが、要素数が2個未満の場合に要素を追加するというのはあまり宣言的ではなく、
result.count =< 2
のような間違ったコードを書いてしまう可能性もあります。var result = [Int]() for number in 1...10 { let tripleNumber = number * 3 if tripleNumber % 6 == 0 && result.count < 2 { result += [tripleNumber] } }高階関数パターンであれば、以下のようにprefixを追加するだけです。
let result = (1...10) .map { $0 * 3 } .filter { $0 % 6 == 0 } .prefix(2)バグを生みやすい
修正がしにくいにも書きましたが、可読性が悪い、変更がしづらいというのはバグを生みやすいコードです。
また、let定数でなく、var変数のように、コード内に値が変化する箇所があるというのもバグを生みやすい原因になります。よりバグの少ないない安全なプログラムを作るには変化する箇所をできるだけ減らすことが有効です。
状態がなく、変化しないものはテストパターンが少なく済むので、バグを見逃す確率がぐっと減ります。
- varよりもletをなるべく使う(変化させない)
- class(参照型)よりもstruct/enum(値型)を使う(変化を伝播させない)
- プリミティブ型よりも独自型やenumを利用する(変化する値のとりうる範囲を制限する)
まとめ
- var-forパターンよりも高階関数を利用する
- var-forパターンは可読性、修正しやすさ、安全さが劣る場合がある