20200705のSwiftに関する記事は15件です。

[iOS, Swift] シングルトンの使い所について考察してみる

本稿では、Swiftにてオブジェクト指向のデザインパターンの1つである 「シングルトン(Singleton)パターン」 をどう活用するのが良いか、考察してみたいと思います.

はじめに

この記事では以下について紹介します。

  • シングルトンとは?
  • iOSにおけるシングルトンの実装例
  • 画像処理の例で考察

使用環境は iPhoneXs (実機, iOS 13.5), Xcode 11.5 です.

シングルトンとは?

Wikipediaによると、次のように定義されています.

Singleton パターンとは、そのクラスのインスタンスが1つしか生成されないことを保証するデザインパターンのことである。ロケールやルック・アンド・フィールなど、絶対にアプリケーション全体で統一しなければならない仕組みの実装に使用される。

つまりシングルトンは基本的に、アプリケーションのコアとなる情報が無闇に複製されることを制限し、Globalで同一の情報を参照するための機構として用いるもののようです.
このような機構は特に、複数スレッドから同一情報にアクセスする場合などにおいて重宝されそうです.

また以下の記述も見られます.

扱い次第では、グローバル変数のように機能させることもできる。

つまりGlobal空間において複製禁止の状態を保持するために用いることもできると.
Swiftではどのような場面がこれに該当するでしょうか.

Swiftでシングルトンパターンが有効な場面

Swiftではこちらの記事にあるように、クラスや構造体にて static 変数やメソッド (インスタンス化せず使用できる変数およびメソッド) を用いることで、シングルトンパターンを用いずともシングルトンのような振る舞いを得ることができます. 状態の授受を担う機能のみが必要な場合は、この実装で事足ります.

しかしながらこの方法では、授受された情報をクラスや構造体の中で何かしら操作したい場合には問題が生じ得ます. すなわち、クラスや構造体のstatic変数はメンバ変数として用いることはできず、またメンバ変数はstaticメソッドでは使用することができません.

総合すると、以下のような場合がシングルトンパターンを使用したい場面であると言えると思います.

  • Global空間にデータを保持したい.
  • 保持されたデータ (の参照先) の複製を禁止したい.
  • クラスないし構造体にデータを保持し、これを制御するメソッドを設けたい.

私が関わる画像処理の分野では特に後述の通り、ポインタを用い直接的にメモリを制御する場面によく出くわすのですが、このような場合にはシングルトンパターンを活用した方が良いと思える場合があります.

iOSにおけるシングルトンの実装例

Swiftではクラスや構造体の仕様上、static修飾子を活用することで簡単にシングルトンパターンを実現することができます.

class mySingleton {
    private init() { } // 外部からのインスタンス化を禁止するために private 指定.
    public static var singleton = mySingleton() // static変数で、初期化済みの自分自身のインスタンスを保持.
}

static修飾子は変数に用いる場合、厳密には「明示的に解放、ないしクラスの属性である場合にはそのクラスがアンロードされない限り、メモリ割り当て後からアプリケーションの終了までメモリを保持しておく」 ことを示します.
この仕組みを活用することで、常に一定のインスタンスへアクセスすることができる訳ですね.

シングルトンの実際の使用例は次章に示します.

シングルトンの使用例

以下では、Stack Overflowの記事に記載されたコードの問題点について言及し、この問題をシングルトンを用いて解決する方法について解説します.

例: MTLTextureをCGImageに変換する

上記リンクではMTLTextureオブジェクトをCGImageオブジェクトに変換するため、以下のようなMTLTexture クラスの extension コードが示されています.

import Metal
import MetalKit

extension MTLTexture {
  func bytes() -> UnsafeMutablePointer<Void> {
    let width = self.width
    let height   = self.height
    let rowBytes = self.width * 4
    let p = malloc(width * height * 4)

    self.getBytes(p, bytesPerRow: rowBytes, fromRegion: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0)

    return p
  }

  func toImage() -> CGImage? {
    let p = bytes()

    let pColorSpace = CGColorSpaceCreateDeviceRGB()

    let rawBitmapInfo = CGImageAlphaInfo.NoneSkipFirst.rawValue | CGBitmapInfo.ByteOrder32Little.rawValue
    let bitmapInfo:CGBitmapInfo = CGBitmapInfo(rawValue: rawBitmapInfo)

    let textureSize = self.width * self.height * 4
    let rowBytes = self.width * 4
    let provider = CGDataProviderCreateWithData(nil, p, textureSize, nil)
    let cgImageRef = CGImageCreate(self.width, self.height, 8, 32, rowBytes, pColorSpace, bitmapInfo, provider, nil, true, CGColorRenderingIntent.RenderingIntentDefault)!

    return cgImageRef
  }
}

*(Metalについては拙著などをご覧ください)

このコードには大きな欠陥があります.

let p = malloc(width * height * 4) の通り、データ変換のための中間情報としてメモリ領域を確保してますが、mallocで確保されたメモリ領域はC言語と同様に明示的に解放 (dealloc) する必要があります.
このコードではメモリ解放を行っていないため、この関数が呼ばれる度にメモリがスタックされていき、最終的には容量超過でアプリケーションがクラッシュします.

ではコードに dealloc を組み込めば良いだけじゃないか、と思われるかもしれませんが実は今回のコードでは、以下の理由からそう単純には実現できません.

  • cgImageRefオブジェクト (CGImageクラスのインスタンス) はポインタpを参照しています. そしてSwiftでは言語の仕様としてクラスインスタンスは必ず参照渡しになります. したがってこの cgImageRef オブジェクトは return された後も常にポインタpを参照し続けます.

すなわちポインタ p をdeallocすると同時に cgImageRef のデータも消失する事になるため、もしextensionの関数中において単純に dealloc を組み込んだとしても実行エラーが発生してしまいます.
このようなメモリの危険な振る舞いを適切に保守管理できるよう、できるだけミニマルな記述で解決したいところです.

以上を鑑み、アプリケーション全体で用いられる1つ以上の画像データのポインタを一元管理するために複製が制限されたクラスが必要となりそうです. すなわちシングルトンパターンが活用できる場面であることが分かりました.

シングルトンの実装例

上記コードではポインタpをextention内部だけで適切に扱うことが実質不可能である構図がみて取れました. したがって、ポインタpをextensionの外で扱うクラス (シングルトン) を設け、適切なタイミングでポインタを dealloc できるようにする以下のような修正を行ってみました.

final class ImgPtrManager {
    private let num_cached: Int = 8 // キャッシュしておくポインタの数. アプリケーションに応じて数値を設定.
    private var p: Array<UnsafeMutableRawPointer?> = [] // 件のポインタをシングルトンで保持 (キャッシュ) する.

    private init() { }
    public static let singleton = ImgPtrManager() // Swiftのシングルトンパターン.

    // push & shift の要領で古いデータを削除する
    public func manageCachedMem(ptr: UnsafeMutableRawPointer) {
        if p.count >= num_cached {
            do { p.first!!.deallocate() }
            p.removeFirst()
        }
        p.append(ptr)
    }
}


extension MTLTexture {
    public func convTexToCGImg() -> CGImage {
        let ptr = self.bytes()

        ImgPtrManager.singleton.manageCachedMem(ptr: ptr) // シングルトンを用いてメモリを管理する.

        return toImage(p: ptr)!
    }

    // 以下は概ね変更なし (ポインタpの受け渡し方法だけ少し変更)
    fileprivate func bytes() -> UnsafeMutableRawPointer {
        let width = self.width
        let height   = self.height
        let rowBytes = self.width * 4
        let p = malloc(width * height * 4)

        self.getBytes(p!, bytesPerRow: rowBytes, from: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0)

        return p!
    }

    fileprivate func toImage(p: UnsafeMutableRawPointer) -> CGImage? {
        let pColorSpace = CGColorSpaceCreateDeviceRGB()

        let rawBitmapInfo = CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
        let bitmapInfo:CGBitmapInfo = CGBitmapInfo(rawValue: rawBitmapInfo)

        let selftureSize = self.width * self.height * 4
        let rowBytes = self.width * 4
        let releaseMaskImagePixelData: CGDataProviderReleaseDataCallback = { (info: UnsafeMutableRawPointer?, data: UnsafeRawPointer, size: Int) -> () in
            return
        }
        let provider = CGDataProvider(dataInfo: nil, data: p, size: selftureSize, releaseData: releaseMaskImagePixelData)
        let cgImageRef = CGImage(width: self.width, height: self.height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: rowBytes, space: pColorSpace, bitmapInfo: bitmapInfo, provider: provider!, decode: nil, shouldInterpolate: true, intent: CGColorRenderingIntent.defaultIntent)!

        return cgImageRef
    }
}

終わりに

いかがでしたでしょうか? 参考になれば幸いです!
ご意見や改善点などあれば、どしどしコメントください!

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

imageViewをタップできるようにしょう!

profileでイメージ変更をしたい時など使用する、imageViewにtapアクションを持たせる

スクリーンショット 2020-07-05 18.35.08.png

xcodeの右上の「+」から TapGesture Recognizer を選択し、imageViewの上にドロップ

次にInteractionのUser Interacton Enabledにチェックを入れる
(僕はこれを忘れてて少しの間頭を抱えていました。。)

スクリーンショット 2020-07-05 18.32.56.png

以上でimageViewをタップできるようになりました!

ここからカメラやアルバムに飛んで写真を表示させます!

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

imageViewをタップする

profileでイメージ変更をしたい時など使用する、imageViewにtapアクションを持たせる

スクリーンショット 2020-07-05 18.35.08.png

xcodeの右上の「+」から TapGesture Recognizer を選択し、imageViewの上にドロップ

次にInteractionのUser Interacton Enabledにチェックを入れる
(僕はこれを忘れてて少しの間頭を抱えていました。。)

スクリーンショット 2020-07-05 18.32.56.png

以上でimageViewをタップできるようになりました!

ここからカメラやアルバムに飛んで写真を表示させます!

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

UIColorとIntの相互変換

n番煎じですが、Intとの相互変換についての日本語の記事が見当たらなかったため投稿します

使い方

let color = UIColor(hex: 0xff0000) // red
color.hex // 0xff0000
extension UIColor {
    convenience init(hex: Int) {
        let red = (hex & 0xff0000) >> 16
        let green = (hex & 0x00ff00) >> 8
        let blue = hex & 0x0000ff

        self.init(
            red: CGFloat(red) / 255,
            green: CGFloat(green) / 255,
            blue: CGFloat(blue) / 255,
            alpha: 1
        )
    }

    var hex: Int {
        var red = CGFloat(0)
        var green = CGFloat(0)
        var blue = CGFloat(0)
        getRed(&red, green: &green, blue: &blue, alpha: nil)

        return Int(red * 255) << 16
            + Int(green * 255) << 8
            + Int(blue * 255)
    }
}

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

SILを見てたら?(SUPERHERO)が出てきた

最初の方はSILやOpaque Return Typeの説明になっています。?に早くお目にかかりたい場合は適当に飛ばしましょう。


SIL

Swiftをコンパイルする時、まずコンパイラはソースをSILと呼ばれる中間言語に翻訳する。これは最適化に適している言語であり、Swiftに限らず多くの言語はこうした中間言語を持っている。SILは最適化を受けた後、LLVMIRと呼ばれる形になり、LLVMというローレベルコンパイラにてバイナリにされる。LLVMは言語に依存しないコンパイラであり、SwiftがSwiftとしての最適化が行えるのがSILという訳だ。Swift固有の機能がコンパイラ内部でどう処理されるかを確認したい場合はSILを確認すると良い。

Opaque Return Type

Swift 5.1で追加されたopaque return typeという機能がある。opaque typeはジェネリクスの一種で、ジェネリクスの中でも単純なものをより簡単に書けるようにしたものである。

ジェネリクスは総称型と訳されるが、こと関数の返り値におけるジェネリクス(リバースジェネリクスという)は総称型というより実装を抽象化するためのものである。

例えばある型がgetHashという関数を持っていた時、大抵の場合getHashの返す型がIntであるかStringであるかはたまたArray<Float>.Iterator.Element(あまり良い例が思いつかなかった。)であるかは必要な情報ではない。getHashがある型を返し、それがハッシュ値として機能することが分かれば良いのだ。

ここで出てくるのがジェネリクスである。ジェネリクスを用いると「Hashableプロトコルに適合しているなんらかの型」を返す関数を作成できる。例えばこのように。

func getHash() -> <T: Hashable> T { // Intを返す }

この例においてgetHashIntを返すが、getHashを呼び出す側は返ってくる値がIntだとはわからない。Hashableに適合したなんらかの値が返ってくることだけがわかる。

let x = getHash()  // xの型はIntではない。

現在のSwiftではこのような書き方(リバースジェネリクス)は許されていないが、opaque return typeを使うことによって同じ効果を得ることができる(今回は問題ないが、リバースジェネリクスの下位互換であることに注意。)。opaque return typeはSwiftUIで効果的に使えるため(SwiftUIでViewを記述する時、bodyプロパティの型は非常に細かく膨大になものになってしまう。詳細な型情報は必要ないためジェネリクスの出番というわけだ。また、詳しくは説明しないがリバースジェネリクスである必要はない。)、Appleはリバースジェネリクスより一足先にopaque return typeを導入したのだ。opaque return typeはこう記述する。

func getHash() -> some Hashable { ... }

これは「とあるHashableな値を返す」という意味であり、someというキーワードを使用してそれを表す。

SILでのOpaque Return Type

ところでopaque return typeの本当の型はコンパイラだけが知っている。これがSwiftの中間言語であるSILでどのように表現されているか気になるだろう。swiftcコマンドにはSILを見るオプションが備わっている。swiftc ファイル.swift -emit-silだ。先程のgetHashをSILにしてみよう。中身を書かないわけにはいかないので、常に1を返すようにした。一体なんのハッシュだというツッコミは気にしないでおこう。

SIL(まあまあ長いので畳んでみた。)

(わかりやすいようにSwiftのシンタックスハイライトを適用している。)

swiftsil_stage canonical

import Builtin
import Swift
import SwiftShims

func getHash() -> some Hashable


// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  %2 = integer_literal $Builtin.Int32, 0          // user: %3
  %3 = struct $Int32 (%2 : $Builtin.Int32)        // user: %4
  return %3 : $Int32                              // id: %4
} // end sil function 'main'

// getHash()
sil hidden @$s4main7getHashQryF : $@convention(thin) () -> @out @_opaqueReturnTypeOf("$s4main7getHashQryF", 0) ? {
// %0                                             // user: %3
bb0(%0 : $*Int):
  %1 = integer_literal $Builtin.Int64, 1          // user: %2
  %2 = struct $Int (%1 : $Builtin.Int64)          // user: %3
  store %2 to %0 : $*Int                          // id: %3
  %4 = tuple ()                                   // user: %5
  return %4 : $()                                 // id: %5
} // end sil function '$s4main7getHashQryF'

// Int.init(_builtinIntegerLiteral:)
sil public_external [transparent] [serialized] @$sSi22_builtinIntegerLiteralSiBI_tcfC : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int {
// %0                                             // user: %2
bb0(%0 : $Builtin.IntLiteral, %1 : $@thin Int.Type):
  %2 = builtin "s_to_s_checked_trunc_IntLiteral_Int64"(%0 : $Builtin.IntLiteral) : $(Builtin.Int64, Builtin.Int1) // user: %3
  %3 = tuple_extract %2 : $(Builtin.Int64, Builtin.Int1), 0 // user: %4
  %4 = struct $Int (%3 : $Builtin.Int64)          // user: %5
  return %4 : $Int                                // id: %5
} // end sil function '$sSi22_builtinIntegerLiteralSiBI_tcfC'

SILの説明まですると日が暮れてしまうし、何より僕もそんなに知らないので簡単になってしまうが、中程に// getHash()という行があり、ここから10行程度先ほどの関数がSILで書かれている。(ちなみに上にあるmainはこのプログラムのエントリーポイントを表しており、今回は0、つまり成功を返している。下のInt.init(_builtinIntegerLiteral:)1というリテラルをInt型に変換している。)

$s4main7getHashQryFというのはgetHashをマングリングしたもの(一意な表現にした関数名)で、その型が:の後に書いてある。この関数の型は@convention(thin) () -> @out @_opaqueReturnTypeOf("s4main6opaqueQryF", 0) ?だ。今回注目すべきは@_opaqueReturnTypeOfというアノテーション。この中に関数名と0が記述されている。

@_opaqueReturnTypeOf("s4main7getHashQryF", 0)

opaque return typeは各関数では一つの型だが、同じsome Hashableを返す関数でもそれが違う関数であれば同じ型とは言えない。例えば、

func getValue() -> some Equatable { Bool.random() }
let x = getValue()
let y = getValue()
print(x == y)

はコンパイルできるが、

func getValue() -> some Equatable { Bool.random() }
func getAnotherValue() -> some Equatable { Int.random(in: 1...10) }
let x = getValue()
let y = getAnotherValue()
print(x == y)

はコンパイルできない。この例だと当たり前に見えるかもしれないが、

func getValue() -> some Equatable { Bool.random() }
func getAnotherValue() -> some Equatable { Bool.random() }
let x = getValue()
let y = getAnotherValue()
print(x == y)

も出来ない。これはopaque return typeの型は関数ごとに決まっており、なんらかの型で表すことは不可能であることを意味する。よってSILでの表現も@_opaqueReturnTypeOf("s4main7getHashQryF", 0)と関数名を含んだ表現になっているのだろう。

0はTupleの添字を表しているのではないかと思っているが、正確なところはわからない。func f() -> (some Equatable, Int)を試してみたが、コンパイラに怒られた。どうも今のところTupleの中でopaque return typeは使えないようだ。将来的にサポートされるのかも不明である。)

?

しかし?とはなんなのだろうか。Unicode名はSUPERHEROらしい。opaque return type以外のところで見たことはない。そもそも型情報の一部なのかも疑問である。:より後、{より前にあるのでそう判断したが、何もかもがわからない。Swiftレポジトリをきちんと読むしかないのだろうか。

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

SwiftUI で星型のシェイプを作ってみる

はじめに

昔の記事で、UIKit で星型の図形を作ってみたことがあったのですが、勉強ついでに SwiftUI でも試してみました。次のようなビューを作成してみます。

正五角形 正六角形
Simulator Screen Shot - iPhone SE (2nd generation) - 2020-07-04 at 21.46.56.png Simulator Screen Shot - iPhone SE (2nd generation) - 2020-07-04 at 21.47.38.png
正七角形 正八角形
Simulator Screen Shot - iPhone SE (2nd generation) - 2020-07-04 at 21.47.52.png Simulator Screen Shot - iPhone SE (2nd generation) - 2020-07-04 at 21.48.10.png

環境

  • Xcode Version 11.5 (11E608c)

実装方針

星型の図形は、次の方法で作図します。

  1. 星型の多角形の外接円と内接円を作る
  2. 外接正多角形の頂点の数 n に対して、円を 2n 等分の扇形に分割する直線を引く
  3. 直線と外接円の交点、内接円の交点を交互に取得する

あとは、取得した頂点を直線で結ぶだけです。正五角形の場合をアニメーションで表すとこんな感じ。
star_outline.gif

基本的なデータ型を実装する

まず、SwiftUI に依存しないデータ構造を実装します。この準備をすることで、座標の位置計算など、SwiftUI フレームワークに依存しない処理を分離することができます。

PolarCoordinate.swift
import CoreGraphics

/// 極座標
struct PolarCoordinate {
    /// 半径
    var radius: Double
    /// 角度( 弧度法 )
    var angle: Double

    init(radius: Double = 1, angle: Double = 0) {
        self.radius = abs(radius)
        self.angle = angle / (2 * .pi)
    }
}

extension PolarCoordinate {
    /// CoreGraphics の座標系における点
    var cgPoint: CGPoint {
        CGPoint(x: radius * cos(angle), y: radius * sin(angle))
    }
}

extension PolarCoordinate {
    /// 回転
    func rotated(by angle: Double) -> Self {
        var point = self
        point.angle += angle
        return point
    }

    /// 拡大・縮小
    func scaled(by t: Double) -> Self {
        var point = self
        point.radius *= t
        return point
    }
}

PolarCoordinate は極座標系における1点を表す構造体です。半径 radius と角度 angle をプロパティとして所持しています。

最終的には、平面上の点として扱うことになるので、次のように、半径と角度から CGPoint を算出できるようにします。

var cgPoint: CGPoint {
    CGPoint(x: radius * cos(angle), y: radius * sin(angle))
}

星型の図形は多角形なので、極座標の点の集まりと、それを任意の位置に平行移動させるための中心座標の組として表現します。次のような構造体 StarParameters として実装します。

StarParameters.swift
import CoreGraphics

/// 星の頂点を表現した構造体
struct StarParameters {
    /// 中心座標
    private(set) var center: CGPoint = .zero
    /// 極座標系の点群
    private(set) var points: [PolarCoordinate]

    /// CoreGraphics の座標系における点群に変換する
    var cgPoints: [CGPoint] {
        let translatedByCenter = { [center] (point: CGPoint) in
            CGPoint(x: point.x + center.x, y: point.y + center.y)
        }
        return points.lazy.map(\.cgPoint).map(translatedByCenter)
    }
}

extension StarParameters {
    /// 単位円周上で初期化する初期化子
    ///
    /// - Parameters:
    ///   - vertex: 外側の頂点の数
    ///   - smoothness: 外接円と内接円の半径の比率
    init(vertex: UInt = 5, smoothness: Double = 0.5) {
        /// 内側も含めた頂点
        let offests = (1...(2 * vertex))
        /// 中心角
        let rotation = (2 * .pi)/Double(offests.count)
        /// 1つ目の頂点
        let start = PolarCoordinate()

        self.points = offests.reduce(into: [start]) { result, offest in
            var last = result.last!
            last.angle += rotation
            last.radius = (offest % 2 == 1 ? smoothness : 1)
            result += [last]
        }
    }
}

extension StarParameters {
    /// 中心座標を設定します
    func center(x: CGFloat, y: CGFloat) -> Self {
        var parameters = self
        parameters.center = CGPoint(x: x, y: y)
        return parameters
    }

    /// 半径を設定します
    func radius(_ radius: CGFloat) -> Self {
        var parameters = self
        parameters.points = points.map { $0.scaled(by: Double(radius)) }
        return parameters
    }

    /// 回転角を設定します
    func rotated(by angle: CGFloat) -> Self {
        var parameters = self
        parameters.points = points.map { $0.rotated(by: Double(angle)) }
        return parameters
    }
}

cgPoints によって、平面上の点群を取得できるように実装しています。
たとえば、ある矩形領域内の中央に配置した星型の図形の頂点のリストは次のようにして取得することができます。

func starPoints(in rect: CGRect) -> [CGPoint] {
    return StarParameters(vertex: 5, smoothness: 0.5) // 頂点の数, 滑らかさ
        .center(x: rect.midX, y: rect.midY)    // 内接円の中心
        .radius(min(rect.midX, rect.midY))     // 内接円の半径
        .rotated(by: -.pi/2)                   // 回転
        .cgPoints
}

この CGPoint のリスト要素に対して、逐次、直線を引くことによって星型の図形を作図することができます。

ちなみに、これらのデータ構造は CoreGraphics フレームワークにのみ依存した実装なので、SwiftUI だけでなく UIKit でも再利用することができます。UIKit では、UIBezierPath で描画する場合の処理簡易化のために利用すると良いでしょう。

SwiftUI

Shape と Path

図形を SwiftUI のビューとして扱う場合は、Shape プロトコルに適合します。Shape は、与えた矩形に対して Path を返す実装を要求します。

SwiftUI
public protocol Shape : Animatable, View {
    func path(in rect: CGRect) -> Path
}

ここで星型の Path を作成することになります。Path は図形の経路を表現した構造体です。

星型のような多角形は、直線で囲まれた閉じた経路として表現されるので、次の手順で作成します。

move(to:)

まず、図形の開始地点の頂点に移動します。

addLine(to:)

次に、次の頂点に向かって、逐次、線を引いていきます。

closeSubpath()

最後に経路を閉じます。

実装例

実際の実装は次の通りです。

StarShape.swift
import SwiftUI

/// 星型のシェイプ
struct StarShape: Shape {
    /// 頂点の数
    var vertex: UInt = 5
    /// 滑らかさ
    var smoothness: Double = 0.5
    /// 回転角
    var rotation: CGFloat = -.pi/2

    func path(in rect: CGRect) -> Path {
        Path { path in
            let points: [CGPoint] = starPoints(in: rect)
            path.move(to: points.first!)
            points.forEach { point in
                path.addLine(to: point)
            }
            path.closeSubpath()
        }
    }

    /// 星型の頂点のリスト
    ///
    /// - Parameter rect: 星型の外接円が内接する矩形領域
    private func starPoints(in rect: CGRect) -> [CGPoint] {
        return StarParameters(vertex: vertex, smoothness: smoothness)
            .center(x: rect.midX, y: rect.midY)
            .radius(min(rect.midX, rect.midY))
            .rotated(by: rotation)
            .cgPoints
    }
}

Shape は View のサブタイプなので、PreviewProvider でプレビューすることもできます。

#if DEBUG
struct StarPath_Previews: PreviewProvider {
    static var previews: some View {
        StarShape(vertex: 5, smoothness: 0.5)
    }
}
#endif

プレビューによって、次のように表示されます。
Screen Shot 2020-07-04 at 23.11.55.png
プレビューでは、Attribute Inspector でプロパティを変更できて、変更ごとに図形が更新されるので面白いです。
StarShape.mov.gif

内部の塗りつぶし

StarShape を Shape として実装したことで、利用する側で自由に図形内部を塗りつぶすことができます。
塗りつぶしには、Shapefill を使用します。

StarView.swift
import SwiftUI

/// 星型のビュー
struct StarView<Style: ShapeStyle>: View {
    /// 頂点の数
    var vertex: UInt
    /// 滑らかさ
    var smoothness: Double
    /// 塗りつぶし
    var style: Style

    var body: some View {
        StarShape(vertex: vertex, smoothness: smoothness)
            .fill(style)
            .aspectRatio(1, contentMode: .fit)
    }
}

PreviewProviderGroup を使って、まとめて表示確認ができて便利です。

#if DEBUG
struct StarView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            StarView(vertex: 5, smoothness: 0.5, style: Color(red: 0, green: 1, blue: 1))
            StarView(vertex: 6, smoothness: 0.6, style: Color(red: 1, green: 0, blue: 1))
            StarView(vertex: 7, smoothness: 0.7, style: LinearGradient(gradient: .init(colors: [.pink, .orange]),
                startPoint: .init(x: 0.5, y: 0), endPoint: .init(x: 0.5, y: 0.6)))
            StarView(vertex: 8, smoothness: 0.8, style: RadialGradient(gradient: .init(colors: [.purple, .yellow]),
                center: .center, startRadius: 0, endRadius: 75))
        }
        .previewLayout(.fixed(width: 160, height: 160))
    }
}
#endif

こんな感じでプレビューされます。
Screen Shot 2020-07-05 at 13.38.34.png

内部と枠線の塗りつぶし

さて、Shape は内部の塗りつぶしだけでなく stroke を使って、枠線を塗りつぶす事もできます。

StarShape(vertex: vertex, smoothness: smoothness)
    .stroke(style, lineWidth: 4)
    .aspectRatio(1.1, contentMode: .fit)

しかし、fillstroke は同時にはできません。fillstrokeShape を返さずに View を返します。ViewShape と異なり、fillstroke メソッドを持っていないため、これが原因でコンパイルエラーになります。

StarShape(vertex: vertex, smoothness: smoothness)
    .fill(style1)
    .stroke(style2, lineWidth: 4) // コンパイルエラー
    .aspectRatio(1.1, contentMode: .fit)

この場合は、fillstroke で別の View を作成して、それを重ね合わせて表示させると良さそうです。ZStack を使って重ね合わせます。

struct StarView2: View {
    /// 頂点の数
    var vertex: UInt = 5
    /// 滑らかさ
    var smoothness: Double = 0.5
    /// 内部の塗りつぶし
    var fill: LinearGradient
    /// 枠線の塗りつぶし
    var stroke: LinearGradient

    var body: some View {
        let shape = StarShape(vertex: vertex, smoothness: smoothness)
        return ZStack {
            shape.fill(fill)
            shape.stroke(stroke, lineWidth: 4)
        }
        .aspectRatio(1.1, contentMode: .fit)
    }
}

枠線と内部の塗りつぶす LinearGradient を適当に設定してプレビューしてみます。
Screen Shot 2020-07-05 at 14.47.58.png
実装はこんな感じです。

#if DEBUG
struct StarView2_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            StarView2(vertex: 5, smoothness: 0.5, fill: Styles.style1, stroke: Styles.style2)
            StarView2(vertex: 6, smoothness: 0.6, fill: Styles.style2, stroke: Styles.style3)
            StarView2(vertex: 7, smoothness: 0.7, fill: Styles.style3, stroke: Styles.style2)
            StarView2(vertex: 8, smoothness: 0.8, fill: Styles.style4, stroke: Styles.style4)
        }
        .previewLayout(
            PreviewLayout.fixed(width: 160, height: 160)
        )
    }
}
#endif
Styles.swift
import SwiftUI

enum Styles {}

extension Styles {
    static var style1: LinearGradient {
        LinearGradient(
            gradient: Gradient(colors: [
                Color(red: 239/255, green: 120.0/255, blue: 221/255),
                Color(red: 239/255, green: 172.0/255, blue: 120/255)
            ]),
            startPoint: UnitPoint(x: 0.5, y: 0),
            endPoint: UnitPoint(x: 0.5, y: 0.6)
        )
    }

    static var style2: LinearGradient {
        LinearGradient(
            gradient: Gradient(colors: [
                Color(red: 90/255, green: 177/255, blue: 187/255),
                Color(red: 247/255, green: 221/255, blue: 114/255)
            ]),
            startPoint: UnitPoint(x: 0.5, y: 0),
            endPoint: UnitPoint(x: 0.5, y: 0.6)
        )
    }

    static var style3: LinearGradient {
        LinearGradient(
            gradient: Gradient(colors: [
                Color(red: 14/255, green: 107/255, blue: 168/255),
                Color(red: 166/255, green: 225/255, blue: 250/255)
            ]),
            startPoint: UnitPoint(x: 0.5, y: 0),
            endPoint: UnitPoint(x: 0.0, y: 0.7)
        )
    }

    static var style4: LinearGradient {
        LinearGradient(
            gradient: Gradient(colors: [
                Color(red: 155/255, green: 120/255, blue: 116/255),
                Color(red: 211/255, green: 62/255, blue: 67/255)
            ]),
            startPoint: UnitPoint(x: 0.5, y: 0),
            endPoint: UnitPoint(x: 0.5, y: 1)
        )
    }
}

#if DEBUG
struct Styles_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            Styles.style1.previewDisplayName("style1")
            Styles.style2.previewDisplayName("style2")
            Styles.style3.previewDisplayName("style3")
            Styles.style4.previewDisplayName("style4")
        }
        .previewLayout(
            PreviewLayout.fixed(width: 100, height: 100)
        )
    }
}
#endif

感想

プレビューしながら実装の内容が確認できるので、快適に実装できるように感じました。
普段はインターフェースビルダーを使うことが多いのですが、慣れてこれば SwiftUI の方が生産性が高くなりそうです。

UIKit と異なり、Path が構造体であることも印象が良かったです。
参照渡しだと、経路の変更が外部から容易に可能であるのに対して、値渡しならその心配もなくなるので、今回のようなサンプル実装だけでなく、実際に応用する際にハマりが少なくなりそうだと思いました。

iOS 14 からは、WidgetKit の登場によって、SwiftUI の導入が本格化されていくので、引き続き SwiftUI で遊んでみて色々と試していきたいところです。

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

UserDefaultsに配列を追加・リセットする

端末にデータを保存するにはUserDefaultsを使います。その中でもよく使う配列についてまとめます。
追記:端末にデータを保存することはUserDefaultsの本来の意図ではないようです。ご指摘感謝いたします。

データを追加する

データを保存するためにはUserDefaultsからKeyを指定して呼び出す必要があります。
呼び出した後は配列の操作と同じなので、appendinsertを用いて追加しましょう。(insertを使うことで新しいデータが先頭に保存されます。)
呼び出した配列に値を追加した後は、Key(呼び出したものと同じ)を指定してsetします。

var userDefaults = UserDefaults.standard
var passwordArray = self.userDefaults.array(forKey: "password") as? [String] ?? []
passwordArray.insert(self.roomNameTextField.text!, at: 0)
self.userDefaults.set(passwordArra, forKey: "password")

データをリセットする

var userDefaults = UserDefaults.standard
userDefaults.removeObject(forKey: "password")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初回起動時のみ画面遷移する方法

備忘録。ログインしていない状態の時には登録画面に遷移するなどにも応用できます。

application(_:didFinishLaunchingWithOptions:)にUserDefaultsを設定する

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        let userDefaults = UserDefaults.standard
        let firstLunchKey = "firstLunchKey"
        let firstLunch = [firstLunchKey: true]
        userDefaults.register(defaults: firstLunch)
        return true
    }

起動画面のViewDidAppeearにUserDefaultsを設定する

override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        let userDefaults = UserDefaults.standard
        let firstLunchKey = "firstLunchKey"
        if userDefaults.bool(forKey: firstLunchKey) {
        performSegue(withIdentifier: "遷移させたい画面へのsegue.identifier", sender: self)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift]Gameplaykit Programming Guide超要約

GameplayKit Programming Guide

GameplayKit Programming Guide

GameplayKitとはどんなものか

  • ゲームを作る際のアーキテクチャ 設計・構築
  • ゲームを作る上で必要な複雑なアルゴリズムの構築をサポート
  • 他の上位層のゲーム用ライブラリとは独立しており、ゲームを作る際自由に組み合わせて使うことができる。
    • 例えば、Gamekitと、2Dゲームの場合はSpriteKit, 3Dゲームの場合はSceneKit, サードパーティ製のエンジンを使いたい場合はMetal や OpenGLESを組み合わせて使える。グラフィックがさほど要らないゲームならUIKitと組み合わせてもいい。
  • GameplayKitのカバーする領域
    • ランダム化
      • 乱数を提供する。
    • エンティティ-コンポーネントアーキテクチャ
      • ある動作を定義したコンポーネントをオブジェクトに複数つけることで、ゲーム上に多様なオブジェクトを定義するためのアーキテクチャ 。
    • ステートマシン
      • ゲーム内のオブジェクトがどの状態においてどの動きをするか、またどの状態からどの状態へ移行できるかを簡単に記述するための機能。
    • ミニマックス法
      • オセロ・将棋などの完全情報ゲームにおいて、コンピュータ(人工知能)の動きを実装するために用いる。
    • 経路検索
      • ゲーム盤、フィールド上などでオブジェクトを動かす際の経路の検索に関わるライブラリ。
    • Agent,Goal,Behaviorアーキテクチャ
      • 自律的にゲーム上のオブジェクトを動かすために、オブジェクトに行動の目的(Goal)を設定するためのアーキテクチャ 。
    • ルールシステム
      • ゲーム上に設定するルールを規定する。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS]ゲーム開発 初心者の学び

自己紹介

iOSで開発経験が2年程度ありますが、ゲームは全く作ったことがありません。趣味でゲームを作ろうと思っています。前提知識を随時まとめてみます。書いたコードはこちらです Github

アーキテクチャ

  • Entity-Component Architecture

    • ゲームで使うオブジェクト(Entity)に、様々な再利用可能な振る舞いを定義した部品(Component)を持たせる形で作っていく。GameplayKitの中のGKComponentを継承したクラスにより、Componentを作成していく。
    • 自前でこうした仕組みを作ることもできるが、GameplayKitを利用していくことで楽にできる。
  • GKEntity

    • 通常は特にサブクラス化せず、addComponent(component:)メソッドでコンポーネントを追加していく。
    • update(deltatime:): Entity-Componentアーキテクチャ では周期的に行う動作を定義する必要がある。このメソッドを呼ぶと、そのエンティティが持っているコンポーネントのupdate(deltaTime:)メソッドも全て呼ばれる。deltatimeとは、前回アップデートがされてからの秒数を指す。
  • GKComponentSystem

    • ある種類のコンポネント全てに対しupdateを行いたい時に使用できる。例えば、ゲーム内に存在する全オブジェクトに物理演算を行いたい時など。
let graphicsComponentSystem = GKComponentSystem(componentClass: GraphicsComponent.self)
let physicsComponentSystem = GKComponentSystem(componentClass: PhysicsComponent.self)
for entity in entities {
    //entity内に存在するGraphicComponentを取得
    graphicsComponentSystem.addComponent(foundIn: entity)
   //entity内に存在するPhysicsComponentを取得
    physicsComponentSystem.addComponent(foundIn: entity)
}
//全GraphicComponentをupdate
graphicsComponentSystem.update(deltaTime: 0.033)
//全PhysicsCOmponentをupdate
physicsComponentSystem.update(deltaTime: 0.033)
  • ゲーム全体の時間経過の管理
    • どのオブジェクトで時間経過を管理するかを決める。ViewControllerサブクラスか、SKSceneGLKViewController、その他自分で作ったカスタムクラスなど。
    • そのオブジェクトの中に次のような変数を作る。
    • このような時間経過とは、言い換えればゲーム内で直前のフレームからどのくらい時間が経ったかという事を示す。できれば、1秒間に60フレーム(すなわち、経過時間が0.0166秒 = 16ミリ秒程度)になるのが良いが、処理が重い場合は達成できないこともある。
TimeKeeper.swift
public class TimeKeeper {
    public var lastFrameTime: Double = 0.0

    public func update(currentTime: Double) {

        // このメソッドが最後に呼ばれた時からどのくらい時間が経ったか計算
        let deltaTime = currentTime - lastFrameTime

        // 1秒で3単位移動する
        let movementSpeed = 3.0

        // 速度と時間を掛け算し、このフレームでどのくらい移動するか決める
        someMovingObject.move(distance: movementSpeed * deltaTime)

        // lastFrameTime に currentTime を代入し、次回このメソッドが呼ばれた際、
        // 経過時間を計算できるようにしておく
        lastFrameTime = currentTime
    }
}
  • アプリがアクティブ・非アクティブになるタイミングを知る
    • ゲームがポーズできるようにアプリが非アクティブになるタイミングが知りたい時や、逆にアクティブになったタイミングが知りたい場合がある。NotificationCenterを用いてそのタイミングを知ることができる。
    • ターン制のゲームの場合、リアルタイム性がないので、ポーズなどはあまり気にしなくて良くなる。
    • アプリがバックグラウンドになった時には、あまりメモリを消費するような動きはせず最小限の動きのみにすること。消費が激しい場合、Appleからリジェクトされる場合がある。
GameViewController.swift
private extension GameViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        setObserver()
     }

    func setObserver() {

        let center = NotificationCenter.default

        let didBecomeActive = #selector(
            GameViewController.applicationDidBecomeActive(notification:)
        )

        let willEnterForeground = #selector(
            GameViewController.applicationWillEnterForeground(notification:)
        )

        let willResignActive = #selector(
            GameViewController.applicationWillResignActive(notification:)
        )

        let didEnterBackground = #selector(
            GameViewController.applciationDidEnterBackground(notification:)
        )

        center.addObserver(self,
                           selector: didBecomeActive,
                           name: UIApplication.didBecomeActiveNotification,
                           object: nil)

        center.addObserver(self,
                           selector: willEnterForeground,
                           name: UIApplication.willEnterForegroundNotification,
                           object: nil)

        center.addObserver(self,
                           selector: willResignActive,
                           name: UIApplication.willResignActiveNotification,
                           object: nil)

        center.addObserver(self,
                           selector: didEnterBackground,
                           name: UIApplication.didEnterBackgroundNotification,
                           object: nil)
    }

    @objc
    func applicationDidBecomeActive(notification: Notification) {
        print("アプリがアクティブになった")
    }

    @objc
    func applciationDidEnterBackground(notification: Notification) {
        print("アプリがバックグラウンドに入った (テクスチャのアンロードなどを行う)")
    }

    @objc
    func applicationWillEnterForeground(notification: Notification) {
        print("アプリがアクティブになる (アンロードしたテクスチャの再読み込みなどを行う)")
    }

    @objc
    func applicationWillResignActive(notification: Notification) {
        print("アプリが非アクティブになる (ゲームのポーズなどを行う)")
    }
}

なお上に列挙した関数に限っていうと

  1. アプリ起動時にはapplicationDidBecomeActive
  2. ホームボタンなどを押してアプリがバックグラウンドに行った時にはapplicationWillResignActiveapplciationDidEnterBackground
  3. アプリアイコンを押すなどしてアプリが前面に戻った時にはapplicationWillEnterForegroundapplicationDidBecomeActive

が呼ばれる。

このようなアプリのライフサイクルの詳細についてはこちらこちら(iOS13以降)など参照。

  • Timerクラスによるゲームの更新
    • 特定の秒数が経過した後(あるいは、経過する毎)に処理をしたいということがある。その場合、Timerクラスを使うと良い。
GameViewController.swift
    var timer: Timer? = nil 

    override func viewDidLoad() {
        super.viewDidLoad()

        // タイマーの設置
        timer = Timer.scheduledTimer(
            timeInterval: 0.5, // 0.5秒後に発火
            target: self,
            selector: #selector(updateWithTimer(timer:)),
            userInfo: nil,
            repeats: true // くり返しON
        )
    }

    @objc
    func updateWithTimer(timer: Timer) {
        //タイマー発火時に呼ばれるメソッド。ゲームの更新などを行う。

        print("Timer Went Off!")
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        // タイマーを止める
        timer?.invalidate()
        timer = nil
    }

なお、DispatchQueueを使い以下のように書くこともできる。

func placeBomb() { print("bomb placed.") }
func explodeBomb() { print("bomb!") } //"bomb placed"の10秒後に"bomb!"と表示したい。

let deadline = DispatchTime.now() + 10

placeBomb()

DispatchQueue.main.asyncAfter(deadline: deadline, execute: {
    // Time's up
    explodeBomb()
})
  • ゲームのポーズ

    • ゲームをポーズした際、一部のオブジェクトは動作を続け、一部のオブジェクトは停止するということが多い。例えば、ネットワークを介して通信する部分や、ユーザインターフェースを担当する部分はポーズで止めないのが普通だと考えられる。
    • その場合、Bool値でゲームのポーズを管理するとともに、ゲームオブジェクトをポーズし得るものとし得ないものの2タイプに分ければ良い。
 for gameObject in gameObjects {

    if paused == false || gameObject.canPause == false {
        gameObject.update(deltaTime: deltaTime)
    }
 }
  • ゲーム開始からの経過時間
    • ゲーム開始時刻、現在時刻の二つをDateクラスで持つ。その二つの間の経過時間を、timeIntervalSinceメソッドで計算すればよい。
GameViewController.swift
private extension GameViewController {

    /// ゲームの開始時刻
    var gameStartDate: Date?

    override func viewDidLoad() {
        super.viewDidLoad()

        gameStartDate = Date()
    }

    /// ゲーム開始からの経過時間を取得する
    func getCurrentElapsedTime() {
        let now = Date()

        guard  let date = gameStartDate else { return }

        let timeSinceGameStart = now.timeIntervalSince(date)

        NSLog("The Game Started \(timeSinceGameStart) seconds ago.")

        //経過時間(秒数)を、何時間・何分・何秒という形に整形する
        let formatter = DateComponentsFormatter()
        formatter.allowedUnits = [.hour, .minute, .second]
        formatter.unitsStyle = .positional

        let formattedString = formatter.string(from: timeSinceGameStart)
        print("Time elapsed: \(formattedString ?? "")")
    }
}
  • 依存性(ある処理が終わってから別の処理をやる)

いろいろやり方はあると思うが、OperationクラスのaddDependency(_:)メソッドが利用できる。

let firstOperation = BlockOperation { () -> Void in
    print("First Operation")
}

let secondOperation = BlockOperation { () -> Void in
    print("Second Operation")
}

let thirdOperation = BlockOperation { () -> Void in
    print("Third Operation")
}

//2番目の`Operation`は、1番目と3番目が終わった後のみやるようにする。
secondOperation.addDependency(firstOperation)
secondOperation.addDependency(thirdOperation)

let operations = [firstOperation, secondOperation, thirdOperation]

//バックグラウンド にて実行開始
let backgroundQueue = OperationQueue()
backgroundQueue.qualityOfService = .background
backgroundQueue.addOperations(operations, waitUntilFinished: true)

こうすると、下記のように依存性を設定した2番目のOperationが最後に実行される。

Third Operation
First Operation
Second Operation
  • 画像などのアセットをゲーム実行中に読み込みたい
    • 先のOperationクラスで、バックグラウンドで実行するよう指定すれば良い。
    • 画像の全読み込みが終わってからやりたい処理(画面を更新するなど)があれば、依存性を設定すればよい。
let imagesToLoad = ["image1.jpg", "image2.jpg", "image3.jpg"]

let imageLoadingQueue = OperationQueue()

// バックグランドで実行するよう設定する
imageLoadingQueue.qualityOfService = .background

// 複数の画像の同時読み込みを許可する(10個まで)
imageLoadingQueue.maxConcurrentOperationCount = 10

// 全画像読み込み完了後にやりたい処理
let loadingComplete = BlockOperation { () -> Void in
    print("loading complete!")
}

var loadingOperations: [Operation] = []

for imageName in imagesToLoad {
    let loadOperation = BlockOperation { () -> Void in
        print("Loading \(imageName)")
    }

    loadingOperations.append(loadOperation)

    // 最後にやりたい処理に対して、依存性を設定していく。
    loadingComplete.addDependency(loadOperation)
}

imageLoadingQueue.addOperations(loadingOperations, waitUntilFinished: false)
imageLoadingQueue.addOperation(loadingComplete)

表示関係

SpriteKit

2Dグラフィックでのアニメーションを行うための公式ライブラリ。

SKView

UIViewのサブクラス。

  • presentScene(_:):シーン(SKSceneクラスオブジェクト)を呼び出すメソッド。シーンがビュー上に呼び出されると、シミュレーションしているだけの状態から実際に画面上にそれを表示させる。
  • isPaused: このプロパティをtrueにすると、SKView上のシーンが一時停止する。

参考

  • "iOS Swift Game Development Cookbook: Simple Solutions for Game Development Problems" 3rd Edition, Jonathon Manning & Paris Buttfield-Addison, O'Reilly Media, 2018
  • GameplayKit Programming Guide
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】ズボラな通知音変更

こんにちは。 いるべさんです。

Firebaseを使ってユーザ間の通知を行う実装をしていたとき、馴染みのある「ティロリン♪」ってやつしか使い方分からなくて困っていた。

通知音をインポートして再生するやり方はよく見かけた。
が、めんどくさがり屋ないるべさんはそんなことしたく無い!!!

今回はAudioToolbox使って通知音とする様にした。
ただ、こんなことしなくてもちゃんとしたやり方がありそう。

構成仕様

構成としては、ユーザがCloud FireStoreにデータを突っ込んだらそれをClound Functionsが検知して購読しているユーザに通知を送るよくある普通の実装。

通知を受け取ったら通知情報からsoundの項目を引っ張ってきてAudioToolboxに再生してもらう簡単仕様。

なので、通知を送るFunctionsのpayloadのsoundの項目にSoundSystemIdの値、もしくは通知ごと、ユーザごとに通知音を変えたいならその識別子を入れておく。

実装

Functions

まずはCloud Functionsの関数を作成する。大事そうなところだけピックアップして記載しておく。
今回の話の肝になるのはpayloadのsound。
ここにAudioToolboxに通用するSystemSoundIdを入れておく。

index.js
exports.sendNotifications = functions.firestore.document('/notifications/{userId}/messages/{messageId}').onCreate((snapshot, context) => {
~~ 省略 ~~

var payload = {                                       
    notification: {
        title: data.username,
        body: data.detail,                                                  
        badge: "1",                                                         
        sound: "1020"                                                       
    }                                                                     
};

~~ 省略 ~~
return admin.messaging().sendToTopic(topic, payload, options)
    .then(function(response) {
        return console.log(response);         
    })
    .catch(function(error) {  
        return console.log(error); 
    }); 

通知を受け取る側

まぁ何も難しいことはしてなくて、通知から必要な情報取り出してAudioToolboxに突っ込んでいるだけ。
普通にやる場合はcompletionHandlerに.soundを入れる形なのでそのやり方で今回と同じ様なことができたらなぁと思う。ご存知の方、ぜひ教えてください!

SceneDelegate.swift
func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let userInfo = notification.request.content.userInfo

        if let aps = userInfo["aps"] as? [String: Any] {
            let soundId: SystemSoundID = SystemSoundID(aps["sound"] as! String) ?? 1007
            AudioServicesPlaySystemSound(soundId)
        }

        completionHandler([.alert, .badge])
    }

これでユーザが通知を受け取ったとき、今回で言えば設定したid:1020が鳴るというわけです。

やり方が正しいかは分からないけど思い通りの実装ができたので今のところはヨシ!!!

お読み頂き、ありがとうございました。

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

Realm マイグレーションについて

マイグレーションとは

Realmに保存しているモデルに変更があった際には、Migration is required due to the following errorsなどのエラーが発生する。

モデルに変更があると、コード上で変更されたモデルと、すでにディスクにある変更前のモデルで整合性が取れなくなるためである。

下記ページのようにマイグレーションを行うことになる。

Realm doc - Migrations

他方、開発段階ではモデルの変更があるのはよくあることなので、一々マイグレーションを書くのではなく、ディスク上のデータを一回削除してから、コード上の新しいモデルを試せばよいだろう。

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

Realm Swiftに関してのポイントまとめ

ある種類のオブジェクトを全て削除したい

// Delete all objects from the realm
try! realm.write {
    realm.deleteAll()
}

・・・これだと、Realm上にある全データを削除してしまう。

特定の種類を全削除するには、まずのその種類のオブジェクトを全て読み込んでから、これをdelete()メソッドに渡せば良い。

let realm = try! Realm()
let objects = realm.objects(HogeHoge.self)

try! realm.write {
    realm.delete(objects)
}

参考: Can we delete all objects of particular type #4769

マイグレーション

Realmに保存しているモデルに変更があった際には、Migration is required due to the following errorsなどのエラーが発生する。

モデルに変更があると、コード上で変更されたモデルと、すでにディスクにある変更前のモデルで整合性が取れなくなるためである。

下記ページのようにマイグレーションを行うことになる。

Realm doc - Migrations

他方、開発段階ではモデルの変更があるのはよくあることなので、一々マイグレーションを書くのではなく、ディスク上のデータを一回削除してから、コード上の新しいモデルを試せばよいだろう。

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

Swift PKCanvasView、データ取得/読込

データ取得

@IBOutlet weak var pKCanvasView: PKCanvasView!
let data: Data = self.pKCanvasView.drawing.dataRepresentation()

Data型を取得出来ます。
取得後、UserDefaults、CoreData等に保存出来ます。

データ反映

do {
    self.pKCanvasView.drawing = try PKDrawing(data: page)
}
catch {
    let nserror = error as NSError
    fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}

保存したData型を使用してPKDrawingを生成、設定します。

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

Composable Architecture を利用した Todo アプリの紹介(Part 1)

はじめに

こちらの記事を拝見して、SwiftUI なら Composable Architecture で書くのが良さそうと思い、SwiftUI・Composable Architecture の勉強がてら書くことにしました。
基本的には Composable Architecture の作者さんが公開されている A Tour of the Composable Architecture の Part 1 - 4 を実際に追ってみたので、それをまとめようかと思います。A Tour of the Composable Architecture は作者である POINT-FREE さんの100個目の動画にあたるらしく、それより前の動画で Composable Architecture に関わる内容についても言及されているようです。(まだ見ることができていませんが...)

Composable Architecture とは?

Composable Architecture 自体の説明については、 ↑の yimajo さんの記事にまとまっているので、本記事では省略しようと思います。触ってみた感想としては、Redux のような感じで、SwiftUI とも相性が良さそうで、実際に使ってアプリを作ってみたいという印象でした。また、Composable Architecture が提供してくれるテストをサポートする機能によって、テストも非常に楽に書けそうで、今後このライブラリを利用して実践的なアプリも作ってみたいと思えました。

Composable Architecture を利用した Todo アプリ (Part 1)

一気に Part 1 - Part 4 の内容を書こうとすると挫けそうなので、一旦 Part 1をかいつまんで行こうと思います。

まずは、Xcode で SwiftUI プロジェクトを作成し、SPM で ComposableArchitecture を導入しましょう。SPM による導入方法などは省略します。

作成できたら、ContentView.Swiftに、空ですが以下の三つを書いていきます。それぞれ Composable Architecture で必要になってくるものです。上から順にざっくりと説明すると、 Stateは名前の通りアプリで扱うことになる状態で、基本的には構造体で定義すると思いますが、必ずしも構造体である必要はないと作者は言っています。Actionはボタンのタップ、テキストフィールドへのテキスト入力など、ユーザが実行するアクションを定義する場所になっています。こちらは基本的には列挙型を使います。最後にEnvironmentは API Client、UUID、Scheduler など宣言する度に値が変わったり、外部から値を差し込んだほうが好都合なもの(テスト時などのことを考えた場合など)を定義する場所になっています。こちらは基本的には構造体を使います。

ContentView.swift
struct AppState {
}

enum AppAction {
}

struct AppEnvironment {
}

次に StateActionEnvironment をまとめて使用し、アプリケーションのビジネスロジックを担当することになる Reducer を定義します。基本的には、Actionごとにロジックを分岐していくので、switchを使用してactionごとの処理を書いていくことになります。

ContentView.swift
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
  switch action {
  }
}

もう一つ、Composable Architecture にはEffectという重要な要素があります。Effectは、API リクエストの実行、ディスクへのデータ書き込みなど、外部との通信を行ったり...という部分になり、Reducerの中でEffectを返却することができますが、今回はEffectを使用しないので、このくらいの説明に留めることにしようと思います。

Reducerによってビジネスロジックを表現することができますが、Reducerで変更したStateをViewから利用するためには、Storeを利用します。一旦、Storeを持った簡単なViewを定義することにしましょう。

ContentView.swift
struct ContentView: View {
  let store: Store<AppState, AppAction>

  var body: some View {
    NavigationView {
      List {
        Text("Hello")
      }
      .navigationBarTitle("Todos")
    }
  }
}

Storeを初期化できる部分は、(プロジェクト作成時の)SwiftUIではSceneDelegate.swiftContentView.swift内のContentView_Previewsになるので、両方からStoreの初期化を行います。初期化には先ほど定義したappReducerAppState()AppEnvironment()を使用します。

SceneDelegate.swift
let contentView = ContentView(
  store: Store(
    initialState: AppState(),
    reducer: appReducer,
    environment: AppEnvironment()
  )
)
ContentView.swift
struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView(
      store: Store(
        initialState: AppState(),
        reducer: appReducer,
        environment: AppEnvironment()
      )
    )
  }
}

基本的な部分は整ってきたので、空で定義してしまっていた部分などを徐々に埋めていきます。Todo アプリを作るので、まずは Todo のモデルを定義します。単純な Todo の文章と、実行済みか否かのフラグを持つだけのモデルです。

ContentView.swift
struct Todo {
  var description = ""
  var isComplete = false
}

モデルを定義したので、当然Stateはこの Todo を保持することになります。追加していきましょう。

ContentView.swift
struct AppState {
  var todos: [Todo] = []
}

ここまで出来上がったので、まだ todos の中身は空っぽですが、View から Storeにアクセスして、todo を表示するようなプログラムを書いてみます。todos は空っぽなので、Helloというテキストを表示するようにしています。

ContentView.swift
struct ContentView: View {
  let store: Store<AppState, AppAction>

  var body: some View {
    NavigationView {
      List {
        ForEach(self.store.state.todos) { todo in
          Text("Hello")
      }
      Text("Hello")
     }
     .navigationBarTitle("Todos")
    }
  }
}

これで上手くいくかなと思いきや、StoreからStateに直接アクセスすることは禁止されているので、↑のようなことはできません。アクセスするためには、ViewStoreというものを使用することになります。
ViewStoreを使用するメリットについては、公式の Part 1 の記事に詳しく載っていますが、簡単に説明するとViewの再計算のコストを抑え、パフォーマンス的にも向上し、iOS/macOS などの異なるプラットフォームで共通のロジックを使うことができるらしいです。
実際にViewStoreを用いたコードを書く前に、事前に定義していたTodoAppStateEquatableプロトコルに適合させます。理由としては、ViewStoreStateを排出する際に重複を自動的に取り除けるようにするためです。(ViewStoreコードを見てみると、Stateの重複を取り除くための仕組みが提供されていることがわかります。)

ContentView.swift
struct Todo: Equatable {
  var description = ""
  var isComplete = false
}

struct AppState: Equatable {
  var todos: [Todo]
}

var body: some View {
  NavigationView {
    WithViewStore(self.store) { viewStore in
      List {
        ForEach(viewStore.state.todos) { todo in
          Text("Hello")
        }
        Text("Hello")
      }
      .navigationBarTitle("Todos")
    }
  }
}

しれっと、ついでにViewStoreを用いたコードも書いてしまいました。このコードの中では、viewStore.state.todosという形でStateにアクセスしていますが、これは Swift の Dynamic Member Lookup を利用していて、あたかもViewStore上に、直接Stateプロパティが存在するかのようにアクセスすることができるようになっています。

しかし、まだアプリを動かすことはできないので、少し手を加えていきます。
ForEachメソッドで各 Todo の要素を一意に識別することができるように Todo に id を持たせて以下のように変更していきます。ForEachについては本質ではないので、細かい説明は省くことにします。

ContentView.swift
struct Todo: Equatable, Identifiable {
  let id: UUID
  var isComplete = false
  var description = ""
}

上記のように定義することによって、ForEachから各 Todo を適切に扱うことができるようになります。アプリを動かすために、Storeの初期化時にいくつかの todo を渡すように変更します。以下のように変更することで、一旦アプリは動くようになります。

ContentView.swift
struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView(
      store: Store(
        initialState: AppState(
          todos: [
            Todo(
              description: "Milk",
              id: UUID(),
              isComplete: false
            ),
            Todo(
              description: "Eggs",
              id: UUID(),
              isComplete: false
            ),
            Todo(
              description: "Hand Soap",
              id: UUID(),
              isComplete: false
            ),
          ]
        ),
        reducer: appReducer,
        environment: AppEnvironment()
      )
    )
  }
}

徐々にアプリに機能を追加していきます。
既に View から 各 todo にアクセスすることができるようになっているので、簡単なチェックボックスと TextField を追加します。todo の isCompleteによってチェックが付くチェックボックスと todo のdescriptionを表示する TextFiled になっています。

ContentView.swift
var body: some View {
  NavigationView {
    WithViewStore(self.store) { viewStore in
      List {
        ForEach(viewStore.state.todos) { todo in
          HStack {
            Button(action: {}) {
              Image(systemName: todo.isComplete ? "checkmark.square" : "square")
            }
            .buttonStyle(PlainButtonStyle())
            .foregroundColor(todo.isComplete ? .gray : nil)

            TextField(
              "Untitled todo",
              text: .constant(todo.description)
            )
          }
        }
        Text("Hello")
      }
      .navigationBarTitle("Todos")
    }
  }
}

見た目はできてきたので、Actionを定義していきます。具体的にはチェックボックスのタップイベントと TextField を編集できるようなActionを定義します。

ContentView.swift
enum AppAction {
  case todoCheckboxTapped
  case todoTextFieldChanged(String)
}

ただ、↑のように定義してしまうと、todos の中のどの todo に対するアクションなのかがはっきりしないので、少し手を加えます。

ContentView.swift
enum AppAction {
  case todoCheckboxTapped(index: Int)
  case todoTextFieldChanged(index: Int, text: String)
}

後は、Reducer で具体的なビジネスロジックを実装していくことになります。イメージだけ先に示すと↓のような感じです。

ContentView.swift
let appReducer = Reducer<AppState, AppAction, Void> { state, action, _ in
  switch action {
  case .todoCheckboxTapped(index: let index):
  // ここに State を変更するための具体的なロジックを実装していく
  case .todoTextFieldChanged(index: let index, text: let text):
  // ここに State を変更するための具体的なロジックを実装していく
  }
}

.todoCheckboxTapped(index: )の中では、state.todos[index].isComplete.toggle()のように単純にisCompleteを変更するだけで良さそうです。.todoTextFieldChanged(index:, text: )の方でも単純にtextdescriptionに入れてあげるだけになります。それらを踏まえた実装は以下のようになります。

ContentView.swift
let appReducer = Reducer<AppState, AppAction, Void> { state, action, _ in
  switch action {
  case .todoCheckboxTapped(index: let index):
    state.todos[index].isComplete.toggle()
    return .none
  case .todoTextFieldChanged(index: let index, text: let text):
    state.todos[index].description = text
    return .none
  }
}

勝手に謎のreturn .noneという記述を追加してしまいました。これは前の方でほとんど説明しなかったのですが、ReducerEffectを返すことができるので、Effectを何も返さない場合は明示的にreturn .noneとする必要があります。
一旦これで基本的なビジネスロジックは完成しました。

後はビジネスロジックを View から利用するだけになります。
View から State を変更する方法は、ViewStoreを介してActionを送ってあげれば良いです。
チェックボックスをタップした時には.todoCheckboxTappedアクションを送れば良いので、Button(action: { viewStore.send(.todoCheckboxTapped(index: index)) })のように書くことができます。

TextField の方は若干特殊で、TextField にテキストが入力された場合、.todoTextFieldChangedアクションを送るのと同時に、TextField には todo の中にあるdescriptionを表示する必要があります。
ViewStoreにはこのような場合のためにヘルパーメソッドが用意されているので、以下のように簡単に実現することができます。

ContentView.swift
TextField(
  "Untitled Todo",
  text: viewStore.binding(
    get: { $0.todos[index].description },
    send: { .todoTextFieldChanged(index: index, text: $0) }
  )
)

以上を実装した全体像は以下のようになります。

ContentView.swift
var body: some View {
  NavigationView {
    WithViewStore(self.store) { viewStore in
      List {
        ForEach(viewStore.state.todos) { todo in
          HStack {
            Button(action: { viewStore.send(.todoCheckboxTapped(index: index)) }) {
              Image(systemName: todo.isComplete ? "checkmark.square" : "square")
            }
            .buttonStyle(PlainButtonStyle())
            .foregroundColor(todo.isComplete ? .gray : nil)

            TextField(
              "Untitled todo",
              text: viewStore.binding(
                get: { $0.todos[index].description },
                send: { .todoTextFieldChanged(index: index, text: $0) }
              )
            )
          }
        }
        Text("Hello")
      }
      .navigationBarTitle("Todos")
    }
  }
}

これで基本的な実装は一通り終わりです。
ただ、本当にStateが更新されているかを確かめるための手段も Composable Architecture には備わっていて、それも記事の最後で紹介されているので、説明します。

方法は簡単で、Reducer debug() メソッドを使用するだけで良いです。例えば、

SceneDelegate.swift
let contentView = ContentView(
  store: Store(
    initialState: AppState(
      todos: [
        Todo(id: UUID()),
        Todo(id: UUID()),
      ]
    ),
    reducer: appReducer.debug(),
    environment: ()
  )
)

こんな感じで、debug()を付ければ実現できますし、以下のように特定のReducerだけに付けることもできます。

ContentView.swift
let appReducer = Reducer<AppState, AppAction, Void> { state, action, _ in
  ...
}
.debug()

実際にdebug()を付けたまま実行してチェックボックスをタップすると、Debug Console に以下のような表示がされます。

received action:
  AppAction.todoCheckboxTapped(
    index: 0
  )
  AppState(
    todos: [
      Todo(
−       isComplete: false,
+       isComplete: true,
        description: "Milk",
        id: 5834811A-83B4-4E5E-BCD3-8A38F6BDCA90
      ),
      Todo(
        isComplete: false,
        description: "Eggs",
        id: AB3C7921-8262-4412-AA93-9DC5575C1107
      ),
      Todo(
        isComplete: true,
        description: "Hand Soap",
        id: 06E94D88-D726-42EF-BA8B-7B4478179D19
      ),
    ]
  )

チェックボックスをタップしたことによって、最初の Todo のisCompleteプロパティだけがfalseからtrueに変化していることが簡単にわかります。もちろん、descriptionも同じように調べることができますが、実際に試してみて頂けると良いと思います。

おわりに

まとめると言いながら、元の記事が良くまとめられすぎていて、ただの翻訳のようになってしまった気がします...
Composable Architecture には、あと3つの Part2 - Part4 の記事があり、Composable Architecture のパワフルなテストサポート機能を使ったテストを書いたり、あまり説明しなかった Effect を使ったり、より実践的な Composable Architecture について知ることができます。GitHub のリポジトリにもいくつかサンプルがあったので、そちらも覗いてみると参考になりそうです。
自分も Part4 まで一通り読みながら写経してみて、Composable Architecture すごい!と思い続けていたので、元の記事を参照していただけると、もっと Composable Architecture について知ることができるので、ぜひ読んでみてもらえると良いと思います!

参考文献

※本記事は、 POINT-FREE さんが公開している A Tour of the Composable Architecure をもとに許可を得て作成しています。

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