チンチラのフンみたいなもの

毎日の学んだことを書いていきます

競プロ典型90問 010 - Score Sum Queries(★2)

問題はコチラから

問題文

ABC 大学には N 人の一年生が在籍しています。クラスは 2 つあり、学籍番号 i 番の生徒のクラスは Ci組です。今日は期末試験が返却され、学籍番号 i 番の生徒の点数は Pi点でした。
以下の形式の質問が Q 個与えられます。j=1,2,…,Q それぞれについて答えてください。
・学籍番号 Lj∼Rj番の 1 組生徒における、期末試験点数の合計
・学籍番号 Lj∼Rj番の 2 組生徒における、期末試験点数の合計
・これら 2 つの値をそれぞれ求めよ。

制約

  • 1≤N≤100000
  • 1≤Ci ≤2
  • 0≤Pi≤100
  • 1≤Q≤100000
  • 1≤Lj≤Rj≤N
  • 入力は全て整数

私のコード

 単純にたすだけじゃんと思って書いたのだが、TLEになる。Time Limit Exceededの略で実行時間超過を意味する。今回だと2秒以上かかってしまったためACにならなかった。この問題は2重ループをするとよく起こるみたい。実際私もfor文の中にfor文を入れるということをしている。
 ならfor文を一つ減らそうと思った。一番外側のfor文は答えをprintする部分だから解消する意味は薄そうに思った。ならもう一つの方を変えようと思ったが、なかなか良い方法が思いつかない。そこで今回は他の人のコードを見て勉強させて頂く!

let a = Int(readLine()!)!
var data = [[Int]]()
for _ in 1 ... a {data.append(readLine()!.split(separator: " ").map{Int($0)!})}
let q = Int(readLine()!)!
var question = [[Int]]()
for _ in 1 ... q {question.append(readLine()!.split(separator: " ").map{Int($0)!})}
for j in 0 ..< q {
    var sum1 = 0
    var sum2 = 0
    for i in question[j][0]-1 ..< question[j][1] {
        if data[i][0] == 1{
            sum1 += data[i][1]
        }else{
            sum2 += data[i][1]
        }
    }
    print("\(sum1) \(sum2)")
}

他の人のコード

func readInt() -> Int {
    Int(readLine()!)!
}

func readIntArray() -> [Int] {
    readLine()!.split(separator: " ").map { Int(String($0))! }
}

let n = readInt()
var class1Sum = [Int](repeating: 0, count: n + 1)
var class2Sum = [Int](repeating: 0, count: n + 1)

for i in 1...n {
    let cp = readIntArray()
    let c = cp[0]
    let p = cp[1]
    if c == 1 {
        class1Sum[i] = class1Sum[i - 1] + p
        class2Sum[i] = class2Sum[i - 1]
    } else if c == 2 {
        class1Sum[i] = class1Sum[i - 1]
        class2Sum[i] = class2Sum[i - 1] + p
    }
}

let q = readInt()
for _ in 0..<q {
    let lr = readIntArray()
    let l = lr[0]
    let r = lr[1]
    print("\(class1Sum[r] - class1Sum[l - 1]) \(class2Sum[r] - class2Sum[l - 1])")
}

改善点

  • [ [ Int ] ]型の変数を宣言しなくても、for文の中で[Int]型の変数を宣言することで処理数を減らせる
  • [Int] (repeating: 0, count: n + 1)を使うことで、var sum1 = 0、var sum2 = 0といった宣言をしなくて済む

解説

競プロ典型90問 004 - Cross Sum(★2)

はじめに

 転職活動で溜まったストレスを発散するため、疲れた時はちょこちょここういうの解いていきます。自分で考えて解いた後、他の人の良い書き方を見つけたら随時記事は更新していきます。最初の自分の提出についてはずっと残しときます。

問題文

H 行 W 列のマス目があります。上から i (1≤i≤H) 行目、左から j (1≤j≤W) 列目にあるマス (i,j) には、整数 A i,jが書かれています。 すべてのマス (i,j) (1≤i≤H,1≤j≤W) について、以下の値を求めてください。
 ・マス (i,j) と同じ行または同じ列にあるマス(自分自身を含む)に書かれている整数をすべて合計した値

制約

  • 2≤H,W≤2000
  • 1≤Ai,j ≤99
  • 入力は全て整数

私のコード

そのマスから縦横足した数から、そのマスの値を引いた数が求められている数であることに注目した。縦の合計や横の合計はマス間で共通するものがあるので変数に格納し、最後にそれを使ってマスの値を変更し、出力!

let ab = readLine()!.split(separator: " ").map{Int($0)!}
var twoArray:[[Int]] = []
var hSum:[Int] = []
var vSum:[Int] = []
func printResult(){
    for _ in 1 ... ab[0] {
        twoArray.append(readLine()!.split(separator: " ").map{Int($0)!})
    }
    for i in 0 ... ab[0]-1{
        hSum.append(twoArray[i].reduce(0,+))
    }
    for i in 0 ... ab[1]-1{
        var element = 0
        for j in 0 ... ab[0]-1{
            element += twoArray[j][i]
        }
        vSum.append(element)
    }
    for i in 0 ... ab[0]-1{
        for j in 0 ... ab[1]-1{
            twoArray[i][j] = hSum[i] + vSum[j] - twoArray[i][j]
        }
    }
    
    for i in 0 ... ab[0]-1{
        print(twoArray[i].map{String($0)}.joined(separator: " "))
    }
}

printResult()

他の人のコード

let s = readLine()!.split(separator: " ").map({Int(String($0))!})
let(h,w)=(s[0],s[1])
var t=[[Int]]()
var hs=[Int](repeating:0,count:h)
var ws=[Int](repeating:0,count:w)

for _ in 1...h {
    let s=readLine()!.split(separator: " ").map({Int(String($0))!})
    t.append(s)
}

for i in 0..<h {
    for j in 0..<w {
        let tij=t[i][j]
        hs[i]&+=tij
        ws[j]&+=tij
    }
}
for i in 0..<h {
    var ansl=[String]()
    for j in 0..<w {ansl.append(String(hs[i]&+ws[j]&-t[i][j]))}
    print(ansl.joined(separator: " "))
}

改善点

  • let(h,w)=(s[0],s[1]) でいちいちs[0]とか書かない
  • ..< を使えば-1いらない
  • 繰り返し使わないなら関数宣言しない
  • [Int] (repeating:,count:)で配列を作ることで、t[i][j]に合わせてiやjを指定できる
  • iが横、jが縦を表しているのだからそれに合わせて横の合計、縦の合計も作れる
  • 式が一つの時はfor文でも1行で書ける

解説

 

iOSアプリ開発でAPIキーを切り離す

やり方

 cocoapods-keyを使う。これを使えばAPIキーをソースの中ではなく、キーチェーンの中に保存できるみたい。

実際にやってみる

Usageに従ってPodfileに以下のように書く

plugin 'cocoapods-keys', {
  :project => "SalesCallsRecord",
  :keys => [
    "GMSServices",
    "GMSPlacesClient"
  ]}

そして

pod install

すると、まだ登録していないキーについて入力が求められるのでそこでAPIキーを入力。入力後インストールが完了する。
そして、APIキーを使うAppDelegateに以下のように書く

import Keys

class AppDelegate: UIResponder, UIApplicationDelegate {

let keys = SalesCallsRecordKeys()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        GMSServices.provideAPIKey(keys.gMSServices)
        GMSPlacesClient.provideAPIKey(keys.gMSPlacesClient)
        return true
    }

Keysをインポートすることで先程Podfileで書いた内容にしたがって自動的にクラスとプロパティが作られるみたい。 よし、これでGitHubをprivateからpublicにするぞ!と思ったのですが、やはりというかファイルの変更部分にガッツリAPIキー載ってますね笑。APIキーを直接書いたまま1回でもコミットするとだめなのかな?その辺り明日くらいに調べてみようと思います。

チェスみたいな55将棋

作ったもの

 今日は眠すぎる!早めに寝たいので以前に作ったチェスみたいな55将棋を紹介して終わります。GitHub
 これはセールスマップがAppleにGuidline4.3問題でリジェクトくらいまくってた時に作ったものです。リジェクト理由が訳からなすぎて、リリースできないものを作ってしまったんだと考えました。そこで一番作りたかった将棋のアプリを何でもいいからリリースしようと思って作りました。このチェスの動画を参考にしました。将棋のロジックとビューをどのように連携するのかだいたい分かった所で切り上げました。その後は将棋のロジックについて深く勉強するためライブラリ「SwiftShogi」のソースコードを読み、以前書いたこの記事に入っていく訳です。
 結局このアプリはセールスマップの問題が解決したことにより、開発が止まっていますが、転職活動が終わりましたらまた再開したいと思います。

終わりに

 記事のストックを作っとかないと忙しい日や早く寝たい日などに対応できないことがよく分かった。とにかく勉強してネタを作ろう。おやすみなさい。

Udemyで英語でMVVMを学んでみた(a little)

はじめに

 Udemyが今セール中で、MVVMについて学びたかったのでこの動画を買ってみた。Udemyで言語が英語の教材は何度かある。字幕をGoogle翻訳で日本語で表示できる様にしても、あまり理解できず途中で止めてしまうことがよくあった。そこで今回はいつもとはやり方を変えて学ぶことにした。
 やり方はまず動画を見る前にトランスクプリションを表示して、それを全てコピーして、Deepleで翻訳する。そして翻訳された文章を読んである程度理解する。またあまり理解できなかった所には目星をつける。その状態で動画を見る様にする。これなら理解できるのではないかと思いやってみる!

MVCにおける問題

 Modelはデータのこと、Viewはユーザーインターフェースのこと、ControllerはModelとViewの仲介を表す。モデルは直接ビューに話かけず、コントローラーに話しかけ、コントローラーがデータをビューに送る。つまりコントローラーは、モデルを受け取り、それをビューに渡すのが仕事。
 MVCの場合Viewを変更しようとすると、Controllerにコードを書き込むことになる。その結果 Controllerが肥大化し、保守性の低いコードになる。MVCでも中身を切り分けることでその問題を解決することはできる。しかし、それには多大な時間を要する。

MVVMとは?

・・・正直MVCとの違いがよく分からなかった。分かったのはVMはModelとViewをカップリングしたもの、Modelはビジネスロジックを表すくらいでしょうか。この辺りは実際にコードで書かないと理解できないと思うので、今回のイントロダクションだけでこの部分を書くのは難しかった。

感想

 初めてこのやり方をとって勉強したがかなり非効率。動画は12分くらいだったが、終わるのに1時間以上かかった。動画自体は評価も高く、決して悪いものではない。しかし、動画のトランスクプリションは悪かった。講師が話した言葉から自動生成されるもので、かなり精度が低い。Deepleに訳してもらっても理解できない部分が目立った。よってこの方法はあまりよくないものだろう。
 よく公式ドキュメントやREADMEをDeeplに訳してもらいるが、そこで理解しにくいと感じることはあまりない。なので現状は英語での教材はドキュメントに限定した方が良さそう。色々調べてみるとこのサイトが良質なチュートリアルをたくさん載せていて良さそう。以前に一度何気なくこのサイトを利用したことがあるが、その時も分かりやすいなと少し感動した覚えがある。なので今度はこちらで勉強してみようと思う。
 ただUdemyは本当にいい動画が多く、更新頻度も高いので、今後の成長の観点からもこのまま捨てるのは絶対に避けたい。英語の勉強をするしかないと思った。

ライブラリ「SwiftShogi」のソースコードを読んでみた

はじめに

将棋のアプリをリリースしたいなと思って調べていると、Swiftのみで将棋のロジックを書いている凄いライブラリを発見!ソースコードを読んで将棋のロジックをプログラミングで書くとどのようになるのか勉強させて頂く!GitHubコチラから

※この記事は勉強の時にNotionにメモっていたのをコピペしたものです。中身も途中ですし、見にくいですがご了承下さい。いつかもう少しいいものを書きます。

Game

  • board、color、capturedPiecesの3つのプロパティを持つ構造体。それぞれ盤の駒の配置、手番、持ち駒を表す。イニシャライザは普通のものの他に引数にSFEEN クラスを使って初期化することも出来るが、これは盤を文字列で表したもの。構造はGameとよく似ている。初期位置で初期化すると↓のような感じになる。

プロパティ

  • movesFromBoard: [Move]
  • movesFromCapturedPieces: [Move]

メソッド

  • perform(_ move: Move)
  • validate(_ move: Move)
  • validMoves() -> [Move]
  • validMoves(from source: Move.Source, piece: Piece) -> [Move]
  • sortCapturedPieces()
  • capturePieceIfNeeded(from destination: Move.Destination)
  • remove(_ piece: Piece, from source: Move.Source)
  • insert(_ piece: Piece, to destination: Move.Destination, shouldPromote: Bool)
  • validateSource(_ source: Move.Source, piece: Piece)  動かそうとする駒が大丈夫なものか検証するメソッド。盤上の駒でその駒がpieceと一致しないなら「盤上に駒がない」エラー。持ち駒で持ち駒にpieceが含まれていないなら「持ち駒に駒がない」エラー。pieceの先手後手がcolorと一致しないなら「自分の駒ではない」エラー。
  • validateDestination(_ destination: Move.Destination)  駒がboard(square)に動いてもいいか検証するメソッド。case let .board(square)はバリューバイディングパターンと言う書き方。(参考参考)board(square)に駒がありかつ、その駒が自分の駒なら「そのマスには自分の駒がすでにある」のエラーを投げる
func validateDestination(_ destination: Move.Destination) throws {
        switch destination {
        case let .board(square):
            // If a piece at the destination does not exist, no validation is required
            guard let piece = board[square] else { return }

            guard piece.color != color else {
                throw MoveValidationError.friendlyPieceAlreadyExists
            }
        }
    }
  • validatePromotion(source: Move.Source, destination: Move.Destination, piece: Piece)
  • isValid(for move: Move) -> Bool  do-catch文を使っている唯一のメソッド。ここからthrowsキーワードを指定したメソッドを実行していく。このような形にしているのは処理ごとにメソッドを定義することで可読性を上げる為だと思われる。また定義にthrowsキーワードを指定していない場合、do-catch文に囲まれていないthrow文によるエラーはコンパイルエラーになるため、エラーが起こりうるメソッドには全てthrowsが指定されている。そしてthrowsキーワードが指定された処理を呼び出すためには呼び出す前にtryキーワードが必要となるので、このメソッド内ではもちろん、他のthrowsキーワードが指定されたメソッド内でtryキーワードが使われている。その2つ以外で使うとコンパイルエラー。
  • boardPieceMoves(for piece: Piece, from square: Square) -> [Move]
  • capturedPieceMoves(for piece: Piece) -> [Move]

ネストされた型

  • MoveValidationError: Error  throw文で投げるエラーを表した列挙体。当然ながらエラープロトコルに準拠している。上から順に「盤上に駒がない」、「持ち駒に駒がない」、「自分の駒ではない」、「そのマスには自分の駒がすでにある」、「駒は成れない」、「ルール違反な盤上の駒の成」、「ルール違反な持ち駒の駒の成」、「ルール違反な駒の動き」、「王手がかかっている」、「駒は既に成っている」を表している
public enum MoveValidationError: Error {
        case boardPieceDoesNotExist
        case capturedPieceDoesNotExist
        case invalidPieceColor
        case friendlyPieceAlreadyExists
        case pieceCannotPromote
        case illegalBoardPiecePromotion
        case illegalCapturedPiecePromotion
        case illegalAttack
        case kingPieceIsChecked
        case pieceAlreadyPromoted
    }

Square

  • 盤のマスを表した列挙型。file、rankのプロパティを持つ。file、rankはチェスの縦、横の意味。プロパティも列挙型で配列として使えるようにCaselterableプロトコルに準拠している。
  • この辺りのコードの意味がよく分からない
extension Square {
    public init(file: File, rank: Rank) {
        self = Self.allCases.first { $0.file == file && $0.rank == rank }!
    }

    public var file: File { File(rawValue: rawValue / File.allCases.count)! }
    public var rank: Rank { Rank(rawValue: rawValue % Rank.allCases.count)! }

    public static func cases(at file: File) -> [Self] { allCases.filter { $0.file == file } }
    public static func cases(at rank: Rank) -> [Self] { allCases.filter { $0.rank == rank } }

    public static func promotableCases(for color: Color) -> [Self] {
        let ranks: [Rank] = color.isBlack ? [.a, .b, .c] : [.g, .h, .i]
        return allCases.filter { ranks.contains($0.rank) }
    }
}
  • この記事enumについて分かりやすかった。rawValeuはenumの宣言の後にIntを指定することで使用することが出来るようになっている。Square.rawValueで各caseの0~80の番号を取得出来る。またFile(rawValue: Int)やRank(rawValue: Int)でインスタンス化出来る。これは失敗可能イニシャライザなので強制アンラッピングをしている。
  • Selfキーワードで型内部で型自身へアクセス出来るためスタティックプロパティにアクセス出来る。allCasesはCaselterableプロトコルで定義されているスタティックプロパティ。またSelfキーワードはメソッドの返り値の型として扱うことが出来る。 参考
  • スタティックメソッドとして、ある行や列と同じマスを返すものと、手番から成駒に変わることが出来るマスを返すメソッドを定義している。
  • file、rankはコンピューテッドプロパティ。returnがないがswift5.1からgetterの中身の式が1つだけなら省略してもいいことになったみたい。参考 つまりSquareのイニシャライザはallCasesを使うことでrawValueを取得でき、そこから計算してfileとrankプロパティを初期化し、引数と一致するものを返すようにしている。

Color

  • 手番を表す列挙型。blackとwhiteの2つ。CaseIterableプロトコルに準拠している。

プロパティ

  • isBlack : Bool  先手か後手かを判定するコンピューテッドプロパティ

メソッド

  • toggle()  Colorのインスタンスの値をもう一方の値に変える。enumの値を変えるためmutatingキーワードが使われている。
  • toggled() - > Self  toggle()を使い、現在の Colorインスタンスの値を返す
  • init?(character: Character)  characterが”b”ならblack、”w”ならwhite、それ以外ならnilを返す失敗可能イニシャライザ。character型はstring型とよく似ているがcharacter特有のメソッド等が定義されているみたい。しかし、ここでなぜわざわざcharacterを採用しているかは分からない
  • < (lhs: Color, rhs: Color) -> Bool  Comparebleに準拠した時に必要なメソッド。Color.allCases.firstIndex(of:Element)を使って先手後手を比較している。このメソッドの使い道はよく分からない

Move

  • 反則がないように動ける範囲を示す構造体。source、destination、piece、shouldPromoteプロパティを持つ。shouldPromoteのデフォルトはfalse。動いた先に駒があるかどうかとかのプロパティはいらないのだろうか?

プロパティ

  • source : Sorce  Sorceはboard(Square)とcapturedPieceの2つのcaseを持つ列挙型。盤上の駒か持ち駒かを分けるため。boardは連想値にSquare型を持つ。連想値によって盤上のどのマスにある駒かを表す
  • destination : Destination  Destinationはboard(Square)飲みをcaseに持つ列挙型。Destinationは英語で行き先という意味なので駒の動けるマスではなく、動いた先の1マスを表すのだろう。動けるマスとかなら配列とかになるだろうし。1caseのみで列挙型にしているのはよく分からない。コンピューテッドプロパティにするとか、メソッドでSquareを返すとかではまずかったのだろうか。
  • piece : Piece  駒の種類を表す
  • shouldPromote : Bool  駒が必ず成らなければならないかどうかを示す。駒が進めないように駒を配置することは反則なので、盤上から駒が動いた時成が必要かどうかを示す。これに関係あるのは歩、桂馬、香車

Direction

  • 駒の動き方を表す列挙体。CaseIterableに準拠する。north、sourth、east、west、northEast、northWest、southEast、southWest、northNorthEast、northNorthWest、southSouthEast、southSouthWestの12個のcaseがある。northNorthEast、northNorthWest、southSouthEast、southSouthWestは桂馬の動きを表し、自分駒と敵駒とで分けている

プロパティ

  • flippedVertically: Self  componentsの要素の.northを.sorthに.sorthを.northに変えて、その動きにあったDirectionを返すコンピューテッドプロパティ。要はPieceの駒の種類ごとに先手の駒の動きを設定した時、これを使えば後手でのその駒の動きも取得できるというもの。将棋の駒の動き方は全て左右対象であるが、上下対象ではないためこのようになる
  • containsNorth: Bool  componentsの中に.northが含まれるかどうかの真偽を表すコンピューテッドプロパティ。flippedVerticallyを使うかどうかの判断するのには必要がないことなので、どういった場面で使うか疑問
  • containsSouth: Bool  componentsの中に.southが含まれるかどうかの真偽を表すコンピューテッドプロパティ。
  • shift: Int  componentsの各要素のshiftを全て足し合わせたコンピューテッドプロパティ。これによってPieceのSquareのRawValueとこのshiftを足し合わせることで、駒がどのマスに動くのかが分かる。足し合わせた時0~80以外の数字ならそこには移動できないということもすぐ分かる
  • components: [Component]  駒の動き方がどのようなマスの移動の組み合わせによって成り立つかを表したコンピューテッドプロパティ
var components: [Component] {
        switch self {
        case .north: return [.north]
        case .south: return [.south]
        case .east: return [.east]
        case .west: return [.west]
        case .northEast: return [.north, .east]
        case .northWest: return [.north, .west]
        case .southEast: return [.south, .east]
        case .southWest: return [.south, .west]
        case .northNorthEast: return [.north, .north, .east]
        case .northNorthWest: return [.north, .north, .west]
        case .southSouthEast: return [.south, .south, .east]
        case .southSouthWest: return [.south, .south, .west]
        }
    }

ネストした型

  • Component  Directionの各caseでSquareで考えた時どこに移動するのかを表した列挙型。north、sourth、east、westの4個のcaseがある。プロパティにshift: Intを持つ。Squareは右縦1列から順に並んでいる。前へ1マス移動するとSquare.allCasesの配列番号は1下がることになる。後ろへ1マス移動する時はその逆。右に1マス移動する時は縦1列分下に下がることでちょうど隣に移動するので、縦1列のマス分マイナス。左に1マス移動する時はその逆
var shift: Int {
            switch self {
            case .north: return -1
            case .south: return 1
            case .east: return -File.allCases.count
            case .west: return File.allCases.count
            }
        }

Piece

  • 駒の種類を表す構造体。構造体のインスタンスに関わる部分で1つのネストがある。extensionで成駒に関するネストがある。

プロパティ

  • kind : Kind  列挙体Kind
  • color : Color  先手か後手かを表す
  • isPromoted: Bool  駒の種類が成駒かどうかを表すコンピューテッドプロパティ。↓のように書くことで何度もtureと書くのを省略できる
public var isPromoted: Bool {
        switch kind {
        case .pawn(.promoted),
             .lance(.promoted),
             .knight(.promoted),
             .silver(.promoted),
             .bishop(.promoted),
             .rook(.promoted):
            return true
        default:
            return false
        }
    }
  • canPromote: Bool  駒の種類が成駒になれるかどうかを表すコンピューテッドプロパティ。上のコードのpromotedをnormalに変えたもの
  • attacks: Set  コンピューテッドプロパティ。Setについてはこの記事が分かり安かった。Setは大体配列と同じ。要素はユニーク。Set型を2つを合わせる時などはSetならではのメソッドが使える。要素はHashableに準拠する必要があるのでAttack定義でHashableに準拠させている。pieceAttacksをスタティックプロパティにすることで、インスタンス化せずにpieceAttacksを使って定義することができている。
var attacks: Set<Attack> { Self.pieceAttacks[self]! }
  • pieceAttacks: [Self: Set]  スタティックプロパティ。定数なのmutatingを使っても値を変えられない。Dictionary(uniqueKeysWithValues:)はArrayやDictionaryのようなSequenceに準拠した型を引数にとり、それを元に新たにDictionary型を生成するメソッド。
static let pieceAttacks: [Self: Set<Attack>] = Dictionary(uniqueKeysWithValues: piecesAndAttacks)
  • piecesAndAttacks: [(Self, Set)]
  • attackableDirections: [Direction]  駒が動くことができる方向を表したコンピューテッドプロパティ。駒に当たるまで動けることなどは考慮しない。kindの各caseに応じて[Direction]を設定する。colorが.whiteの場合は[Direction]の各要素をflippedVerticallyを使って変換
  • farReachingDirections: [Direction]  駒に当たるまで動くことができる駒の動きを表したコンピューテッドプロパティ。kindが.lance(normal)、.bishop、.rookの場合に[Direction]を設定。他の場合は[]。colorが.whiteの場合は[Direction]の各要素をflippedVerticallyを使って変換
  • allCases: [Self]  kindsAndColorsの各要素を引数にしてイニシャライザでPieceを生成。全てのパターンとPieceがいっちょ上がり
  • kindsAndColors: [(Kind, Color)]  KindとColorの全ての組み合わせパターンを配列にしたスタティックプロパティ。Kind.allCases.flatMapを処理内で使っているが、flatMapは多次元配列を1次元配列にするもの。(参考)これは型に合わせるために必要なもの。タプルと言う型みたい。(参考)わざわざこの型にしているのはイニシャライザを使う時に使いやすくする為だと思われる。↓は実際のコードと実行結果
private static var kindsAndColors: [(Kind, Color)] {
        Kind.allCases.flatMap { kind in
            Color.allCases.map { color in (kind, color) }
        }
    }

Kind.allCase.flatMapバージョン

[(__lldb_expr_15.Kind.pawn(__lldb_expr_15.State.normal), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.pawn(__lldb_expr_15.State.normal), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.pawn(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.pawn(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.lance(__lldb_expr_15.State.normal), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.lance(__lldb_expr_15.State.normal), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.lance(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.lance(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.knight(__lldb_expr_15.State.normal), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.knight(__lldb_expr_15.State.normal), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.knight(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.knight(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.silver(__lldb_expr_15.State.normal), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.silver(__lldb_expr_15.State.normal), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.silver(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.silver(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.gold, __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.gold, __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.bishop(__lldb_expr_15.State.normal), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.bishop(__lldb_expr_15.State.normal), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.bishop(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.bishop(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.rook(__lldb_expr_15.State.normal), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.rook(__lldb_expr_15.State.normal), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.rook(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.rook(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.king, __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.king, __lldb_expr_15.Color.white)]

Kind.allCases.mapバージョン

[[(__lldb_expr_22.Kind.pawn(__lldb_expr_22.State.normal), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.pawn(__lldb_expr_22.State.normal), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.pawn(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.pawn(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.lance(__lldb_expr_22.State.normal), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.lance(__lldb_expr_22.State.normal), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.lance(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.lance(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.knight(__lldb_expr_22.State.normal), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.knight(__lldb_expr_22.State.normal), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.knight(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.knight(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.silver(__lldb_expr_22.State.normal), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.silver(__lldb_expr_22.State.normal), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.silver(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.silver(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.gold, __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.gold, __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.bishop(__lldb_expr_22.State.normal), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.bishop(__lldb_expr_22.State.normal), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.bishop(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.bishop(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.rook(__lldb_expr_22.State.normal), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.rook(__lldb_expr_22.State.normal), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.rook(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.rook(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.king, __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.king, __lldb_expr_22.Color.white)]]

メソッド

  • init(kind: Kind, color: Color)  駒の種類、先手か後手かの2つで生成
  • promote()  kindの連想値がnormalならpromotedに変えるメソッド。構造体のプロパティの値を変えるのでmutatingキーワードが使われている
  • unpromote()  kindの連想値がpromotedならnormalに変えるメソッド。mutaitingキーワードが使われている。成駒が盤上でまた裏返ることは将棋で起こり得ないが、成駒が取られて持ち駒になる時などに使うのだろうか?⇦そうだった
  • capture(by color: Color)  unpromote()してcolorを引数のものに変える。mutatingキーワードが使われている。しかし、引数で指定する形だと誤ってもともとのcolorと同じものを指定してしまう可能性がある。そのような状況になることは将棋のルール上あり得ないのでColorのメソッドtoggle()を使った方がいいと思う
  • init?(character: Character, isPromoted: Bool)  棋譜とかの文字から生成する用のイニシャライザ。引数をStringではなくCharacterを使うことで、文字を小文字にするlowercased()と文字が大文字かどうかを判定するisUppercaseを処理の中で使っている。isPromotedからstate:Stateを定義する。character.lowercased()によってpieceを設定する。character.isUppercaseがtrueならcolorに.black、falseなら.whiteを設定する。先手の”歩”ならinit?(character:”P”,isPromoted:false)、後手の”と”ならinit?(character:”p”,isPromoted:true)のように使う

ネストした型

  • Kind  pawn(State)、lance(State)、night(State)、silver(State)、gold、bishop(State)、rook(State)、kingの8つのcaseからなる列挙体。連想値Stateで成駒かどうかを表す。連想値を使用しているため、CaseIterableに準拠しても自動的にallCasesプロパティが作られないためallCasesプロパティを実装している。またComparableに準拠して演算子を実装しているが、こちらは活用法はよく分からない。
extension Piece.Kind: CaseIterable {
    public static let allCases: [Self] = [
        .pawn(.normal), .pawn(.promoted),
        .lance(.normal), .lance(.promoted),
        .knight(.normal), .knight(.promoted),
        .silver(.normal), .silver(.promoted),
        .gold,
        .bishop(.normal), .bishop(.promoted),
        .rook(.normal), .rook(.promoted),
        .king,
    ]
}
extension Piece.Kind: Comparable {
    public static func < (lhs: Piece.Kind, rhs: Piece.Kind) -> Bool {
        return allCases.firstIndex(of: lhs)! < allCases.firstIndex(of: rhs)!
    }
}
  • State  Stateはnormal、promotedの2つのcaseからなる列挙体。成駒かどうかを表す
  • Attack: Hashable  プロパティにdirection: DirectionとisFarReaching: Boolを持つ構造体。Hashableに準拠しているので辞書型のキーに使える

アプリリリースしました!!!!④(最終回)

前回 

アプリに使った技術 Firebase編

Firebaseを採用した理由

 アプリの認証機能、DBにはFirebaseを使っています。認証機能を付ける必要があるのは、データをユーザーごとに切り分ける必要があるからです。Firebaseはクラウドサービスです。DBからデータを全て持ってこようとすると全てのユーザーが記録した訪問情報を表示することになるので、ユーザー認証して、そのユーザーのデータのみ持ってくる必要があります。クラウドサービスではなく、Realmのようなローカルに保存するDBを使えばローカル内でしかデータのやり取りをしないので認証機能を付ける必要がありません。そうすればユーザーも少しめんどくさいメールアドレスやパスワードの入力もしなくて済みます。
 今回わざわざクラウドサービスを使ったのには2点理由があります。まず1点目はアプリをアンインストールでデータが消えるようにはしたくなかったからです。ローカルDBはアプリをアンインストールするとデータが消えます。仕事で使うことをイメージしたアプリなので、仕事の大事なデータが簡単に消えてしまうのはよくないと考えました。2点目は今後の機能追加を見越してのことです。当初のアプリのイメージは記録と共有ができるというものでした。共有機能があれば仲間内での記録のやり取りができるので、チームのその日のノルマまであとどれくらい必要かがリアルタイムで把握でき、日報提出や引き継ぎにも役立ちます。この機能の実装は難易度が高く、パッとできるものではないので「Done is better than perfect」の精神で今回は見送らせて頂きました。以上2点の理由からFirebaseを採用させて頂きました。

工夫した点

 Firebaseの使用にはお金が掛かります。と言っても無料枠があり、1日5万件の読み取り、2万件の書き込み、2万件の削除までは無料で使えます。なのでよっぽどアプリが流行らない限りはお金は掛からないと言っていいでしょう。しかし、アプリが流行らなくてもお金が掛かってしまうことがあります。「クラウド破産」です。クラウド破産とはクラウドサービスの料金が意図せず高額になることです。詳しくは記事を読んで欲しいのですが、主にアプリの設計ミスや公開してはいけない情報が漏れてしまったことが原因で起こるようです。私はこのことにかなりビビりました。なんせアプリが流行ったわけでもないのに、数十万、数百万の請求が突然くるのですからたまったものではありません。Googleに事情を話せば返金してくれることもあるようですが、できるだけそんな面倒ごとは避けたいです。
 そこで私は2点ほど工夫をしました。1点目はprint文を使うことです。読み込み、書き込み、削除をした際には必ず「〇〇データの取得に成功しました」や「〇〇データの削除に成功しました」のような文がコンソールに出力されます。これにより設計ミスにより必要もなく何度もデータを取得等していたら、コンソールに文が何度も表示されるのですぐに気づきます。また「セールスマップ」の場合GoogleMapsも使っているので、住所を取得する際にはそれもprint文で表示するようにしています。
 2点目はFirebaseとのやり取りそのものを少なくすることです。Firebaseを使っている記事を見るとよくviewWillAppearにデータを取得するメソッドを使っているのですが、私はそんなことはしません。確かにそうすれば簡単に最新のデータを表示することができます。しかしこれは、毎回全てのデータを取得することになるので無駄が多いです。そこでスナップショットリスナーを使います。スナップショットリスナーを使えばデータの変更を感知し、その変更後のデータのみを取得することができます。これにより例えばrecordDataを元にテーブルを表示していた場合、変更後のデータをrecordDataに入れ、reloadDataすれば、必要最低限のデータのやり取りでテーブルを更新できます。 そしてそれと合わせてNotificationCenterを使うことでさらにやり取りを減らします。NotificationCenterを使えば画面遷移が伴わなくても異なるviewController間でデータの受け渡しができます。これによりスナップショットリスナーで取得したデータを他のviewControllerに渡すことができるので、スナップショットリスナーをviewControllerごとにつけて無駄に取得するということがなくなります。以上2点が私がクラウド破産を避けるため工夫したことです。

課題

 アプリをインストールして、最初に得られるユーザー体験にあるストレスをできる限り減らすことが一番の課題だと感じています。個人アプリなので、最初でちょっとでも嫌なことがあるとユーザーはそれ以後使ってくれないように思います。最初のインストール後の一番のストレスはやはりユーザー登録でしょう。面倒だし、個人情報を渡すし渡すし、できればやりたくありません。この点でログイン機能を付けるべきか少し悩んだのですが、調べるとFirebaseでゲストログイン機能を実装できるのですね!凄い!今現状のデータだと仕事で使ってくれてる人がいないので当然といえば当然なのですが、10個データを記録した人はいません。なので10個まではゲストでも記録できるけど、それ以降はユーザー登録してねという形に変えたいと思います。10個ならアンインストールしてデータが無くなってもそれほど問題はないでしょうし。「セールスマップ」はポートフォリオも兼ねており、企業さんはユーザー登録とかは避ける傾向にあるので、転職活動中の今このことはできる限り早急に対応したいなと思います。