JSON ArrayをSwift.Rangeでdecodeすると?

Swift.RangeはCodableである、ではencodeフォーマットは? - がんばってなんか書く の続き

疑問

Swift.RangeJSON encodeした結果は [a, b] となることがわかった。
これはJSON Arrayを使って表現している。

では、[a, b, c] な要素数3以上のArrayに対して、RangeJSON decodeをかけるとどうなるだろうか?

環境

  • Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51)
  • Xcode Version 14.2 (14C18)
    • Playground

実験

インプットするJSONはこちら

{
    "name":"0, 9, 10",
    "value":[
        0,
        9,
        10
    ]
}

JSONDecoder で、decodeする。

JSON内のArrayをRangeでdecodeする

import Foundation

let decoder = JSONDecoder()

let json = """
{
    "name":"0, 9, 10",
    "value":[
        0,
        9,
        10
    ]
}
"""

do {
    struct Content: Codable {
        var name: String
        var value: Range<Int>
    }

    do {
        let data = json.data(using: .utf8)
        let content = try decoder.decode(Content.self, from: data!)

        print(content)
    } catch {
        print(error)
    }
}

結果

Range(0..<9)

decoding errorとはならず、Arrayの第3要素目以降は無視して扱われることがわかった。

素数が足りない場合は...

ちなみに、要素数が足りない場合には、ちゃんとエラーになる

{
    "name":"0",
    "value":[
        0
    ]
}
valueNotFound(Swift.Int, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "value", intValue: nil), _JSONKey(stringValue: "Index 1", intValue: 1)], debugDescription: "Unkeyed container is at end.", underlyingError: nil))

WEB+DB PRESS Vol.132 特集2 「iOS 16最前線」に寄稿しました #wdpress

 おはこんハロチャオ~!あなたの目玉をエレキネット!何者なんじゃ? id:yutailang0119 です!

 これははてなエンジニアアドベントカレンダー2022 24日目の記事です。 昨日は id:yajimasanPostfix のサンドボックス環境をローカルに作る でした。
メールサーバー、何もわからない。

 今回はWEB+DB PRESSの宣伝をしつつ、執筆事情についてお話します!

宣伝

 本日 2022/12/24 (土) 発売 WEB+DB PRESS Vol.132 に寄稿しました!
担当は特集2「iOS 16最前線」で、vol. 121に引き続き、はてなで同僚の id:cockscomb id:kouki_dan との共著です。
その中で、id:yutailang0119 は第2章「Swift 5.6/5.7のアップデート」と第4章「新登場! Swift Chartsフレームワーク」の2つを担当しています。

 今回も編集長の 稲尾さん に連絡をした流れで、「iOS 16の特集記事を」ということになりました。
ありがとうございます!!!

 前回のiOS 14特集との違いは、2022年内発売号での掲載を目指すため、iOS 16 beta期間から執筆が始まりました。
そのため、信頼と実績のある前回メンバー id:cockscomb id:kouki_dan に共著をお願いしました。

 WEB+DB PRESSiOS特集を同じメンバーが担当するのは、初めてのようです。

WEB+DB PRESSの書き方

 アドベントカレンダーということで、雑誌の特集執筆に興味がある人向けに流れを紹介します。

編集部へのコンタクト

 まずは編集部とつながることがファーストステップです。
自分が最初に稲尾さんと知り合ったのは、builderscon tokyo 2017の「WEB+DB PRESS 100号記念特別企画」でした。
知り合いから紹介されて、という流れが多いのではないでしょうか。
WEB+DB PRESSでは持ち込みの投稿も募集しているようですので、詳しくは誌面をご確認ください!

今回はWWDC 22直後の6月中旬に稲尾さんへコンタクトを取りました。

特集テーマ設定

 iOS特集は季節性があるトピックのため、おおむね12月か年明けの2月の号で取り上げられることが多そうです。
その他のトピックについては、残念ながら自分が幹事で担当したことがないので、詳しくは語れません。

 テーマが決まったら以下を並行して進め、技術評論社での企画会議に備えます。

  • 担当するメンバー集め
  • 原稿執筆用GitHubリポジトリの準備
    • issueでコミュニケーションして、PullRequestで原稿を書いていく
  • 特集のアウトラインを決める
    • 章毎のページ数割

今回は12月発売号を目指すことになったので、8月下旬の企画会議で採用が決まりました。
無事に採用が決まれば、執筆のスタートです。

原稿以外の要素決め

 特集を完成させるには、原稿以外にもやることがあります。
以下は執筆スタートから準備を始めます。

  • 特集タイトル
  • キャッチコピー
  • デザインモチーフ

 特に特集タイトルとキャッチコピーは、掲載予定の前の号に予告として掲載されるため、早めに用意が必要です。

 デザインモチーフは、抽象、具体なんでもオーダーできるとのことでしたが、特集テーマに沿って決めています。
2022年のAppleホットトピックは「iPhone 14 Proの常時点灯ディスプレイ」ということで、「ひまわり」で依頼しました。
他の候補は Dynamic Island から「南国の島でリゾート」だったり。
後から突っ込まれたんですが、12月のクリスマス時期発売で、夏っぽいモチーフ...

 vol.121のiOS 14特集のホットトピックはAppleSoCだったので、「シリコン感のあるリンゴ」で依頼しました。

原稿執筆

 原稿執筆のスタートです。

草稿

 まずは1ヵ月程度で草稿を作ります。
体裁の確認が主目的のため、完成している必要はありませんが、特に初めて執筆にチャレンジする人はできるだけ進めておいた方がよいです。
また、共著の場合は各人の癖を把握する期間でもあります。
計画したページ数通りに進行しないこともあるため、ページ数の融通は草稿の段階で相談したいですね。

 今回は経験メンバーをそろえていたので、9月下旬をターゲットに、緩めに意識しながら進めました。

完成原稿

 あっという間に完成原稿です。
全章を通して一通り執筆を完了させます。
ここは気合いあるのみですが、どんどん文字を書くのは楽しくなってきます。

 今回は10月上旬が目標設定でしたが、1週間強遅らせてもらいました...
すみませんでした!!!
あっという間と言っても、実はまだまだ折り返し...

完成原稿(最終)

 完成原稿を簡易レイアウトにはめ込んだPDFを準備してもらえます。
この時点で普段見る雑誌の形式になっていて感動します。

 簡易PDFと元データをもとに、以下の作業をします。

  • 自身、相互にレビュー
  • 分量調整
  • 図の表示確認
  • 文章のブラッシュアップ
  • コードブロックの整形
  • サンプルコードの準備
  • and more...

 対応すべき項目はすべてissueに用意してもらえます。

 おそらく、ページ想定の分量よりも多くなっていることが多いでしょう。
編集担当の忙しさにもよりますが、依頼すると簡易PDFを更新してらえます。
分量調整を始めとして細かな作業が続き、この期間が毎回たいへんです。

 完成原稿から最終まではおよそ1週間ですが、今回は自分が遅れる側でした...

校正

 編集部校正、校正対応、PDFゲラ、著者校正完了、念校完成と進行します。

 著者校正からはGitHubリポジトリに変更していく方式から、Adobe Acrobatでオンラインにフィードバックする方式に変わります。
校正の段階からは、簡易ではなく発売されるレイアウトに入った状態を確認できます。
レイアウトには、前述で依頼したデザインモチーフもあしらわれていることでしょう。

 発売されるレイアウトに厳密に調整ができる反面、ある程度の枠組みが確定するため、大きな変更はしづらくなります。
変更自体のレビューもしづらくなるので、GitHubリポジトリの段階で調整の必要がなくなる心づもりで進めましょう。

 今回はなんだかんだと、11月まで続きました。

校了

 ついに校了です🎉
印刷所に入稿され、あとは発売を待つのみ。

 お疲れさまでした!!!

合計作業時間

 今回はPixelaに記録しておきました。

Total: 111 hour
Max: 7 hour
Min: 0.5 hour
Avg: 2.47 hour

Total Pixels: 45 pixels

 以上、執筆の流れを詳しく紹介しました。
そんなこんなで雑誌が完成して、紙版、電子版がみなさんのお手元に届きます!

謝辞

前回に引き続き、一緒に書いてくれた id:cockscomb id:kouki_dan のお二方、ありがとうございました!

編集長の 稲尾さん をはじめとした編集部の皆様、今回もありがとうございました!

最後に

本日 2022/12/24 (土) 発売 WEB+DB PRESS Vol.132 を、よろしくお願いします!
Amazoneでの購入はこちら

アドベントカレンダー最終日を飾るのは id:motemen です!



以下、小話

 雑誌執筆に関する小話です。

ブラッシュアップ

 作業の中で登場したブラッシュアップには、これまでの集合知で作り上げられてきたツールが用意されています。
秘伝のgrepを使うと、冗長な表現が一掃できます。
URLのチェックにはテキストファイル中に含まれるURLが有効かどうかチェックするツールを作った - EagleLandを紹介してもらいました。

 また、過去の回でも利用していましたが、textlint/textlintyutailang0119/action-textlintは必須のツールでした。
ただ、textlintのルール整備が後手に回っていたのは改善したいポイント。
textlint-ja/textlint-rule-preset-ja-technical-writingを使っていますが、WEB+DB PRESSルールに則れていない箇所があるため、専用のルール用意したい。

 ツールに限らず、文章の書き方指南が充実していて、これをインプットできるだけでも価値のある経験です。

原稿料と特典

 詳細は控えますが、執筆に対して原稿料が支払われます。
我がチームは担当する分量によらず、三等分にしています。
これを海賊スタイルと呼んでいます。

 また、掲載号は紙版と電子版両方、それ以降しばらくはどちらかを毎号受け取る特典も付いています。
かなりありがたく、毎号楽しみに読ませていただいています!

学びの機会

 本を書くことは時間、労力が大きくかかることです。
前述のリターンが見合うかの受け取り方はさまざまでしょう。

 自分は学びの機会を得ることを一つの目標にしています。
毎年、WWDCの内容の9割に目を通すようにしていますが、特集を担当したiOS 14、16の知識が定着している感覚があります。
レビューとして、共著者のアウトプットを最初に読むこともすばらしい特典です。

 iOS特集に限ってですが、WWDCの動画をソースにすることも多いです。
自分は担当した章の中でも、以下の動画は何度も見ていたので、話の内容まで覚えるほどでした...

 学びの機会として、雑誌特集にチャレンジしてみませんか?

Swift.RangeはCodableである、ではencodeフォーマットは?

Swift.Range/ClosedRangeはCodable

Swiftは範囲を表現する型にRange/ClosedRangeを用意してる。

CountableRange CountableClosedRange というそれぞれのwhere条件を持つtypealiasもあるが、ここでは同じものだと思ってほしい。

Range/ClosedRangeBoundEncodable/Decodable である時、Range/ClosedRangeEncodable/Decodable にconformする。
つまり、Codableである。

では、Range/ClosedRangeJSONEncoderJSONDecoder に通すと、どんなフォーマットになるだろう?
試してみましょう。
※encode/decodeはカスタマイズしないものとする

環境

  • Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51)
  • Xcode Version 14.2 (14C18)
    • Playground

Range

Range は半開区間 (左閉右開) である。

RangeをJSONファイルで保存する

import Foundation

let fileManager = FileManager.default
let encoder = JSONEncoder()

let path = NSTemporaryDirectory()
print(path)

do {
    struct Content: Codable {
        var name: String
        var value: Range<Int>
    }

    let contents: [Content] = [
        .init(name: "0..<9", value: 0..<9),
        .init(name: "10..<13", value: 10..<13),
        .init(name: "14..<39", value: 14..<39),
    ]

    do {
        let data = try encoder.encode(contents)
        let json = try JSONSerialization
            .jsonObject(with: data, options: .fragmentsAllowed)
        print(json)

        let location = URL(fileURLWithPath: path)
            .appending(path: "Range.json")
        try data.write(to: location)
    } catch {
        print(error)
    }
}

結果

[
    {
        "name":"0..<9",
        "value":[
            0,
            9
        ]
    },
    {
        "name":"10..<13",
        "value":[
            10,
            13
        ]
    },
    {
        "name":"14..<39",
        "value":[
            14,
            39
        ]
    }
]

ClosedRange

ClosedRange は閉区間である。

ClosedRangeをJSONファイルで保存する

import Foundation

let fileManager = FileManager.default
let encoder = JSONEncoder()

let path = NSTemporaryDirectory()
print(path)

do {
    struct Content: Codable {
        var name: String
        var value: ClosedRange<Int>
    }

    let contents: [Content] =  [
        .init(name: "0...9", value: 0...9),
        .init(name: "10...13", value: 10...13),
        .init(name: "14...39", value: 14...39),
    ]

    do {
        let data = try encoder.encode(contents)
        let json = try JSONSerialization
            .jsonObject(with: data, options: .fragmentsAllowed)
        print(json)

        let location = URL(fileURLWithPath: path)
            .appending(path: "ClosedRange.json")
        try data.write(to: location)
    } catch {
        print(error)
    }
}

結果

[
    {
        "name":"0...9",
        "value":[
            0,
            9
        ]
    },
    {
        "name":"10...13",
        "value":[
            10,
            13
        ]
    },
    {
        "name":"14...39",
        "value":[
            14,
            39
        ]
    }
]

つまり...

Range/ClosedRange をencodeした結果は [a, b] となり、フォーマットは共通である。
encode/decodeにご注意ください。

追記

2022/12/15 23:55

RangeCodable にconformさせるプロポーザルを教えてもらいました。
Add Codable conformance to Range types

iOSDC Japan 2022にオフライン参加した #iosdc

自分自身がずっと溜めていた...

はじめに

iosdc.jp

登壇

LT枠で5分お話ししました。
立木文彦さんにタイトルコール、名前を呼んでもらったので満足。

fortee.jp

speakerdeck.com

オフラン参加

2019年ぶりのオフライン会場にも集まれる形式でした。
生活が変わった人たちも多く、久々に会った人たちと同窓会感があって非常によかった。
はじめましての人とも繋がったり、iOSDC恒例で@shiz3とSwift Concurrencyの立ち話をしたり、終電新幹線に飛び乗る体験をしたり。

id:gigi-net とは毎晩飲みに行きました。

まとめ

情勢もあるし、絶対はないと思うけど、やっぱりいろんな人と会ってプログラミングだったり、仕事だったり、ゲームだったりの話をできるのは、とても楽しかった。

運営の皆様、お疲れ様でした!

プログラムでCore Dataをセットアップする without xcdatamodel

Core Dataを使おうと思って調べると、解説のほとんどが .xcdatamodeld ファイルでセットアップを始める。
.xcdatamodeld はグラフィカルに構造を表示し、便利な反面、裏側の仕組みを理解しづらい、GitHubでレビューしづらいなどのコストは無視できない。

さらに近年問題になり得るのは、Swift PlaygroundsでiOS/iPadOS/macOS向けアプリを作成する環境整備が進んでいることだ。
Macだけでなく、iPadでも手軽にアプリ開発を進めることができるようになるが、Swift Playgroundsでは .xcdatamodel を取り扱うことができない。*1

ということで、ここでは .xcdatamodeld ファイルで始めたプロジェクトを、プログラマブルに置き換える。

ちなみに、.xcdatamodel を複数束ねたものが、.xcdatamodeld

cocoa.hatenablog.com

.xcdatamodeld で定義した状態の確認

まずは置き換え元になる .xcdatamodeld 入りプロジェクトを作る。

プロジェクト作成 with xcdatamodeld

Create a new Xcode project からスタート

"Use Core Data" をチェックして、プロジェクトを作成

.xcdatamodeld がProjectツリーにある

xcdatamodeld の状態

生成されるコード

初期状態だと、Itemは "Class Definition" として、コード生成されている。
ContentView の中で参照されている item.timestamp をクリックすると、生成されたコードを見ることができる。

import Foundation
import CoreData


extension Item {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Item> {
        return NSFetchRequest<Item>(entityName: "Item")
    }

    @NSManaged public var timestamp: Date?

}

extension Item : Identifiable {

}

確認用のデータを入れる

この後の変更で振る舞いが変わっていないかを確認するために、アプリを実行して、データを入れておくとよいだろう。

プロジェクト作成の状態で、タイムスタンプを記録するアプリが動く

Itemをプログラムとして再定義

型定義

コード生成を参考に、自分で定義する。

public class Item: NSManagedObject {
    @NSManaged public var timestamp: Date?
}

extension Item : Identifiable {}

Entity情報

Item型のEntityとしての情報を定義する。

extension Item {
    static var entityDescription: NSEntityDescription {
        let entity = NSEntityDescription()
        entity.name = String(describing: Self.self)
        entity.managedObjectClassName = NSStringFromClass(Self.self)

        entity.properties = [
            {
                $0.name = "timestamp"
                $0.attributeType = .dateAttributeType
                $0.isOptional = true
                return $0
            }(NSAttributeDescription()),
        ]
        return entity
    }
}

NSPersistentContainer初期化時にNSManagedObjectModelを指定

NSManagedObjectModelのEntityにItemNSEntityDescriptionを渡すことで、.xcdatamodeld ファイルがある時と同じ状態のContainerとして初期化する。

let managedObjectModel = NSManagedObjectModel()
managedObjectModel.entities = [
    Item.entityDescription,
]
container = NSPersistentContainer(name: "CoreDataExample",
                                  managedObjectModel: managedObjectModel)

余談だが、nameのみのイニシャライザはnameに渡した文字列で .xcdatamodeldファイルを探しているだけなので、ファイル名を変えるか、nemeの指定文字列を変えると初期化に失敗する。

[error] error: Failed to load model named CoreDataExamples

.xcdatamodeld ファイルを削除

ファイルがあると (正確にはItem定義があると) 生成されるコードと、定義したItemが衝突するので、ファイルを削除しておく。

置き換えの確認

アプリの実行して、変更前のデータを表示できていることを確認。

xcdatamodeld 定義で入れていたデータを表示できる

データ操作もできている。

データの追加、削除も、もちろんできる

To Be Continued

IndexやRelationship、Migrationといった複雑な設定はまた今度

*1:2022/07/14現在

iOSDC Japan 2022にプロポーザル提出した -> 採択された #iosdc

fortee.jp

過去にも採択してもらってトーク枠をもらったけど、隔年の周期っぽい。

提出したプロポーザル

よろしくお願いします。

追記

fortee.jp が無事採択された。

SSOを切っても、GitHubの草を1年生やせた

Contributions in the last year

前の1年は仕事でのリポジトリも含めて1年間GitHubの草を生やしていたが、この1年はSSOを切った状態でも草を生やすことを目標にやってきた。
毎日意味のあるコードを書き続けられた訳ではないけど、毎日PCやスマホGitHubアプリでコードを開いたり、利用しているライブラリのリリースノートを読んだりは、有意義だったと思う。
また1年やっていきたい。

SSOをONにした状態

Contributions in the last year with work

昨年の様子