- 投稿日:2020-05-31T22:37:20+09:00
Swift StoryboardでScroll ViewとStack Viewを組み合わせて使う
- 投稿日:2020-05-31T21:48:23+09:00
Swift StoryboradでStack Viewを使う
- 投稿日:2020-05-31T20:26:29+09:00
[iOS] Metalで効率的にループ計算する機能 "Raster Order Group" について
Apple製のGPUシェーダー言語Metalにて、ループ計算を効率的に行うことができる仕組み「Raster Order Group」について実験してみたのでまとめてみたいと思います。
はじめに
この記事では以下について紹介します。
- Raster Order Groupとは?
- Raster Order Groupの処理の流れ
- 実装における注意点
以下では、Appleが公開しているサンプルコード (リンク) をベースに、コードの中身の簡単な解説と実装時の注意点について説明します。
Raster Order Groupとは?
例えば以下例のように、一番めのテクスチャに対してカーネル計算を行い、その結果を使って次のテクスチャを描画するような場合、従来の方法だとTexture1のカーネル計算の結果を受け取るためには一度結果のデータをGPUからCPUへ転送し、改めてCPUからGPUへ転送するような手順を踏まなければ、計算結果を反映下処理ができませんでした。
*Apple開発者ページより (リンク) より.このような手順を踏むと、上図のように余計な待ち時間が発生することは素より、この実装におけるより大きな問題はCPU-GPU間のデータ転送に小さくないコストが掛かる事にあります。(下図)
*Apple開発者ページより (同上ページ)
CPUへの無駄なデータ転送を抑制する
そこでこのような計算を実施するための自然な方法として、GPU内部のメモリに一時的に中間状態のbufferを保持しておき、CPU-GPU間のデータ転送に係るオーバーヘッドを極力減らす手法 "Raster Order Group" がMetal2で公開されました。
これと同様の機能は、例えば Unity (HLSL) では GrabPass{} が相当します. 一般的にはマルチパスレンダリングと呼ばれる機能になります.
Raster Order Groupの処理の流れ
基本的に下図のように、MTLTextureオブジェクトを用意してループ計算結果をRead/Writeしながら描画を進めることになります.
textureの設定の仕方は筆者の把握する限り、2通りのやり方があると思います.
-renderEncoder.setFragmentTexture()
でループ描画用の MTLTexture オブジェクトを設置して描画する方法.
- 1つはrenderPassDescriptor.colorAttachment
に設置する MTLTexture オブジェクトに描画する方法.これらに共通する点は、入力に加えて描画のループ回数分だけCPU側で MTLTexture オブジェクトを用意し、ループ計算の度に結果を texture に write するという点です. この texture の Read/Write においては注意点があります.
- 1つのrenderPassDescriptorに対しcolorAttachmentは8つまで. つまり8ループが限界である.
- これはどうやら renderEncoder.setFragmentTexture() を使う場合でも同じで、1度に8ループまでが限度になっているようである.
- 同じtextureに2度書き込むことはできない. よって必ず描画回数分のtextureを用意する必要がある.
- (textureのReadに関する重要な注意点は次節で述べます.)
具体的な実装
1. colorAttachmentを使う方法
ここではApple開発者ページのコードを引用します.
struct GBufferData { half4 lighting [[color(AAPLRenderTargetLighting), raster_order_group(AAPLLightingROG)]]; half4 albedo_specular [[color(AAPLRenderTargetAlbedo), raster_order_group(AAPLGBufferROG)]]; half4 normal_shadow [[color(AAPLRenderTargetNormal), raster_order_group(AAPLGBufferROG)]]; float depth [[color(AAPLRenderTargetDepth), raster_order_group(AAPLGBufferROG)]]; };lightingやalbedo_specularはtexture名で、それぞれに紐づくtextureオブジェクトはrenderPassDescriptorで設定するcolorAttachmentに渡されています.
そして上記のcolor(n)はcolorAttachmentのインデックスを、raster_order_group(n)は本稿の主題であるループ計算のグループを示すインデックスを、それぞれ設定している修飾子です.raster_order_groupのインデックス値が同じtexture同士は同じタイミング (同じループ) で描画されます.
ちなみにraster_order_groupのインデックスの数値はループ計算の順番とは関係ありません. しかし可読性のため、ループ計算の順番とインデックスの数値を揃えておいた方が無難かもしれません.以下は入力されたMTLTextureをreadしてraster_order_group指定されたcolorAttachmentへwriteする部分の抜粋です.
fragment GBufferData gbuffer_fragment(ColorInOut in [[ stage_in ]], constant AAPLUniforms &uniforms [[ buffer(AAPLBufferIndexUniforms) ]], texture2d<half> baseColorMap [[ texture(AAPLTextureIndexBaseColor) ]], texture2d<half> normalMap [[ texture(AAPLTextureIndexNormal) ]], texture2d<half> specularMap [[ texture(AAPLTextureIndexSpecular) ]], depth2d<float> shadowMap [[ texture(AAPLTextureIndexShadow) ]]) { (略) half4 base_color_sample = baseColorMap.sample(linearSampler, in.tex_coord.xy); half4 normal_sample = normalMap.sample(linearSampler, in.tex_coord.xy); half specular_contrib = specularMap.sample(linearSampler, in.tex_coord.xy).r; // Fill in on-chip geometry buffer data GBufferData gBuffer; (略) // Store shadow with albedo in unused fourth channel gBuffer.albedo_specular = half4(base_color_sample.xyz, specular_contrib); // Store the specular contribution with the normal in unused fourth channel. gBuffer.normal_shadow = half4(eye_normal.xyz, shadow_sample); gBuffer.depth = in.eye_position.z; (略) return gBuffer;colorAttachmentへの書き込みはreturnによって行われます. 複数のcolorAttachment (MTLTexture) へwriteするためには上記のように構造体を用います.
texture2dオブジェクトのbaseColorMapなどはrenderEncoderのsetFragmentTexture()で渡される入力textureです. これを所定の描画を行い、raster_order_groupを指定したGBufferDataの要素に渡します. これをreturnすれば描画順を考慮して出力してくれます.
fragment AccumLightBuffer deferred_directional_lighting_fragment(QuadInOut in [[ stage_in ]], constant AAPLUniforms & uniforms [[ buffer(AAPLBufferIndexUniforms) ]], GBufferData GBuffer) { AccumLightBuffer output; output.lighting = deferred_directional_lighting_fragment_common(in, uniforms, GBuffer.depth, GBuffer.normal_shadow, GBuffer.albedo_specular); return output; }次ループで先ほど書き込まれたtextureをreadするのは簡単で、fragment関数の入力にGBufferDataを指定して要素にアクセスするだけです.
なおraster_order_group指定されたtextureの描画順ですが、これはrenderEncoderのdraw関数のコール順となります.
_viewRenderPassDescriptor.colorAttachments[AAPLRenderTargetLighting].texture = drawableTexture; _viewRenderPassDescriptor.depthAttachment.texture = self.view.depthStencilTexture; _viewRenderPassDescriptor.stencilAttachment.texture = self.view.depthStencilTexture; id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_viewRenderPassDescriptor]; renderEncoder.label = @"Combined GBuffer & Lighting Pass"; [super drawGBuffer:renderEncoder]; [self drawDirectionalLight:renderEncoder]; [super drawPointLightMask:renderEncoder]; [self drawPointLights:renderEncoder]; [super drawSky:renderEncoder]; [super drawFairies:renderEncoder]; [renderEncoder endEncoding];コードの詳細は割愛しますが下記のdrawGBuffer等の関数中でrenderEncoder.draw---が呼ばれ描画されています. renderEncoderが生成されてからrenderEncoder.endEncoding()が呼ばれるまでの間にfragment関数を呼ぶ順でループ順が考慮されてraster_order_groupが機能する仕組みです.
2. setFragmentTextureを使う方法
renderEncoder.setFragmentTexture()に渡すtextureへのread/writeでも、shader側の修飾子による指定でraster_order_groupを利用することができます. こちらのApple開発者ページよりコードを引用します.
fragment void blend(texture2d<float, access::read_write> out[[texture(0), raster_order_group(0)]]) { float4 newColor = 0.5f; // the GPU now waits on first access to raster ordered memory float4 oldColor = out.read(position); float4 blended = someCustomBlendingFunction(newColor, oldColor); out.write(blended, position); }なおtextureの修飾子にはwriteが必要になりますが、読み込み時に用いることができる
.sample()
に相当するピクセル間をサブサンプリングしてくれるようなfunctionは、書き込みにおいては(多分)ありません. したがって座標値を正しく指定してwriteする必要がありそうです.実装における注意点
CPU側の設定
colorAttachmentの設定
_viewRenderPassDescriptor = [MTLRenderPassDescriptor new]; _viewRenderPassDescriptor.colorAttachments[AAPLRenderTargetAlbedo].loadAction = MTLLoadActionDontCare; _viewRenderPassDescriptor.colorAttachments[AAPLRenderTargetAlbedo].storeAction = MTLStoreActionDontCare; _viewRenderPassDescriptor.colorAttachments[AAPLRenderTargetNormal].loadAction = MTLLoadActionDontCare; _viewRenderPassDescriptor.colorAttachments[AAPLRenderTargetNormal].storeAction = MTLStoreActionDontCare; _viewRenderPassDescriptor.colorAttachments[AAPLRenderTargetDepth].loadAction = MTLLoadActionDontCare; _viewRenderPassDescriptor.colorAttachments[AAPLRenderTargetDepth].storeAction = MTLStoreActionDontCare;colorAttachmentのパラメータとして、storeActionには
MTLStoreActionDontCare
を指定します. loadActionにも同じものを指定しており、これはraster_order_groupを使用してもしなくても変わりません.MTLTextureの設定
MTLTextureDescriptor *GBufferTextureDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm_sRGB width:size.width height:size.height mipmapped:NO]; GBufferTextureDesc.textureType = MTLTextureType2D; GBufferTextureDesc.usage |= MTLTextureUsageRenderTarget; GBufferTextureDesc.storageMode = storageMode; // !ここは MTLStorageModeMemoryless を指定!Raster Order Groupの書き込み用MTLTextureの生成において、textureDescriptorには
.memoryless
を指定する必要があるようです. memorylessはCPU側のメモリを使わないという意味らしい.ループ途中のtexture読み込み時の注意
Raster Order Groupを使用する際の最大の注意点と言っても良いかもしれません. 実はRaster Order Groupは非常に使いにくい一面があります. ループ途中のtextureはkernel計算等で必要な周囲の画素を読み込むことが (`20/May時点で) できません.
これはraster_order_groupループ途中のtextureはGPUの "Tile Memory" という仕組みを活用する一時メモリ領域に格納されるからです. Tile Memoryは、画素を一定サイズのブロックに分けてGPU描画を効率化する仕組みであり、どのメーカーのGPUでもよく使われる類の工夫のようです.
各タイルの描画は非同期であり、早く計算が完了したら随時次のタイルの計算に移行します. すなわちraster_order_groupのループ計算はタイル単位で非同期であることに気を付けねばなりません. この非同期計算を所謂awaitするような機能は、筆者がざっと探したところありませんでした.
(ただしMetalにはNeural Networkを効率化する機能が多数実装されており、kernel計算をループで同期的に処理する手段も用意されているはずです. これについてはいずれ調べてみたいと思います.)
おわりに
いかがでしたでしょうか?
ご参考になれば幸いです!
改善方法やご意見などあれば、どしどしコメント下さい!
- 投稿日:2020-05-31T17:56:31+09:00
【Swift】ノベルゲームっぽく文字を表示するライブラリ
ノベルゲームを作りたかった
Swiftでノベルゲームを作りたかったのですが、文字をそれっぽく表示するライブラリだけは発見できました。
他にも何かいい情報あれば教えてください!
cocoapodsでこれをインストール
pod 'SRGNovelGameTexts'あとはこのコードの表示位置やテキストを変えれば完成です。
// テキスト出力用のViewを初期化 let novelGameText = SRGNovelGameTexts() novelGameText.frame = CGRect(x: 100,y: 100,width: 100,height: 100) novelGameText.textColor = UIColor.black self.view.addSubview(novelGameText) // 出力したいテキストをセット novelGameText.setText("こーら!まだ起きないの?") // 演出を再生開始 novelGameText.startDisplayingText()
- 投稿日:2020-05-31T11:08:01+09:00
多段階選抜 (Swift)
Swift5で、yieldが使えるようになったようです。が、_read/_modifyアクセサ限定な上に、Pythonのcontextmanagerレベルのことしかできないようです(複数回yieldすらできない)。
私としては少々キレかかってしまいました、、お恥ずかしい。
まずはRuby版を、ループ末尾でyieldするように変換。
def drop_prev(check,prev) return to_enum(:drop_prev,check,prev) if !block_given? a=nil b=prev.next loop{ a,b=b,prev.next yield a if !check[b] } end def drop_next(check,prev) return to_enum(:drop_next,check,prev) if !block_given? first=true a=nil b=nil loop{ a,b=b,prev.next (first=false;yield b) if (first || !check[a]) } end def drop_n(check,n,prev) return to_enum(:drop_n,check,n,prev) if !block_given? i=0 loop{ i+=1 a=prev.next yield a if !check[i,n] } endこれであれば、yieldなしに書けそうです!
# にしても、
A!=B
と書くとdereference A assign Bと解釈されるので、<>
演算子を定義したほうが早いっていうね…tyama_hena24_enum.swift//usr/bin/env swift $0 $@;exit // http://qiita.com/Nabetani/items/1c83005a854d2c6cbb69 // http://nabetani.sakura.ne.jp/hena/ord24eliseq/ precedencegroup PowerPrecedence { associativity: right higherThan: MultiplicationPrecedence } infix operator <>: ComparisonPrecedence infix operator ≠: ComparisonPrecedence infix operator ≤: ComparisonPrecedence infix operator ≥: ComparisonPrecedence infix operator **: PowerPrecedence public func <> <T: Equatable>(lhs: T, rhs: T) -> Bool{return lhs != rhs} public func ≠ <T: Equatable>(lhs: T, rhs: T) -> Bool{return lhs != rhs} public func ≤ <T: Comparable>(lhs: T, rhs: T) -> Bool{return lhs <= rhs} public func ≥ <T: Comparable>(lhs: T, rhs: T) -> Bool{return lhs >= rhs} public func ** <T: Numeric>(lhs: T, rhs: Int64) -> T{ var x = lhs var k = rhs var e = 1 as T while k>0{ if k%2>0 {e*=x} x*=x k/=2 } return e } func isqrt(_ n: Int64) -> Int64{ if n<=0 {return 0} if n<4 {return 1} var x: Int64 = 0 var y: Int64 = n while x<>y && x+1<>y { x = y y = (n/y+y)/2 } return x } func icbrt(_ n: Int64) -> Int64{ if n<0 {return icbrt(-n)} if n==0 {return 0} if n<8 {return 1} var x: Int64 = 0 var y: Int64 = n while x<>y && x+1<>y { x = y y = (n/y/y+y*2)/3 } return x } class generate: Sequence, IteratorProtocol{ var i: Int64 = 0 func next() -> Int64?{ i += 1 return i } } class drop_prev: Sequence, IteratorProtocol{ var prev: AnyIterator<Int64> var check: (Int64) -> Bool var a: Int64 = 0 var b: Int64 = 0 init(_ check_: @escaping (Int64) -> Bool, _ prev_: AnyIterator<Int64>){ check = check_ prev = prev_ b = prev.next()! } func next() -> Int64?{ while true{ a = b b = prev.next()! if !check(b){ return a } } } } class drop_next: Sequence, IteratorProtocol{ var prev: AnyIterator<Int64> var check: (Int64) -> Bool var first: Bool = true var a: Int64 = 0 var b: Int64 = 0 init(_ check_: @escaping (Int64) -> Bool, _ prev_: AnyIterator<Int64>){ check = check_ prev = prev_ } func next() -> Int64?{ while true{ a = b b = prev.next()! if first || !check(a){ first = false return b } } } } class drop_n: Sequence, IteratorProtocol{ var prev: AnyIterator<Int64> var check: (Int64,Int64) -> Bool var n: Int64 var i: Int64 = 0 init(_ check_: @escaping (Int64,Int64) -> Bool, _ n_: Int64, _ prev_: AnyIterator<Int64>){ check = check_ prev = prev_ n = n_ } func next() -> Int64?{ while true{ i += 1 let a = prev.next()! if !check(i,n){ return a } } } } func is_sq(_ n: Int64) -> Bool{return isqrt(n)**2==n} func is_cb(_ n: Int64) -> Bool{return icbrt(n)**3==n} func is_multiple(_ i: Int64, _ n: Int64) -> Bool{return i%n==0} func is_le(_ i: Int64, _ n: Int64) -> Bool{return i<=n} var f: [Character: (AnyIterator<Int64>) -> AnyIterator<Int64>] = [:] f["S"]={AnyIterator(drop_next(is_sq,$0))} f["s"]={AnyIterator(drop_prev(is_sq,$0))} f["C"]={AnyIterator(drop_next(is_cb,$0))} f["c"]={AnyIterator(drop_prev(is_cb,$0))} f["h"]={AnyIterator(drop_n(is_le,100,$0))} for i in 2...9{f[Character(String(i))]={AnyIterator(drop_n(is_multiple,Int64(i),$0))}} while true{ let s = readLine() if s==nil{break} var g: AnyIterator<Int64> = AnyIterator(generate()) for c in s!{ g = f[c]!(g) } var c = 0 for e in g{ c += 1 print(e,terminator:c==10 ? "\n" : ",") if c==10{break} } }しかし。。こうできちゃった以上はイテレータの概念がない言語でも(OOであれば)書けるってことなんだよなぁ…
# 正直IteratorProtocolにちゃんと準拠した書き方を調べるほうが大変だったんですけどね--;;;
- 投稿日:2020-05-31T10:46:56+09:00
【Swift】Protocolについて
仕様書、プロトコル
プロトコルとは、一言で言えば仕様書です。
クラスや構造体、列挙などはしばしば設計書と言われますが、プロトコルはその設計書の仕様書と言えます。
プロトコルでは設計書内にどのようなプロパティやメソッドが入っているのを単文で記述します。そして、設計書でその具体的な処理について記述すると言うように分担しています。
プロトコルを使うメリット
仕様書であるプロトコルを使うことで、設計書であるクラス、構造体、列挙などの構成の組み立てに枠組みを用意することができます。
仕様書に従って、この設計図にはこのメソッドを書く必要があるな、、などと言うように。
コードで見てみる
クラス(設計書):Testは、プロトコル(仕様書):testProtocolに準拠する場合のコードです。
プロトコルで、プロパティを指定する際は、
- varのみ(letは使えない)
- {get}:読み取り専用か{get set}:読み取り・代入 かを指定しなければならない
の2つの注意点があります。
下のサンプルコードでは、1つを{get}
、もう1つを{get set}
としました。protocol testProtocol{ var getProperty: String { get } //読み込み専用 var getset: String { get set } //読み込み・代入が可能 func ABC() } class Test: testProtocol{ var getProperty: String = "get" var getset: String { get { return "getset" } set{ getProperty = newValue // getPropertyは"get"のまま } } func ABC() { print(getProperty) print(getset) } } let test = Test() test.ABC()出力get getsetgetPropertyは
{get}
で指定したので、値が変更されてませんね最後に
プロトコルは、delegateなどにも使われています。
delegate単体で概念を理解するより、プロトコル自体の理解を深めた方がdelegateだけでなく、他の概念についても理解が深まるなあと思いました!
- 投稿日:2020-05-31T08:31:50+09:00
こんなソースコードはイヤだ-必要のないクラスのプロパティは作らない
プログラムのソースコードのより良い書き方をまとめていこうと思います。
必要のないクラスのプロパティは作らない
sample.swiftclass SampleViewController : UIViewController { var nextViewController: UIViewContoller! func showNext() { self.nextViewController = NextViewController() self.navigationController?.pushViewController(self.nextViewController , animated: true) } }
- 投稿日:2020-05-31T07:16:53+09:00
Swift: ネットワークリクエスト(URLSession、Alamofire を使って)、ファイルのアップロード、SSL証明書の検証を行うためのコード
この記事の内容
NSURLSession
とAlamofire
を使用してネットワークPOST
もしくはGET
リクエストを行う方法について説明します。- SSL Pinning (SSL証明書の検証を行うためのコード)
サンプルコードが提供されるので、このページをブックマークし、これらのコードをプロジェクトにすばやく追加できます。
どうやってテストをするのか?
アプリケーションがネットワークリクエストを成功させることができるかどうかをテストするためのデバッグオプションはたくさんあります。プロキシ、またはWebフックを使用できます。ここでは、無料のオンラインWebフックツール
https://webhook.site/
を使用しました。サイトにアクセスすると、Webフックリンクが表示されます。そのURLへのリクエストを行うことができ、そしてウェブサイトのインターフェイス上でリクエスト内容を見ることができます。
Alamofire
Alamofire
はネットワーク用のオープンソースのフレームワークです。GET / POST
func sendRequestRequest() { // httpリクエストヘッダーを追加する let headers = [ "Authorization":"Bearer ABCD-EFGH-123-456", "CatName":"NekoNohe", ] let httpHeaders = HTTPHeaders(headers) // リクエストを実行する AF.request("https://apple.com/", method: .get, headers: httpHeaders) .validate(statusCode: 200..<300) //.responseJSON { response in .responseString { response in switch response.result { case .success(let value): debugPrint("HTTP Response Body: \(response.data)") case .failure(let error): print(error.localizedDescription) } } }ファイルのアップロード (multipartFormData)
func requestWith(image: UIImage){ let url = "https://webhook.site/xxxxxxxxxxxx" let headers: HTTPHeaders = [ "Content-type": "multipart/form-data" ] guard let imageData = image.jpegData(compressionQuality: 1.0) else { return } AF.upload(multipartFormData: { (multipartFormData) in multipartFormData.append(imageData, withName: "image", fileName: "image.jpg", mimeType: "image/jpeg") }, to: url, method: .put, headers: headers).responseString { (response) in switch response.result { case .success(let result): print(result) case .failure(let error): print(error.localizedDescription) } } }上記の方式を示す引数は使用対象のウェブサイトが要求する方式に変更する必要があります。一部のウェブサイトでは、
POST
方式が必要となる可能性があります。SSL Pinning (証明書の検証)
SSLピン留めとは
クライアントからサーバーにネットワーク要求を行う場合、クライアントとサーバーの間に第三者が存在する可能性があります。そして、第三者がリクエストを傍受し、独自の証明書を使ってデータを変更する可能性があります。しかし、SSL証明書の有効性を検証し、それらが実際のサーバーに属していることを確認すると、そういった行為を防ぐことができます。
ウェブサイトのSSLサーバ証明書を取得する
Safariブラウザを開き、ウェブサイト(https://apple.com など)にアクセスします。
ロックアイコンをクリックし、ボタンをクリックすると詳細が表示されます。
次に、証明書をアイコンからファインダにドラッグします。
コード
まず、パブリック認証ファイルをプロジェクトに追加する。この例では、ファイル名は
www.apple.com.cer
だ。ファイルがプログラミングのターゲットにリンクされていることを確認すること。
Alamofire session
を用意する。var session: Session! func getSecureSession() -> Session? { if let filePath = Bundle.main.path(forResource: "www.apple.com", ofType: "cer"), let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)), let certificate = SecCertificateCreateWithData(nil, data as CFData) { let manager = ServerTrustManager(evaluators: [ "www.apple.com": PinnedCertificatesTrustEvaluator(certificates: [certificate], acceptSelfSignedCertificates: false, performDefaultValidation: true, validateHost: true) ]) let configuration = URLSessionConfiguration.af.default return Session(configuration: configuration, serverTrustManager: manager) } return nil }ここに、ドメイン名とポリシーをマッピングする辞書(ディクショナリ)がある。
"www.apple.com": PinnedCertificatesTrustEvaluator(certificates: [certificate], acceptSelfSignedCertificates: false, performDefaultValidation: true, validateHost: true)リクエストする。
func sendRequestRequest() { guard let session = getSecureSession() else { return } self.session = session // Perform the request self.session.request("https://apple.com/", method: .get) .validate(statusCode: 200..<300) //.responseJSON { response in .responseString { response in switch response.result { case .success(let value): debugPrint("HTTP Response Body: \(response.data)") case .failure(let error): print(error.localizedDescription) } } }そうすると、接続しているドメインが
www.apple.com
なのであればリクエストが通る。SSL証明を別のウェブサイトの証明書と置き換え、リクエストエラーが発生することを確認してみるといいだろう。
注
SSLサーバ証明書には有効期限があります。
アプリに保存されているSSLサーバ証明書は、たまに更新する必要があります。
NSURLSession
POST / GET
func sendRequest() { let sessionConfig = URLSessionConfiguration.default let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil) //URLを設定する let urlString = "https://webhook.site/xxxxxxxxxxxx" guard let URL = URL(string: urlString) else { return } var request = URLRequest(url: URL) //リクエストの方式 `(GET, POST)` を設定する request.httpMethod = "GET" //OAuth 2 Bearer トークン request.addValue("Bearer ABCD-EFGH-123-456", forHTTPHeaderField: "Authorization") //httpリクエストヘッダーを追加する request.addValue("NekoNohe", forHTTPHeaderField: "CatName") request.addValue("token=NekoNeko", forHTTPHeaderField: "Cookie") //httpリクエストボディーを追加する let JSONdictionary : [String: Any] = [ "name": "axel", "favorite_animal": "fox" ] if let postData = try? JSONSerialization.data(withJSONObject: JSONdictionary, options: []) { request.httpBody = postData } //データタスクを定義する let task = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in if (error == nil), let httpResponse = response as? HTTPURLResponse { // リクエスト成功 let statusCode = httpResponse.statusCode // TODO: コードをここに追加する } else { print(error?.localizedDescription) } }) task.resume() session.finishTasksAndInvalidate() }その他の
私は以前に
Swift
&NSURLSession
プログラムを書く方法について記事を書いています。こちらからお読みいただけます。
- 投稿日:2020-05-31T04:14:15+09:00
[Swift]Admobの「ポリシー違反(インタースティシャル広告の導入における禁止事項)」に対応した
背景
初めてGoogle Admobを導入したアプリのリリースから約1ヶ月後、ポリシー違反のメールを受信した。
メール本文内に記載のポリシーセンターへのリンクを参照すると、「ユーザーが予期しないタイミングでの広告表示」のため対応するようにとのこと。
ガイドラインを確認すると、なるほど、確かに記載されている。
違反内容の確認
ポリシーセンターでは、具体的にどの画面のどの操作のどの動きが違反しているとまでは記載されていない。
案内されるガイドラインを読み進めて違反ケースを一つ一つ確認し、該当するケースを自分で見極める必要がある。今回のケースはインタースティシャル広告の導入における禁止事項の
予期しないタイミングでの表示
が該当した。
対応
ガイドラインで解説されている通り
アクション → 画面遷移 → 広告表示
、から、アクション → 広告表示 → 画面遷移
の動きに変更した。再審査
アプリを修正しリリースを行った後、Google Admobに審査を行ってもらう必要がある。
Admobのポリシーセンター上で変更点を記載し、審査リクエストを送信する。1週間以内には再審査を行う旨の自動応答メールを受信し、待つことになる。
が、今回は半日と待たずに再審査で問題がなかった旨のメールを受信し、違反ステータスが解除された。
- 投稿日:2020-05-31T03:29:01+09:00
[Swift]R.swiftで生成するViewControllerのinitにパラメータを渡す
背景
例によって、個人開発したScoreBoxという麻雀成績管理アプリからのネタ。
iOS13ではStoryboardでもDIができる件についてを拝見し、せっかくなのでR.swiftを利用して実装しました。
リンク先でもメリットを記載されていますが、プロパティのlet化、この恩恵は非常に大きいです。やったこと
let gameDetailVC = R.storyboard.main().instantiateViewController(identifier: R.storyboard.main.gameDetail.identifier) { coder in // GameDetailViewControllerはプロパティに`let game`を持ち、initの引数に必須としている GameDetailViewController(coder: coder, game: game) } navigationController?.pushViewController(gameDetailVC, animated: true)
- 投稿日:2020-05-31T02:37:40+09:00
[Swift]複数のScrollViewのスクロールを同期する
背景
例によって、個人開発したScoreBoxという麻雀成績管理アプリからのネタ。
本アプリのコア機能となる成績管理の入力は表形式になります。
大きく分けると、成績を蓄積する部分、合計値を表す部分の2つ。
性質が異なるため1つの表での表現は難しく、今回は2つの表に分けることにしました。なお、表のコントロールにはSpreadsheetViewを利用させていただきました。
ありがとうございます。悩んだ
表を分けるようにしたのはいいものの、
ScrollViewのスクロール検知はdelegateが紐づいている1つのViewでしか有効でないので、
AのViewをスクロールしたからといって、勝手にBのViewも一緒にスクロールはしてくれません。そこで、AのViewのスクロールを検知したら、他のViewもスクロールを同期させる工夫が必要でした。
工夫した
- UIScrollViewDelegate
- NotificationCenter
を利用しました。
import UIKit import SpreadsheetView class GameDetailViewController: BaseViewController { // (A View)成績を蓄積する部分 @IBOutlet weak var scoreSheetView: SpreadsheetView! // (B View)合計値を表す部 @IBOutlet weak var sumSheetView: SpreadsheetView! override func viewDidLoad() { super.viewDidLoad() // 表コントロールのScrollViewへ Delegate を紐付ける scoreSheetView.scrollView.delegate = self sumSheetView.scrollView.delegate = self // スクロール同期を実行する通知イベントを登録する NotificationCenter.default.addObserver(self, selector: #selector(receiveScrollNotification(notification:)), name: .tableScroll, object: nil) } // スクロール同期する @objc func receiveScrollNotification(notification: NSNotification) { guard let scrollView = notification.object as? UIScrollView else { return } // 先にスクロールしたViewのスクロール位置を、他のViewに追従させる if scrollView == scoreSheetView.scrollView { sumSheetView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: sumSheetView.scrollView.contentOffset.y), animated: false) }else{ scoreSheetView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: sumSheetView.scrollView.contentOffset.y), animated: false) } } } extension GameDetailViewController : UIScrollViewDelegate { // あるViewのスクロールを検知したら、他のViewも一緒にスクロールするよう通知する func scrollViewDidScroll(_ scrollView: UIScrollView) { NotificationCenter.default.post(name: .tableScroll, object: scrollView) } }実際の画面
その他
今回は横スクロールの同期だけでしたが、縦スクロールも同期可能です。
- 投稿日:2020-05-31T02:22:09+09:00
Swiftでセマフォ(semaphore)にさわってみよう
はじめに
最近セマフォ(semaphore)について振り返る機会があり、Swiftでの動作を確認する意味もあってSwiftでセマフォのサンプルソースコードを作成しました。
そのソースコードを用いて、「セマフォって聞いたことはあるけど??」と思っているエンジニアのために本記事を書いてみました。
セマフォ(semaphore)とは
下記はwikipediaからの引用です
セマフォ(英: semaphore)とは、計算機科学において、並列プログラミング環境での複数の実行単位(主にプロセス)が共有する資源にアクセスするのを制御する際の、単純だが便利な抽象化を提供する変数または抽象データ型である。
セマフォを理解するキーワードは、「並列」と「資源」だと思います。
「並列」な動作を行う複数のスレッドやプロセスが、共有する「資源」を正しく利用できる(アクセス制御する)仕組みがセマフォです。
まずは、セマフォがない場合に何が起こるか、の例をSwiftのソースコード 1で以下に提示します。
セマフォ(semaphore)がない場合
このソースコード内では、resourceが「資源」で、スレッド1、スレッド2が「並列」に動作します。
スレッドは何度も繰り返すタスクをもっていて、それを完了すればスレッドも終了します。
それぞれのタスクは、resourceがゼロ以上の場合は、自分のカウンターを1加算してresourceを1減算します。resourceがゼロになれば処理を終了します。また
DispatchQueue.global(qos: .background).async
( リファレンス?) がそれぞれ使われており、qosが同じレベルのため、ほぼ均等のタイミングで実行状態が切り替わります。func testHandleResourceWithoutSemaphoreQiita() throws { var resource: Int = 100 var task1_counter: Int = 0 var task2_counter: Int = 0 // resourceがゼロ以上の場合は、自分のカウンターを1加算してresourceを1減算する func task1() -> Bool { var value = resource guard value > 0 else { return false } task1_counter += 1 value -= 1 resource = value return true } // resourceがゼロ以上の場合は、自分のカウンターを1加算してresourceを1減算する func task2() -> Bool { var value = resource guard value > 0 else { return false } task2_counter += 1 value -= 1 resource = value return true } // スレッド 1 DispatchQueue.global(qos: .background).async { var executing = true while executing { executing = task1() } } // スレッド 2 DispatchQueue.global(qos: .background).async { var executing = true while executing { executing = task2() } } // 2つのスレッドが完了するまで待つ print("task1_counter = \(task1_counter)") print("task2_counter = \(task2_counter)") }結果は、
task1_counter = 88
task2_counter = 102だったり
task1_counter = 100
task2_counter = 0だったり
task1_counter = 13
task2_counter = 101します。
resourceがゼロ以上の場合は、自分のカウンターを1加算してresourceを1減算します。resourceがゼロになれば処理を終了します。
であればtask1_counterとtask2_counterの合計は100になりそうなものですが、そうはなりません(なることもあります)
これはなぜかというと、
var value = resource guard value > 0 else { break } task2_counter += 1 value -= 1 resource = valueこの処理を行っている間に、別の処理が割り込んできて、resourceの値を書き換えてしまうからです。
つまり、変数resourceはアクセス制御されていません。スレッドセーフではない
、という言い方がよくされます。セマフォ(semaphore)を使ってみる
関数の先頭で
let semaphore = DispatchSemaphore(value: 1)を宣言します。
DispatchSemaphore(value: 1)
で、アクセス制御するリソースは1つだけだと宣言しています。リソースとは、var resource: Int = 100のことです。
この変数をアクセス制御できれば、おかしな現象は起こりません。続いて、先ほどの割り込みされるtask1(), task2()の先頭でそれぞれ
defer { semaphore.signal() } semaphore.wait()を追加します。
これで、関数に入った時点でsemaphore.wait()
が実行され、関数を抜けるタイミングで必ずsemaphore.signal()
が実行されます。
wait()ではDispatchSemaphoreで指定したカウンタを1減算し、signal()で1増加させます。カウンタがゼロの状態でwait()を呼び出したスレッドは、後述のようにBlocking状態となり処理は停止します。このスレッドは、signal()によってリソースが解放されるまでOSによってブロックされます。 2このような仕組みを導入することで、task1_counterとtask2_counterの合計は必ず100になることが保証されます。
スレッド/プロセスの状態遷移
Running(実行中)状態で、wait()を呼び出して運悪く誰かがリソースを使用していた場合は、Blocked状態になります。その後利用していたスレッドがsignal()を発行してリソースを解放すると、うまくOSスケジューラに拾ってもらえれば、Waiting状態に遷移し、無事Runningに復帰し、リソースにアクセスする権限を得られます。
上記の状態遷移図はiOSのものとは若干異なるのかもしれませんが、おおよそ同じような動きになるはずです。(正式な情報をお持ちでしたらお教えください)
セマフォがない場合のコンテキストスイッチ
アクセス制御がなされていない最初のコードでは、下記のようにスレッドが強制的にWaiting状態にスイッチされる(プリエンプションされてコンテキストスイッチが発生する)ことでtaskが中断しています。この中断の間に別のスレッドで、変数resourceが操作されるのが問題です。
繰り返しになりますが、ここで重要なのはセマフォがアクセス制御しているのは、リソースである変数resourceです。当該のリソースが何なのかを意識しないで漠然とセマフォを利用すると痛い目に遭います。要注意です。
セマフォがある場合のコンテキストスイッチ
セマフォを用いた状態遷移は下図のようになります。task1がセマフォを取得するとtask2はwait()を読んだ時点でBlocking状態に遷移します。のちにOSによって実行許可が出た場合は、Waitingを介してRunning状態に遷移し、wait()関数を抜けることになります。
セマフォが解放されたタイミングで次にどちらのスレッドが割り当てられるかは、OSのスケジューリングに寄ります。これを制御しようと思うのならば、
DispatchQueue.global(qos: .background).asyncのQoS(リファレンス?)を変更することである程度は可能です。QoSとはquality of serviceの略で、優先制御と呼ばれることが多いです。例えばインターネットで動画を途切れなく見るためにはQoSが有効なネットワークを利用することが必要だったりします。この場合は優先制御の他にも帯域制御が行われたりします。(参考記事?)
ここでは、iOSのスレッドスケジューリングの優先制御に影響を与えるパラメータと言うことができるでしょう。DispatchQueue.globalでは下記のようなオプションが用意されています。(リファレンス?)
定義値 説明 補足 userInteractive The quality-of-service class for user-interactive tasks, such as animations, event handling, or updating your app's user interface. アニメーションやUIの更新などはモバイルアプリにとっては重要です。そこでiOSではuserInteractive には最も高い優先順位が与えられています3 userInitiated The quality-of-service class for tasks that prevent the user from actively using your app. ユーザが開始したアクション、またそれに付随する動作、結果に対して高い優先順位を与えるオプションです default
The default quality-of-service class. 標準優先順位 utility The quality-of-service class for tasks that the user does not track actively. すぐに実行する必要がない処理に対して指定する background The quality-of-service class for maintenance or cleanup tasks that you create. バックグラウンドでCPUリソースなどに余裕がある時に実行される優先順位 unspecified The absence of a quality-of-service class. (?参考記事)
カウンティングセマフォ(Counting Semaphore)
let semaphore = DispatchSemaphore(value: 1)ではリソースが1つのために初期値として
1
を与えています。リソースが1つだけのセマフォをバイナリーセマフォ(Binary Semaphore)と呼びます。厳密には違う(参考時事?)のですが、ミューテックス(Mutex)と呼ばれることもあります。
リソースが2以上のセマフォはカウンティングセマフォ(計数セマフォ)と呼ばれ、アクセス制御に使うことはできません。
実は筆者も使ったことはありません?実際に2にしたらどうなるでしょうか。
サンプルソースコードの中でも実験していますが、興味がある方は試してみてください。?♂️?♂️?♂️?♂️
イベントフラグ(Event Flag)
イベントフラグにも触れておきたいと思います。
イベントフラグとは、主に組み込みプログラミングなどで利用されるμITRONで使われる用語です。実はPOSIXなどにもこの機能は明示されておらず、良い名称が無いのでイベントフラグという名称を使いたいと思います。イベントフラグについてはこちらの記事ITRON入門 イベントフラグで学ぶタスク間同期・通信機能を参照してください。
サンプルソースコードに記載されているソースコードを紹介します。
このサンプルでは、
DispatchSemaphore(value: 0)
で初期値をゼロとしています。バイナリーセマフォがスレッドをBlockすることを利用して、別のスレッドにイベントフラグとしてをsignalを送信し、Blockを解除します。func testSingleEventFlag() throws { let semaphore = DispatchSemaphore(value: 0) // ⚠️ イベントフラグの初期化 let expectation1 = XCTestExpectation(description: "expectation1") let expectation2 = XCTestExpectation(description: "expectation2") semLog.format("✳️ Start") DispatchQueue.global(qos: .background).async { usleep(1000_000) semaphore.signal() // ⚠️ イベントフラグを送信 semLog.format("✳️ Sent EventFlag") expectation1.fulfill() } semaphore.wait() // ⚠️ イベントフラグを受信 semLog.format("✴️ Recieved EventFlag") expectation2.fulfill() wait(for: [expectation1, expectation2], timeout: 10.0) }こちらの実行結果は、下記のようになります(semLog.format()は自作のログ出力ツールです。サンプルソースコードの中にあります)
テストを開始してから約1秒後にbackgroundのスレッドからイベントフラグの送信が起こり、待ち受けていたメインスレッドがBlockingからRunningに状態遷移して続きの処理が実行された様子が分かります。
/* ✳️ Start [18:37:25.837] [main] ✳️ Sent EventFlag [18:37:26.847] [com.apple.root.background-qos] ✴️ Recieved EventFlag [18:37:26.847] [main] */リソースのアクセス制御という観点から見ると、最初に説明した手法が
変数
などを守るためにあるのに対して、こちらはスレッドそれ自身を対象にしています。そのためスレッドが、いつ生成されて、いつ消滅するのかを考慮して設計、実装しなければなりません。上記のようなシンプルなものであれば良いのですが、大規模なマルチスレッドのシステムに動的に適用しようとすると、
イベントフラグの初期化と送受信のタイミング
が問題になることもあります。デッドロック(Deadlock)/ リソーススタベーション(Resource Starvation)
最後に、バイナリセマフォ、ミューテックスで必ず取り上げられるデッドロックについても紹介いたします。
下記は、二つのスレッドが、互いの管理するリソースを参照して動作しようとしてデッドロックします。
大きな問題は、wait() → signal()の間に、外部の(しかも相互参照している)オブジェクトを呼び出していることです。
外部のオブジェクトが実は、自分のリソースを操作する関数を呼び出していると知らなければ、容易にデッドロックが発生します。func testDeadlockBySemaphore() throws { var expectations: [XCTestExpectation] = [XCTestExpectation]() class SemThread { let semaphore = DispatchSemaphore(value: 1) var resource: Int = 0 var other: SemThread? let expectation: XCTestExpectation let name: String // 生成時にセマフォを取得し、外部からのインクリメントをブロックする init(name: String) { self.name = name expectation = XCTestExpectation(description: name) } // otherに対してカウントアップ要求を出す。その後、外部からのインクリメントを許可 func run() { DispatchQueue.global(qos: .background).async { self.increment() self.expectation.fulfill() semLog.format("⭐️\(self.name) completed") } } func increment() { semLog.format("⭐️\(self.name) will wait") semaphore.wait() usleep(100) semLog.format("⭐️\(self.name) make other increment") // 自セマフォを取得中に、相互参照している外部オブジェクトを利用する self.other?.increment() resource += 1 semaphore.signal() } } let thread1 = SemThread(name: "thread1") let thread2 = SemThread(name: "thread2") expectations.append(thread1.expectation) expectations.append(thread2.expectation) // クロス参照する thread1.other = thread2 thread2.other = thread1 // スレッド開始 thread1.run() thread2.run() // 2つのスレッドが完了するまで待つ(が、必ず失敗) wait(for: expectations, timeout: 5.0) }iOSではあまりセマフォを使う機会はないのですが、安易に導入すると、このようなデッドロックを引き起こす可能性があります。セマフォが本当に必要なのか、対象となるリソースは何なのか、相互参照はあるのか、wait() → signal()区間でreturnしていないか、などを十分に吟味して利用することが必要です。
iOSの場合、このようなプリミティブな操作を使うことなく、マルチスレッドを使いこなすために、DispatchQueueが用意されています。
セマフォの説明を延々した後でなんですが、まずDispatchQueueを使うことを検討した方が良いかもしれません。(自戒?)DispatchQueueとマルチスレッド操作については、別途記事を書いてみたいと思います。
- 投稿日:2020-05-31T02:18:49+09:00
[妄想] ExpressibleByMonadType ~Optionalについて~
EvolにあったMake Result expressible by literals when its Success type conforms to expressible by literal protocolsを見ていて思いついたOptionalのサブタイプをなくして代入可能を一般化しようぜって話。というか妄想。
引き続き可能な構文
代入
let a: Int? = 1 let b: Int?? = 1 let c: Int? = someInt()反変
let d: Int? = 1 print(d == 1)
[T?]
のリテラルによる宣言let e: [Int?] = [1, 2]不能となる構文
[T?]
への[T]
の代入let f: [Int] = [1, 2, 3] let g: [Int?] = fただし、このような場合はあまりないと思う。
func h(_: [Int?]) { ... }というAPIを作る意味はまずなく、
[Int]
を引数にすれば良い。[T?]
を必要とする場合もあるが(eg:リバーシ)、そういった場合は常に[T?]
で取り回していることが多く問題にならない気がする。何分検索しづらいのでどの程度使われてるかわからなかった。共変
欲しくはある。が、あまり使われてるのを見ないような?(要検証。検索できない。実装して試した方が早いまである。)
NonEmptyArray
などは不可能となる。class A { func f() -> Int? { ... } } class B: A { func f() -> Int { ... } }バグ修正
Unexpected casting result in Array/Optional.がそもそもできなくなる。
その他
Optional
に関するコンパイラのマジックを減らせる。- ABI的に大丈夫なのかは知らない。ソース互換性はアウト。
Result
はもちろん、プロパティラッパーを普通の型として使うときのlet i = Wrapper(wrappedValue: 2)
をlet j: Wrapper<Int> = 1
とすることなどもできる(いいかは別として)。- 表現可能以上のことができるので
ExpressibleBy
より良い名前はありそう。- 悪用できそう。
- 特段嬉しい点はないような。
実装
protocol Monad { associatedtype Wrapped } protocol ExpressibleByMonadType: Monad { init(wrappedValue: Wrapped) } extension Optional: ExpressibleByMonadType { typealias Wrapped = Wrapped init(wrappedValue value: Wrapped) { self = .some(value) } }とコンパイラ。言うは易し行うは難し。