WEB+DB PRESSの思い出 #wdpress #wdpress_party

WEB+DB PRESSの思い出 - Lento con forza に感化されて、自分も書こうと思います。
WEB+DB PRESSがまさか休刊になると思っていなくて、本当にびっくりしました。

自分がソフトウェアエンジニアを志向したのは大学3 - 4年生の時で、プログラミング教本以外の雑誌という存在に出会ったのは、社会人になってから。
ありがたいことに、WEB+DB PRESSにはイベントレポートを1回、特集を3回寄稿する機会をもらえました。

どれも縁があったと感じられるものです。

builderscon 2017 tokyoイベントレポート

yutailang0119.hatenablog.com

builderscon 2017 tokyo内で行なった「WEB+DB PRESS 100号記念特別企画」の流れで、イベントレポートを担当させてもらいました。
鮮明には覚えていないけど、若者ムーブを発揮して、企画担当に手を挙げたんだと思う。
ここまでWEB+DB PRESSに深い関わりがなかった自分だったけど、読者代表として同じステージに立ち、 WEB+DB PRESS編集長の@inaoの情熱に触れる機会でした。
この出会いが後々に繋がっていくので、目の前のチャンスには躊躇せず飛び込むべきことがわかる。

先日のiOSDC Japan 2023で、@lestrratさんと久しぶりにお会いして、少しお話できましたが、まだまだ伝えきれない感謝があります。

WEB+DB PRESS Vol.116 特集1 「はじめてのトラブルシューティング

yutailang0119.hatenablog.com

builderscon🐟を通じて、より親しくなった id:Soudai さんに誘っていただいて、モバイルアプリの章を担当しました。
忘れもしなくて、会社の忘年会後の二次会中にDMがやってきて、これは夢か?と思った。
初めての特集記事で勝手が分からずに配分に苦労したり、Android領域の解説に悩んだりしたので、無事に入稿された時には灰になっていたと思う。

WEB+DB PRESS Vol.121 特集2 「iOS 14最前線」

yutailang0119.hatenablog.com

この回は自分から@inaoさんにアプローチして、iOSのバージョンアップに対する特集を担当させてもらいました。
今度は自分が特集幹事。
メンバーは同僚の id:cockscomb さん、id:kouki_dan さんにお願いしました。
はてなプレゼンツにできたのは、鼻が高い出来事です。

WEB+DB PRESS Vol.132 特集2 「iOS 16最前線」

yutailang0119.hatenablog.com

iOS 14特集から2年、またどうですかということになり、iOS 16も担当できることになりました。
メンバーは前回と同じく id:cockscomb id:kouki_dan id:yutailang0119
この回も大変だったけど、自分が経験を積み、メンバーも経験者ということで、安定感がある布陣でした。
Swift新機能の解説を浅すぎず、ニッチすぎず、ちょうどよいバランスでできたと思っていて、満足度が高い出来になりました。

まさか、これがWEB+DB PRESS最後のiOS特集になるとは思わなかった...

これ以外にも

自身が直接執筆に携わったわけではないですが...
編集部からPython特集企画の相談をもらい、自分よりも適任がいるだろうとPyCon JPから @rhoboro を中心として、同世代でPythonを解説できそうな人選に協力させてもらいました。
レビューもさせてもらった。

WEB+DB PRESS Vol.104|技術評論社

WEB+DB PRESS Vol.104 特集1「[モダンなコードをギュッと凝縮!]イマドキPython入門 文法,機械学習,Web開発を一気に学ぼう」

その後、書籍の執筆にも繋がったと伺っています。

www.rhoboro.com

id:cockscomb のGraphQL特集もよかったね。

cockscomb.hatenablog.com

おわりに

思い返せば、WEB+DB PRESSとは著者として関わることが深くなり、自分が誇れる仕事の一つになっています。
商業誌でソフトウェアについて文章を書いて、お金を稼ぐという経験ができたのは、本当によかった。
自分をソフトウェアエンジニアとして育ててくれた物の1つなので、WEB+DB PRESS休刊は時代の変化を感じる出来事でした。
最終号Vol.136の次号予告の「ご愛読ありがとうございました。」には、本当に泣きそうになってしまった。

最後に、WEB+DB PRESS編集長の@inaoさんへの感謝。
inaoさんには本当によくしていただいて、任せていただいて、感謝しかありません。
執筆の場での編集者としての関わりだけでなく、イベントでお会いした時には気にかけてくださって、ニコニコ話を聞いてくださって、楽しい思い出ばかりです。
ソフトウェアエンジニアとも、IT企業の他職種とも違った立ち位置で、自分にはとても稀有な存在です。
inaoさんの益々のご活躍を、心からお祈り申し上げます。

この記事はconnpass.comに向かう新幹線で書きました。
所属企業のはてなには、iOSDCに続いて出張として支援もらいました。ありがとうございます!
参加者の皆さま、会場でお会いしましょう!
会場で泣く準備はバッチリです。

最終号の物理本を会場に持って行くので、参加者全員のサイン本にしたいと思っています!
よろしくお願いします!

2022/01/19 (30)

id:yutailang0119 の通り、2023年1月19日に誕生日を迎えました。
ついに三十代に突入。

昨年 yutailang0119.hatenablog.com

ところで、数え年じゃないから、迎えた誕生日は 年齢 - 1 では🤔

仕事

実は去年の誕生日直後、2022年2月1日付で「マンガアプリチーム」に異動しました。
前回ブログを書いた頃には、もう異動が決まっていた。

現在は「GigaViewer for Apps」を作っていて、ほぼiOS Developerとしての稼働になっています。
id:ikesyoid:kouki_dan を始めとして、大人数になったチームで働いています。

CM

hatena.co.jp

生活

三十代怖い

大阪に引っ越します

京都から大阪に引っ越しをします。
はてなから転職するわけではありません。
京都を離れるのは寂しい。

大阪市内へなので、大移動ではないですが。
永住と思って京都に来たわけではなかったけど、関西歴も長くなってきた...

ゲーム

スプラトゥーン3とポケモンSVを行ったり来たり。
最近、Nintendo Switch有機ELモデル)をゲットしました。
スプラトゥーン3モデル、かわいい。

try! Swift Tokyo Meetup

明後日 1/21 (土) 開催の try! Swift Tokyo Meetup のオーガナイザーをやっています!
COVID-19の影響もだいぶ緩和されてきたので、準備運動からという感じです。
今年はWWDCも行きたい。

明日から東京出張。
現地で参加される方、お話ししましょう!

最後に

いつものあれです。
ご支援お待ちしております。
引っ越しもあるので、大きいものや冷凍のものは引っ越し後に...

www.amazon.jp

Programmatic NavigationStack 備考録

これははてなエンジニアアドベントカレンダー2022 37日目の記事です。
2回目の登場 id:yutailang0119 です、おはこんハロチャオ~!
昨日は id:hogashiaタグで#topにリンクするとページ先頭にスクロールするのは仕様 - hogashi.* でした。

冬休みはあつ森で年越ししながら、SwiftUIの2022年OS対応をしていた

Migrating to new navigation typesNavigationStack を読んで、"programmatic navigation" のパターンをイメージしづらかったので、サンプルを用意しながら動きを見てみた備考録。

Enviromnent

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

単一の型に対応した画面遷移

 まずはPark型を単独で扱う画面遷移パターン。

NavigationStackに記載されるコード断片の裏側を推測して進めていく。

対応する型 -- Park

 SwiftUIでナビゲーションに対応する型には Hashable にconformさせる。
Identifiable にもするのは、List で扱いやすくするため。

struct Park: Identifiable, Hashable {
    var id: String {
        name
    }

    var name: String
}

画面遷移の実装

 画面遷移にはNavigationStackiOS 16新API init(_:value:)navigationDestination(for:destination:)を使う。

import SwiftUI

struct ContentView: View {
    var parks: [Park] = [
        Park(name: "Yosemite"),
        Park(name: "Sequoia"),
    ]

    var body: some View {
        NavigationStack {
            List(parks) { park in
                NavigationLink(park.name, value: park)
            }
        }
        .navigationDestination(for: Park.self) { park in
            ParkDetails(park: park)
        }
        .navigationTitle(String(describing: type(of: self)))
    }
}

struct ParkDetails: View {
    var park: Park

    var body: some View {
        Text(park.name)
            .navigationTitle(String(describing: type(of: self)))
    }
}

 これで Park のリストをタップすると ParkDetails の画面に遷移する。

複数の型に対応した画面遷移

 ここから、上記の例を拡張して、複数の型を扱うことを想定してみる。
Location を同一画面に表示して、LocatonDetails画面に遷移できるようにする。

struct ContentView: View {
    var parks: [Park] = [
        Park(name: "Yosemite"),
        Park(name: "Sequoia"),
    ]

    var locations: [Location] = [
        Location(name: "Cupertino"),
        Location(name: "San Jose"),
    ]

    var body: some View {
        NavigationStack {
            List {
                Section {
                    ForEach(parks) { park in
                        NavigationLink(park.name, value: park)
                    }
                }
                Section {
                    ForEach(locations) { location in
                        NavigationLink(location.name, value: location)
                    }
                }
            }
            .navigationDestination(for: Park.self) { park in
                ParkDetails(park: park)
            }
            .navigationDestination(for: Location.self) { location in
                Text(location.name)
            }
            .navigationTitle(String(describing: type(of: self)))
        }
    }
}

 navigationDestination(for:destination:) は、メソッドチェーンで複数連結し、異なる型の遷移先を定義できる。

 NavigationStack の "Manage navigation state" にあるように、init(path:root:) を使うと画面スタックをprogrammaticに操作できる。
例えば、一度にスタックを2つ重ねて遷移させることで、遷移元と遷移先画面の間に画面を挟んでスタックできる。

データの配列をBindして、ナビゲーション操作

struct ContentView: View {
    (省略)

    @State private var presentedParks: [Park] = []

    var body: some View {
        NavigationStack(path: $presentedParks) {
            List {(省略)}
            .navigationDestination(for: Park.self) { park in
                ParkDetails(park: park)
            }
            .navigationDestination(for: Location.self) { location in
                LocationDetails(location: location)
            }
            .navigationTitle(String(describing: type(of: self)))
            .toolbar {
                ToolbarItem {
                    Button {
                        presentedParks = [
                            Park(name: "Yosemite"),
                            Park(name: "Sequoia"),
                        ]
                        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                            presentedParks.removeAll()
                        }
                    } label: {
                        Text("Yosemite+Sequoia")
                    }
                }
            }
        }
    }
}

 この場合、"Yosemite+Sequoia"ボタンを押すと ParkDetails が2つ重なるスタックで画面遷移した後、全画面 dismiss して元の画面に戻った遷移になる。
presentedParks の配列を操作することで、画面スタックをコントロールすることができるということだ。

 しかし、残念なことに LocationDetailsへの遷移は動かなくなってしまう。

 ここで登場するのが、NavigationPath

A type-erased list of data representing the content of a navigation stack.

とある通り、型消去が使われている。

 NavigationPathNavigationStack.init(path:root:) にバインドして、ナビゲーションを操作する。

struct ContentView: View {
    (省略)

    @State private var path: NavigationPath = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List {(省略)}
            .navigationDestination(for: Park.self) { park in
                ParkDetails(park: park)
            }
            .navigationDestination(for: Location.self) { location in
                LocationDetails(location: location)
            }
            .navigationTitle(String(describing: type(of: self)))
            .toolbar {
                ToolbarItem {
                    Button {
                        path.append(Park(name: "Yosemite"))
                        path.append(Location(name: "Cupertino"))
                        path.append(Park(name: "Sequoia"))
                    } label: {
                        Text("Yosemite+Cupertino+Sequoia")
                    }
                }
            }
        }
    }
}

 これで ParkDetailsLocationDetails の両方に遷移可能。
この場合、"Yosemite+Cupertino+Sequoia"ボタンを押すと ParkDetails > LocationDetails > ParkDetails の順にスタックで画面遷移をした状態になる。

データ型のBind vs NavigationPath

 一見すると NavigationPath で実装しておく方が、後々の拡張性が高そうに見える。
しかし、NavigationPath にも弱点はある。

 前述の通り、NavigationPath は型消去のため、詳細なデータに直接アクセスすることができない。
また、配列操作と違い、任意のindexにアクセスできないため、細やかなスタック操作の面では NavigationPath の分が悪そう。
要件に応じて、使い分けることになる。

まとめ

NavigationStack を用いて、programmaticに画面遷移を操作する方法を、備考録としてまとめた。
pathをencode/decodeすることで、画面のState Restorationも可能になる設計になっているので、非常にかしこい。

iOS 16のことをまとめて知りたいあなたへ

年末年始休暇が終わってしまって時間が足りない方に朗報。
WEB+DB PRESS Vol.132 特集2「iOS 16最前線」には、iOS 16についてのエッセンスが詰まっています!
WEB+DB PRESS Vol.132 特集2 「iOS 16最前線」に寄稿しました #wdpress - がんばってなんか書く

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