20200531のSwiftに関する記事は13件です。

Swift StoryboardでScroll ViewとStack Viewを組み合わせて使う

はじめに

StoryboardにてScroll ViewとStack Viewを組み合わせて、
Stack Viewに応じてスクロールする画面を作成する。

環境

  • Xcode 11.5
  • Swift 5

実施手順

1. Scroll Viewを配置する

ScrollAndStack1.png

2. Scroll Viewに制約を追加する

ScrollAndStack2.png

3. Stack Viewを追加する

ScrollAndStack3.png

4. Stack Viewに制約を追加する①

ScrollAndStack4.png

5. Stack Viewに制約を追加する②

ScrollAndStack5.png

6. Stack Viewにアイテムを追加する

ScrollAndStack6.png

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

Swift StoryboradでStack Viewを使う

はじめに

StoryboardにてStack Viewの使い方備忘

環境

  • Xcode 11.5
  • Swift 5

実施手順

1. Stack Viewを配置する

1.png

2. Stack Viewに制約を追加する

2.png

3. Stack Viewにアイテムを追加する

3.png

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

[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へ転送するような手順を踏まなければ、計算結果を反映下処理ができませんでした。

ROG_TextureCompositing.png

ROG.png
*Apple開発者ページより (リンク) より.

このような手順を踏むと、上図のように余計な待ち時間が発生することは素より、この実装におけるより大きな問題はCPU-GPU間のデータ転送に小さくないコストが掛かる事にあります。(下図)

ROG_DataTran.png

*Apple開発者ページより (同上ページ)

CPUへの無駄なデータ転送を抑制する

そこでこのような計算を実施するための自然な方法として、GPU内部のメモリに一時的に中間状態のbufferを保持しておき、CPU-GPU間のデータ転送に係るオーバーヘッドを極力減らす手法 "Raster Order Group" がMetal2で公開されました。

ROG_ROG.png

ROG_DataTran_ROG.png
*Apple開発者ページ (同上ページ)

これと同様の機能は、例えば Unity (HLSL) では GrabPass{} が相当します. 一般的にはマルチパスレンダリングと呼ばれる機能になります.

Raster Order Groupの処理の流れ

基本的に下図のように、MTLTextureオブジェクトを用意してループ計算結果をRead/Writeしながら描画を進めることになります.

Texture_Read_Write.png

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時点で) できません.

tilememory.png
Apple開発者ページより

これはraster_order_groupループ途中のtextureはGPUの "Tile Memory" という仕組みを活用する一時メモリ領域に格納されるからです. Tile Memoryは、画素を一定サイズのブロックに分けてGPU描画を効率化する仕組みであり、どのメーカーのGPUでもよく使われる類の工夫のようです.

各タイルの描画は非同期であり、早く計算が完了したら随時次のタイルの計算に移行します. すなわちraster_order_groupのループ計算はタイル単位で非同期であることに気を付けねばなりません. この非同期計算を所謂awaitするような機能は、筆者がざっと探したところありませんでした.

(ただしMetalにはNeural Networkを効率化する機能が多数実装されており、kernel計算をループで同期的に処理する手段も用意されているはずです. これについてはいずれ調べてみたいと思います.)

おわりに

いかがでしたでしょうか?

ご参考になれば幸いです!
改善方法やご意見などあれば、どしどしコメント下さい!

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

【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()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

多段階選抜 (Swift)

多段階選抜 解答日 シリーズ:yieldの練習/ジェネレータを入れ子に/整数平方根・立方根の実装
問題   http://nabetani.sakura.ne.jp/hena/ord24eliseq/
https://qiita.com/Nabetani/items/1c83005a854d2c6cbb69
Ruby 2014/8/2(当日) https://qiita.com/cielavenir/items/9f15e29b73ecf98968a5
C#/Python 2014/8/4 https://qiita.com/cielavenir/items/a1156e6a4f71ddbe5dcb
  ここから上はdrop_prev_square/drop_prev_cubicをまとめる前の答案
Go/C#/Ruby/Python 2014/8/5 https://qiita.com/cielavenir/items/2a685d3080862f2c2c47
PHP/JavaScript 2014/9/9 https://qiita.com/cielavenir/items/28d613ac3823afbf8407
VB 2014/9/10 https://qiita.com/cielavenir/items/cb7266abd30eadd71c04
D 2015/12/21 https://qiita.com/cielavenir/items/47c9e50ee60bef2847ec
Perl 2017/3/10 https://qiita.com/cielavenir/items/6dfbff749d833c0fd423
Lua 2017/3/13 https://qiita.com/cielavenir/items/c60fe7e8da73487ba062
C++20(TS) 2017/3/15 https://qiita.com/cielavenir/items/e1129ca185008f49cbab (MSVC)
https://qiita.com/cielavenir/items/1cfa90d73d11bb7dc3d4 (clang)
F# 2017/3/17 https://qiita.com/cielavenir/items/a698d6a26824ff53de81
Boo/Nemerle 2017/5/13 https://qiita.com/cielavenir/items/e2a783f0fe4b0fe0ed48
Perl6 2017/5/15 https://qiita.com/cielavenir/items/656ea17fa96c865c4498
Kotlin 2017/5/25 https://qiita.com/cielavenir/items/9c46ce8d9d12e51de285
Crystal 2018/5/8 https://qiita.com/cielavenir/items/1815bfa6a860fd1f90db
MoonScript 2018/6/16 https://qiita.com/cielavenir/items/8b03cce0386f4537b5ad
Julia/Rust 2018/12/20 https://qiita.com/cielavenir/items/3ddf72b06d625da0c4a5
Nim 2018/12/26 https://qiita.com/cielavenir/items/5728944867e609fd52a7
Tcl 2018/12/31 https://qiita.com/cielavenir/items/76cbd9c2022b48c9a2c9
Pascal/Cobra 2019/1/16 https://qiita.com/cielavenir/items/81b81baf8dfc1f877903
Icon 2019/1/17 https://qiita.com/cielavenir/items/889622dcc721f5a4da24
Swift 2020/5/31 https://qiita.com/cielavenir/items/3b0b84a218e35d538f7f
Java/Groovy/Scala 2020/5/31 https://qiita.com/cielavenir/items/7f058203a8fd03b65870
(icbrtの実装に関する)補題 2017/5/11 整数除算であってもn/(x*y)はn/x/yに等しい(ことの証明)
https://qiita.com/cielavenir/items/21a6711afd6be8c18c55

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にちゃんと準拠した書き方を調べるほうが大変だったんですけどね--;;;

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

【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
getset

getPropertyは{get}で指定したので、値が変更されてませんね

最後に

プロトコルは、delegateなどにも使われています。
delegate単体で概念を理解するより、プロトコル自体の理解を深めた方がdelegateだけでなく、他の概念についても理解が深まるなあと思いました!

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

こんなソースコードはイヤだ-必要のないクラスのプロパティは作らない

プログラムのソースコードのより良い書き方をまとめていこうと思います。

必要のないクラスのプロパティは作らない

sample.swift
  class SampleViewController : UIViewController {
    var nextViewController: UIViewContoller!

    func showNext() {
      self.nextViewController = NextViewController()
      self.navigationController?.pushViewController(self.nextViewController , animated: true)
    }
  }

どのようにリファクタリングできるのか

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

Swift: ネットワークリクエスト(URLSession、Alamofire を使って)、‪ファイルのアップロード‬、SSL証明書の検証を行うためのコード

この記事の内容

  • NSURLSessionAlamofire を使用してネットワーク POST もしくは GET リクエストを行う方法について説明します。
  • SSL Pinning (SSL証明書の検証を行うためのコード)

サンプルコードが提供されるので、このページをブックマークし、これらのコードをプロジェクトにすばやく追加できます。

どうやってテストをするのか?

アプリケーションがネットワークリクエストを成功させることができるかどうかをテストするためのデバッグオプションはたくさんあります。プロキシ、またはWebフックを使用できます。ここでは、無料のオンラインWebフックツール https://webhook.site/ を使用しました。

サイトにアクセスすると、Webフックリンクが表示されます。そのURLへのリクエストを行うことができ、そしてウェブサイトのインターフェイス上でリクエスト内容を見ることができます。

Alamofire

Alamofire はネットワーク用のオープンソースのフレームワークです。

Github Repo
インストールガイド

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 など)にアクセスします。

ロックアイコンをクリックし、ボタンをクリックすると詳細が表示されます。

image

次に、証明書をアイコンからファインダにドラッグします。

image

コード

まず、パブリック認証ファイルをプロジェクトに追加する。この例では、ファイル名は 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()
}

その他の

私は以前に Twitter にコンテンツを投稿するための Swift & NSURLSession プログラムを書く方法について記事を書いています。こちらからお読みいただけます。

https://qiita.com/MaShunzhe/items/d7f4ad1df259354d52ae

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

[Swift]Admobの「ポリシー違反(インタースティシャル広告の導入における禁止事項)」に対応した

背景

初めてGoogle Admobを導入したアプリのリリースから約1ヶ月後、ポリシー違反のメールを受信した。
メール本文内に記載のポリシーセンターへのリンクを参照すると、「ユーザーが予期しないタイミングでの広告表示」のため対応するようにとのこと。
ガイドラインを確認すると、なるほど、確かに記載されている。
ポリシー違反.png

違反内容の確認

ポリシーセンターでは、具体的にどの画面のどの操作のどの動きが違反しているとまでは記載されていない。
案内されるガイドラインを読み進めて違反ケースを一つ一つ確認し、該当するケースを自分で見極める必要がある。

今回のケースはインタースティシャル広告の導入における禁止事項予期しないタイミングでの表示が該当した。
予期しないタイミング.png

対応

ガイドラインで解説されている通り
アクション → 画面遷移 → 広告表示、から、アクション → 広告表示 → 画面遷移
の動きに変更した。

再審査

アプリを修正しリリースを行った後、Google Admobに審査を行ってもらう必要がある。
Admobのポリシーセンター上で変更点を記載し、審査リクエストを送信する。

1週間以内には再審査を行う旨の自動応答メールを受信し、待つことになる。
が、今回は半日と待たずに再審査で問題がなかった旨のメールを受信し、違反ステータスが解除された。

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

[Swift]R.swiftで生成するViewControllerのinitにパラメータを渡す

背景

例によって、個人開発したScoreBoxという麻雀成績管理アプリからのネタ。

iOS13ではStoryboardでもDIができる件についてを拝見し、せっかくなのでR.swiftを利用して実装しました。
リンク先でもメリットを記載されていますが、プロパティのlet化、この恩恵は非常に大きいです。

やったこと

  • R.swiftの導入
  • Storyboard上でViewControllerにStoryboard IDを登録
    IDを指定.png

  • 呼び出し元でViewControllerのインスタンス生成/表示

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)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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)
    }
}

実際の画面

20200531_022339.GIF

その他

今回は横スクロールの同期だけでしたが、縦スクロールも同期可能です。

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

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になることが保証されます。

スレッド/プロセスの状態遷移

スクリーンショット 2020-05-30 15.24.18.png

Running(実行中)状態で、wait()を呼び出して運悪く誰かがリソースを使用していた場合は、Blocked状態になります。その後利用していたスレッドがsignal()を発行してリソースを解放すると、うまくOSスケジューラに拾ってもらえれば、Waiting状態に遷移し、無事Runningに復帰し、リソースにアクセスする権限を得られます。

上記の状態遷移図はiOSのものとは若干異なるのかもしれませんが、おおよそ同じような動きになるはずです。(正式な情報をお持ちでしたらお教えください)

セマフォがない場合のコンテキストスイッチ

アクセス制御がなされていない最初のコードでは、下記のようにスレッドが強制的にWaiting状態にスイッチされる(プリエンプションされてコンテキストスイッチが発生する)ことでtaskが中断しています。この中断の間に別のスレッドで、変数resourceが操作されるのが問題です。

スクリーンショット 2020-05-30 14.57.31.png

繰り返しになりますが、ここで重要なのはセマフォがアクセス制御しているのは、リソースである変数resourceです。当該のリソースが何なのかを意識しないで漠然とセマフォを利用すると痛い目に遭います。要注意です。

セマフォがある場合のコンテキストスイッチ

セマフォを用いた状態遷移は下図のようになります。task1がセマフォを取得するとtask2はwait()を読んだ時点でBlocking状態に遷移します。のちにOSによって実行許可が出た場合は、Waitingを介してRunning状態に遷移し、wait()関数を抜けることになります。

スクリーンショット 2020-05-30 16.47.22.png

セマフォが解放されたタイミングで次にどちらのスレッドが割り当てられるかは、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とマルチスレッド操作については、別途記事を書いてみたいと思います。


  1. このソースコードは分かりやすくするために実行するのに必要ないくつかのコードが省かれています。実際に動作させてみたい方は、こちらをご覧ください。 

  2. https://ja.wikipedia.org/wiki/プロセス#ブロック状態 を参照 

  3. モバイルアプリケーションではそうですが、例えば録画再生機器などは録画制御の優先順位が高く設定されており、ついで再生制御、UIの制御は比較的低く設定されていたりします。また、RTOSでは絶対的に他のスレッドより優先される、つまり絶対に譲らないリアルタイムスケジューリングスレッドも存在します。RTOSの概要はこちら 

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

[妄想] 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)
    }
}

とコンパイラ。言うは易し行うは難し。

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