株式会社はてなに入社しました
誕生日を迎えた 30回目
id:yutailang0119 の通り、2023年1月19日に誕生日を迎えました。
ついに三十代に突入。
昨年 yutailang0119.hatenablog.com
ところで、数え年じゃないから、迎えた誕生日は 年齢 - 1
では🤔
仕事
実は去年の誕生日直後、2022年2月1日付で「マンガアプリチーム」に異動しました。
前回ブログを書いた頃には、もう異動が決まっていた。
現在は「GigaViewer for Apps」を作っていて、ほぼiOS Developerとしての稼働になっています。
id:ikesyo や
id:kouki_dan を始めとして、大人数になったチームで働いています。
CM
生活
三十代怖い
大阪に引っ越します
京都から大阪に引っ越しをします。
はてなから転職するわけではありません。
京都を離れるのは寂しい。
大阪市内へなので、大移動ではないですが。
永住と思って京都に来たわけではなかったけど、関西歴も長くなってきた...
ゲーム
スプラトゥーン3とポケモンSVを行ったり来たり。
最近、Nintendo Switch(有機ELモデル)をゲットしました。
スプラトゥーン3モデル、かわいい。
try! Swift Tokyo Meetup
明後日 1/21 (土) 開催の try! Swift Tokyo Meetup のオーガナイザーをやっています!
COVID-19の影響もだいぶ緩和されてきたので、準備運動からという感じです。
今年はWWDCも行きたい。
明日から東京出張。
現地で参加される方、お話ししましょう!
最後に
いつものあれです。
ご支援お待ちしております。
引っ越しもあるので、大きいものや冷凍のものは引っ越し後に...
Programmatic NavigationStack 備考録
これははてなエンジニアアドベントカレンダー2022 37日目の記事です。
2回目の登場 id:yutailang0119 です、おはこんハロチャオ~!
昨日は id:hogashi の aタグで#topにリンクするとページ先頭にスクロールするのは仕様 - hogashi.* でした。
冬休みはあつ森で年越ししながら、SwiftUIの2022年OS対応をしていた
待機 #どうぶつの森 #AnimalCrossing #ACNH #NintendoSwitch pic.twitter.com/d9m1vyYzdt
— Yutaro Muta (@yutailang0119) 2022年12月31日
Migrating to new navigation types や NavigationStack
を読んで、"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 }
画面遷移の実装
画面遷移にはNavigationStack
のiOS 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
のスタックを編集
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
をBindして、ナビゲーションを操作
ここで登場するのが、NavigationPath
。
A type-erased list of data representing the content of a navigation stack.
とある通り、型消去が使われている。
NavigationPath
を NavigationStack.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") } } } } } }
これで ParkDetails
と LocationDetails
の両方に遷移可能。
この場合、"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.Range
をJSON encodeした結果は [a, b]
となることがわかった。
これはJSON Arrayを使って表現している。
では、[a, b, c]
な要素数3以上のArrayに対して、Range
のJSON 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:yajimasan の Postfix のサンドボックス環境をローカルに作る でした。
メールサーバー、何もわからない。
今回はWEB+DB PRESSの宣伝をしつつ、執筆事情についてお話します!
- WEB+DB PRESS Vol.121 特集2 「iOS 14最前線」に寄稿しました #wdpress - がんばってなんか書く
- WEB+DB PRESS Vol.116 特集1 「はじめてのトラブルシューティング」に寄稿しました #wdpress - がんばってなんか書く
宣伝
本日 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 PRESSのiOS特集を同じメンバーが担当するのは、初めてのようです。
WEB+DB PRESS Vol.132、どこよりも早い表紙画像です!
— WEB+DB PRESS編集部 (@wdpress) December 7, 2022
オブジェクト指向神話からの脱却、iOS 16最前線、コンテナ化実践ガイドを大特集!12月24日発売です!#wdpress pic.twitter.com/Nvh8dWcFgK
WEB+DB PRESSの書き方
アドベントカレンダーということで、雑誌の特集執筆に興味がある人向けに流れを紹介します。
編集部へのコンタクト
まずは編集部とつながることがファーストステップです。
自分が最初に稲尾さんと知り合ったのは、builderscon tokyo 2017の「WEB+DB PRESS 100号記念特別企画」でした。
知り合いから紹介されて、という流れが多いのではないでしょうか。
WEB+DB PRESSでは持ち込みの投稿も募集しているようですので、詳しくは誌面をご確認ください!
今回はWWDC 22直後の6月中旬に稲尾さんへコンタクトを取りました。
特集テーマ設定
iOS特集は季節性があるトピックのため、おおむね12月か年明けの2月の号で取り上げられることが多そうです。
その他のトピックについては、残念ながら自分が幹事で担当したことがないので、詳しくは語れません。
テーマが決まったら以下を並行して進め、技術評論社での企画会議に備えます。
今回は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/textlintとyutailang0119/action-textlintは必須のツールでした。
ただ、textlintのルール整備が後手に回っていたのは改善したいポイント。
textlint-ja/textlint-rule-preset-ja-technical-writingを使っていますが、WEB+DB PRESSルールに則れていない箇所があるため、専用のルール用意したい。
ツールに限らず、文章の書き方指南が充実していて、これをインプットできるだけでも価値のある経験です。
原稿料と特典
詳細は控えますが、執筆に対して原稿料が支払われます。
我がチームは担当する分量によらず、三等分にしています。
これを海賊スタイルと呼んでいます。
また、掲載号は紙版と電子版両方、それ以降しばらくはどちらかを毎号受け取る特典も付いています。
かなりありがたく、毎号楽しみに読ませていただいています!
学びの機会
本を書くことは時間、労力が大きくかかることです。
前述のリターンが見合うかの受け取り方はさまざまでしょう。
自分は学びの機会を得ることを一つの目標にしています。
毎年、WWDCの内容の9割に目を通すようにしていますが、特集を担当したiOS 14、16の知識が定着している感覚があります。
レビューとして、共著者のアウトプットを最初に読むこともすばらしい特典です。
iOS特集に限ってですが、WWDCの動画をソースにすることも多いです。
自分は担当した章の中でも、以下の動画は何度も見ていたので、話の内容まで覚えるほどでした...
- 第2章 Swift
- 第4章 Swift Charts
学びの機会として、雑誌特集にチャレンジしてみませんか?
Swift.RangeはCodableである、ではencodeフォーマットは?
Swift.Range/ClosedRangeはCodable
Swiftは範囲を表現する型にRange
/ClosedRange
を用意してる。
CountableRange
とCountableClosedRange
というそれぞれのwhere条件を持つtypealiasもあるが、ここでは同じものだと思ってほしい。
Range
/ClosedRange
のBound
が Encodable
/Decodable
である時、Range
/ClosedRange
もEncodable
/Decodable
にconformする。
つまり、Codable
である。
では、Range
/ClosedRange
を JSONEncoder
と JSONDecoder
に通すと、どんなフォーマットになるだろう?
試してみましょう。
※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
Range
を Codable
にconformさせるプロポーザルを教えてもらいました。
Add Codable conformance to Range types
iOSDC Japan 2022にオフライン参加した #iosdc
— Yutaro Muta (@yutailang0119) September 12, 2022
自分自身がずっと溜めていた...
はじめに
- iOSDC Japan 2022にプロポーザル提出した -> 採択された #iosdc - がんばってなんか書く
- iOSDC Japan 2020に参加 & LT登壇しました #iosdc - がんばってなんか書く
- 2021年も参加してるけど、ブログを書いていなかった
登壇
LT枠で5分お話ししました。
立木文彦さんにタイトルコール、名前を呼んでもらったので満足。
オフラン参加
2019年ぶりのオフライン会場にも集まれる形式でした。
生活が変わった人たちも多く、久々に会った人たちと同窓会感があって非常によかった。
はじめましての人とも繋がったり、iOSDC恒例で@shiz3とSwift Concurrencyの立ち話をしたり、終電新幹線に飛び乗る体験をしたり。
新幹線終電ギリギリを久々にやりました… (@ Shinkansen Shinagawa Station in 港区, 東京都) https://t.co/z1VlGqwr70
— Yutaro Muta (@yutailang0119) September 12, 2022
id:gigi-net とは毎晩飲みに行きました。
まとめ
情勢もあるし、絶対はないと思うけど、やっぱりいろんな人と会ってプログラミングだったり、仕事だったり、ゲームだったりの話をできるのは、とても楽しかった。
運営の皆様、お疲れ様でした!