文字っぽいの。

文字を書いています。写真も混ざります。

横断型テックリードという働き方

f:id:FromAtom:20220208042049p:plain みなさんこんにちは。FromAtomです。

自分は今『モバイルアプリ分野テックリード』という肩書で仕事をしています。世の中のテックリードの皆様におかれましては「"分野" テックリードって?」「横断型テックリードって?」という感じかと思います。そんなテックリード業を2019年末から2年ちょっとやってきたので、この記事では自分がやっていることの説明と、分野テックリードが置かれた経緯を紹介します。

なぜモバイルアプリ分野テックリードが必要になったのか

弊社では、全部で12つのiOSAndroidアプリを開発しています。これだけあると、各アプリのプロダクトオーナー毎に意思決定の精度にばらつきが出てきます。例えば、Appleのレビューリジェクトに対する姿勢が異なると「まぁ適当にごまかして通せば勝ちや!」というチームと「BANリスクもあるからちゃんとやるか。面倒だけど。」というチームが生まれる可能性があります。

Apple的にはアプリが別でも提供してる会社が同じなら同じ扱いをするので、1つのアプリのレビュー対応が粗野なせいで巻き添えを食って会社のアカウント自体BANされ、全アプリ死亡してしまう可能性もあります。ここまで極端なズレはないんですが、なにも手を打たなければここに至ってしまう可能性もあるわけです。こういったズレを無くして全社的に意思決定を担う役割が必要になります。

また、Webからサービスインした企業の多くは、Web技術をメインとしていたエンジニアがCTOをしていることが多いと思います。「Web屋にスマホアプリが分かるかよ!」みたいな話がしたいわけではなくて、ひとりの人間がカバーできる技術範囲には限界があるということです。

スマホアプリという技術領域は単純なプログラミング能力だけでなく、AppleGoogleというプラットフォーマーによる制約をどのように解決していくかという能力も必要になります。例えばアプリのリジェクトに対する対応方法でも、「レビュアーがこういう文面でリジェクトしてくる場合の対応はこう」といった、ググっても出てこない経験値が効いてくる場面が多々あります。また、WebでGDPR対応が大変なように、iOSでのATTやAndroidでのData Safety Sectionなど、担当のアプリエンジニアだけでは解決が難しい課題もしばしば発生します。

そのため、モバイルアプリ分野テックリードというミニCTOのような役割を設けることで、技術や経験に基づく判断や全社的な方針決定を素早く行うことが可能になります。

ぶっちゃけ弊社のCTOは頭良くてバチバチに優秀なので、自分がいなくてもスマホ分野も余裕でカバーできそうなのですが、いかんせん『人間の身体を持っている』という弱点があります。24時間365日寝食休憩排泄皆無で働けるのであれば良いのですが、人間はそれをすると死んでしまいます。そういう意味では、適切に役割分担ができていると感じています。

事業部型テックリードと横断型テックリード

f:id:FromAtom:20220208040424p:plain

弊社には事業部型のテックリードと横断型のテックリードの2タイプのテックリードがいます。事業部型テックリードは、各事業部もしくは部に所属しています。例えばプロダクトAやサービスBのテックリードみたいな感じですね。世の中的にはこちらが一般的なテックリードだと思います。プロダクトやサービスとしての技術的戦略と、会社全体としての技術的戦略でブレがないようにすり合わせ、その思想や方針をチームに浸透させ牽引していく役割を担うことが多いでしょう。そのため、事業部型のテックリードはプロダクトオーナーやマネージャーと密に連携して、目指すべき方向性を示したり、将来を見据えた先行投資を技術面から行っていきます。

このタイプのテックリードについてはこちらの記事が参考になります。

テックリードという役割 | by Shimpei Takamatsu | Medium

一方で横断型テックリードは特定のプロダクトやサービスに所属せず、特定の技術領域を担当しています。技術領域型テックリードとも言い換えられそうです。モバイルアプリ分野のテックリードはこれにあたります。特定のプロダクトやサービスに属さないため、サービスの成長戦略・人材の育成・キャリアパス・人事評価などには関与しません。特定の技術領域(例えばアプリ)に携わるエンジニア全員が、統一された意思決定方針をもって働けるように、技術的な方針を言語化し布教する仕事をします。

モバイルアプリの横断型テックリードがやってること

アプリに関する技術的な相談窓口

自分で答えられる範囲なら答えたり、明るくない分野については「Aさんが詳しいですよ」と伝えています。リジェクトに対する対応方針とATT関係の相談が多いです。また、経営判断が必要だったりセキュリティ上の懸念がある場合は、然るべき役職へのエスカレーションも行います。

言語化・文章化

アプリエンジニア目線から見た常識を、経営層やマネジメント層にも分かるように言語化・文章化しています。今までに文章化した内容としては、

  • 人員計画
  • 中長期的技術戦略
  • アプリを作る・作らないの判断基準
  • レビューリジェクトとの向き合い方
  • PWAがネイティブアプリの代替にならない理由とPWAの活かし方
  • なぜ弊社はネイティブアプリで開発をするのか

などがあります。入社すると読めます。

スマホアプリエンジニア全員1on1

弊社には10人程度1の正社員アプリエンジニアがいるのですが、半年毎に全員と1on1をしています。普通のキャリアや悩み系1on1はチームのマネージャーやメンターが行っているのため、この1on1では、スマホアプリ開発面での困り事をヒアリングすることを主目的にしています。

例えば、「ビルドが遅くて……」と言われたらPCのスペックを聞き、不足しているようなら2両手を振り回しながらマネージャーに「こいつに新しいのを買ってあげてください!メモリはなにも考えずに一番多いので!」みたいな動きをしています。

法律・プライバシー・レビューガイドラインプラットフォーマー対応

多分ここが一番大きい業務領域です。わかりやすい例ではATTやData Safety Sectionなどで、提出する情報を揃えるために奔走します。なにぶんアプリ数が多く、それに伴ってステークホルダーも増えるため、説明・調整・調査・ヒアリングをぶん回しまくることになります。

他には、どこかの国が法律を変えると、それに伴ってAppleGoogleが書類提出やフォーム入力を求めてくるのでその対応方針を決めたり、レビューガイドラインの変更に伴った要件定義と各所への実装依頼を出したりします。

最近では、Appleがレビューガイドラインでアプリ内にアカウント削除機能を求めてきたので、その対応方針やリジェクトリスクの検討をし、実装方針をあちこちと相談して決めたりしてました。余談ですが、Appleの指定した時期に間に合うように急いであれこれ調整したのに、結局直前で延長されたので「おまえおまえおまえぇ!」ってキレました。

モバイルアプリの横断型テックリードがやらないこと

アプリの機能変更に関する意思決定

アプリに新しく機能を加えたり、削除したりといった意思決定は、そのアプリを開発しているプロダクトオーナーが意思決定するべきです。自分はあくまでも「なにか困ったら聞いてね」という存在でいます。もちろん「それリジェクトリスクが高いですよ」といったものは口をだすようにしています。

マネジメント

テックリードはマネージャーではありません。あくまでも現場にいるチームメンバーの目線から、プロジェクトやチームをゴールに向けて技術面でリードしていくのが、テックリードの役割です。そのため、給与に関する評価を行ったり、エンジニアの抱える技術的ではない問題を取り除くのは、役割の外になります。もしその仕事をテックリードがするのであれば、マネージャーが不要になってしまいますからね。

なおかつ、横断型テックリードにはマネージャー権限・人事権がありません。「横断的に特定職種のエンジニアに関わる諸問題を解決してくれる」と勘違いされがちですが、実はそこまでの権限がないので動けません。各事業部のアプリに関する問題や事業部をまたいだタスクの依頼、人員の配置などの問題を主導して解決するのはあくまでも事業部です。これを自分が行ってしまうと、コミュニケーションパスがハチャメチャになってしまうので、意識してやらないように気をつけています。

もしその仕事を横断型テックリードに求める場合、関係するエンジニアを全員集めて1部署を作る必要があります。たとえば「アプリ部」みたいなものを作り、テックリードがアプリ部エンジニアリングマネージャーとして働く形になると思います。こうすれば、アプリ分野テックリードがマネージャー権限を得られるためアプリエンジニアのタスク管理をしたり、各部署へのリソース配分を調整することができます。ただ、弊社的にはこの職能縦割り方式はまだ規模的に早いので、現時点で導入する気はありません。

一方で、「アプリ基盤」のような部署を作るのはありかもと考えています。プロダクト開発を行うエンジニアとは別に人員を確保して、各プロダクトの開発速度を底上げしていく感じですね。その人員を確保するのが難しいのですが……。

キャリアパスやチャレンジの支援

すべての事業部に所属しているわけではなく、情報が不足しすぎているため実質不可能です。というか、ここらへんの領域は事業部型テックリードやマネージャーが考えてくれているので、自分がやる必要はないんですよね。もちろん、自発的に動いていないだけなので、相談されたら1on1などをしています。

なんかすごいライブラリとか作る

基本的には自分で手を動かすのではなく、作りたい人が作れる環境構築をするのがメインだと考えています。

仮説・検証方法・実装の妥当性やビジネス面での利点などを確認した上で、CTOや偉い人にかけあったり、必要な物資を調達します。もちろん、かけるコストに対する対価が得られることが前提ですが、そこらへんをいい感じにする仕事もします。事業部型テックリードがやってくれる場合もありますが、判断が難しい場合もあります。

まとめ

モバイルアプリ分野テックリードという横断型テックリードの紹介と、生まれた経緯を紹介しました。スマホアプリ開発はコードを書く以外の面倒事があまりにも多いです。その面倒事をなるべく引き受けることで、スマホアプリ開発者がアプリの改善に集中できる環境を作ることを目指してテックリード業を行っています。

もし、こういった環境で働いてみたいという方や、自分がテックリードやってやるぜという方がいましたらTwitter@FromAtom までご連絡ください。ただ話を聞いてみたい方や、カジュアル面談(突然面接が始まるタイプではない)をしてみたいという方もぜひぜひ〜。

最後に、文字ばかりの記事を読んでいただいて、ありがとうございました。


  1. マネジメント挑戦中だったり、ジョブチェンジ中だったりもしてぼやっとした数字です。

  2. 入社時に最新PCが得られるんですが、遠慮したのか中くらいのスペックで買ってしまう人がいて、そのまま言い出せない場合があるんですよね。

WordPressにOAuth認証してRubyでREST APIを叩いて記事投稿する。

やりたいこと

  • WordPressREST APIを使って記事を更新したい
  • 認証方法にはBasic認証ではなく、OAuthを使いたい
    • 公式ドキュメントでも開発やテスト以外でBasic認証は使うなと言っている
    • Note that this plugin requires sending your username and password with every request, and should only be used for development and testing i.e. not in a production environment.

手順

OAuth用のプラグインを入れる

wordpress.org

このプラグインを入れるだけでOK

アプリケーションを登録する

WP管理画面のメニューの[ユーザー] > [Applications] を開く

f:id:FromAtom:20220129180754p:plain

こういった画面が表示されるので、中身を入力していく。

f:id:FromAtom:20220129181004p:plain

  • Consumer Name
    • アプリの名前。なんでも良い。
  • Description
    • わかりやすい説明。自分で後からなにに使っているか分かるように書いておく。
  • Callback
    • 自分のWPサイトのドメインに変更してCallback先を指定しておく。
    • 例えば https://example.com/ というドメインなら https://example.com/successhttps://example.com/callback などと指定する。successcallback はかぶらなければ何でも良さそう
    • 後で使うのでメモっておく

入力が終わって Add Consumer ボタンを押すと

f:id:FromAtom:20220129181536p:plain

Client KeyClient Secret が表示される。これは後で使うのでメモっておく。他人には知られないようにする。

RubyでAccessTokenを取得する

Gemfileはこう

source "https://rubygems.org"

gem "oauth"

メインのコードはこんな感じ

require 'oauth'
require 'readline'

CONSUMER_KEY = 'XXXXX'
CONSUMER_SECRET = 'XXXXXXXXXXXX'

WP_OAUTH_ENDPOINT = 'https://[あなたのWPドメイン]/'
REQUEST_TOKEN_PATH = '/oauth1/request'
AUTHORIZE_PATH = '/oauth1/authorize'
ACCESS_TOKEN_PATH = '/oauth1/access'
CALLBACK_URL = 'https://[あなたのWPドメイン]/success'

consumer = OAuth::Consumer.new(CONSUMER_KEY, CONSUMER_SECRET, :site => WP_OAUTH_ENDPOINT, :request_token_path => REQUEST_TOKEN_PATH, :authorize_path => AUTHORIZE_PATH, :access_token_path => ACCESS_TOKEN_PATH)
request_token = consumer.get_request_token(oauth_callback: CALLBACK_URL)

puts '下記のURLにアクセスして認証を行う。`oauth_verifier` というクエリパラメータがあるのでコピペして入力する。'
puts request_token.authorize_url

oauth_verifier = Readline.readline("oauth_verifier >")

access_token = request_token.get_access_token(:oauth_verifier => oauth_verifier)

puts "token: #{access_token.token}"
puts "secret: #{access_token.secret}"

メモっておいた CONSUMER_KEY, CONSUMER_SECRET, CALLBACK_URL を入力してから実行する。

REQUEST_TOKEN_PATH などでPathを変えているが、これは https://[あなたのWPドメイン]/wp-json にアクセスすると取得できるJSONの "authentication" > "oauth1" に記載されているので、うまく動かない場合は確認すると良い。

このコードを実行するとTokenとSecretが取得されて表示されるので、あとはそれを利用してREST APIを叩けば良い。

WPのREST APIを叩いて新規投稿する

上で生成した consumeraccess_token をそのまま利用する。

require 'typhoeus'
require 'oauth/request_proxy/typhoeus_request'

hydra = Typhoeus::Hydra.new
uri = 'https://[あなたのWPドメイン]/wp-json/wp/v2/posts/'
oauth_params = { consumer: consumer, token: access_token, request_uri: uri }

data = { title: "タイトルです", content: "記事の本文です" }
req = Typhoeus::Request.new(
    uri,
    method: :post,
    params: data
)

oauth_helper = OAuth::Client::Helper.new(req, oauth_params.merge(request_uri: uri))
req.options[:headers]["Authorization"] = oauth_helper.header
hydra.queue(req)
hydra.run
response = req.response

これで新しい記事が下書き状態で追加される。最初から公開状態にしたり、CategoryやTagを追加することもできる。詳しいAPIの使い方やパラメータは、公式のハンドブックを見ると良い。

developer.wordpress.org

SwitchBotを使って、加湿器の水を入れ忘れないようにした。

モチベーション

加湿器、水を入れ忘れると乾燥した部屋でMTGなどを続けることで喉が破滅しがち。忘れずに水を入れていきたい。

できたもの

f:id:FromAtom:20220130181709p:plain

こんな感じで、湿度が30%未満だったらSlackに通知されるようにした。加湿器に水が入っていれば30%は超える家の環境なので、30%をしきい値にしてある。

使ったもの

SwitchBot温湿度計で計測した値をHub Mini経由でAPIを叩けるようにしている。Hub Miniがないと温湿度計の値はアプリのBluetooth経由でしか取得できないので、APIで値を取りたい場合は必要になる。

セットも売ってるので、SwitchBot Hubがない場合はこっちを買うと便利。

構成

APIを叩いて、湿度を確認して、SlackにWebhookで通知するというスクリプトRubyで書いて、それをHerokuでHeroku Schedulerを利用して定期実行しているだけ。

APIの使い方は下記記事が参考になるので、こちらを見てもらうと良い。

memo-nikki.info

自動で泡のハンドソープが出てくるやつを買った。

これを買った。今までは手動でハンドソープを出していたけども、毎日何度もプッシュするのが面倒だったので購入。

この商品を選んだ理由としては、

  • 充電式
    • 乾電池入れ替えるの面倒なので
    • バッテリーがへたる頃には本体自体がへたって買い替えだろうなと思ってる
  • 出てくる泡の量が調整できる
    • 「全然少ない」とかなりそうなので、調整できるのが良かった
  • ボトル容量が大きい
    • 詰め替えは面倒なので大量に入れておきたい
  • 詰替しやすい
    • ボトルを外して取って流し込むだけなので簡単
    • 口も広いので良い
  • ホワイトでシンプルな形でシルバーが入っていない
    • キラキラかっこいいって要素は不要なので

やっぱり、自動で丁度いい量のハンドソープが出てくるのは便利。

Amazonレビュー見てると数ヶ月で壊れたってのがチラホラあるので、どうなるかなと思いながら使っていきます。

複数のライブラリを含むPackageをSwiftPMで利用する方法。

環境

  • Xcode 13.2.1
  • Swift 5.5.2

やりたいこと

The Composable ArchitectureFirebase iOS SDK のように、XcodeのPackage Dependencies経由で入れるとこんな感じで複数のライブラリが内包されていて、使いたいものを選択して利用するタイプのライブラリ。

f:id:FromAtom:20220104201200p:plain

これを Package.swift でも選択して利用したい。

やりかた

たとえば、The Composable ArchitectureのComposableArchitectureだけを利用したい場合は、こうやって Package.swift に書けば良い。

import PackageDescription

let package = Package(
    name: "SamplePackage",
    platforms: [.iOS(.v13)],
    products: [
        .library(
            name: "SamplePackage",
            targets: ["SamplePackage"]),
    ],
    dependencies: [
        .package(name: "swift-composable-architecture", url: "https://github.com/pointfreeco/swift-composable-architecture", .upToNextMajor(from: "0.32.0"))
    ],
    targets: [
        .target(
            name: "SamplePackage",
            dependencies: [
                .product(name: "ComposableArchitecture", package: "swift-composable-architecture", condition: .when(platforms: [.iOS]))
            ])
    ]
)

SwiftでLatoフォントを簡単に使えるLatoSwiftをつくりました。

f:id:FromAtom:20220104023507p:plain

作ったもの

github.com

できること

UIKitやSwiftUI製のアプリで、簡単にLatoフォントを扱えるようになります。SwiftUIでも使えるので、macOS向けにSwiftUIアプリを作った場合でも利用可能です。

for UIKit

import LatoSwift

let latoRegularFont = Lato.uiFont(ofSize: 16)
let latoBoldFont = Lato.uiFont(ofSize: 16, weight: .bold)
let latoLightItalicFont = Lato.uiFont(ofSize: 16, weight: .light, style: .italic)

for SwiftUI

import SwiftUI
import LatoSwift

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Lato Regular")
                .font(Lato.font(ofSize: 16))
            Text("Lato Bold")
                .font(Lato.font(ofSize: 16, weight: .bold))
            Text("Lato Light Italic")
                .font(Lato.font(ofSize: 16, weight: .light, style: .italic))
        }
    }
}

こんな感じで簡単にLato指定で表示できます。

f:id:FromAtom:20220104024448p:plain

フォント(.ttf)も一緒に入ってくるので、使うときはこのライブラリを導入するだけで済みます。

なお、現時点では自分しか使わなさそうなのでSwiftPMのみの対応です。

注意点

READMEにも書いてありますが、LicensePlistでライセンス表記を自動生成している場合、LatoSwiftのライセンス(MIT)は読み込まれますが、Latoフォント自体のライセンス(SIL Open Font License)を読み込んでくれません。license_plist.yml を自分で用意して、ライセンス表記を追加してあげて下さい。

地味に面倒なので自動で読み取らせたいのですが、どうにもやり方が分からなくて断念しました。誰か詳しい人がいたら教えて下さい。1

モチベーション

副業や個人アプリでLatoフォントを使うことがままあるのですが、毎回Fontファイルをダウンロードして、読み込み用のコードやそれっぽい仕組みを用意するのが面倒だったので作りました。

また、SwiftPM中心のプロジェクト構成にしていると、Asset周りで地味に面倒な感じになったので「これならライブラリ化しちまえ」となったわけです。

fromatom.hatenablog.com


  1. LICENSEファイルに2つ分のライセンスを書いてしまえば良さそうだけど、それでいいのかよく分かっていない。

SwiftPMでAssetsを含んだtargetを別のtargetで呼んでSwiftUI Previewするとクラッシュする。

わけわからんタイトルだけど、そのままなので仕方ない。

環境

  • Xcode: 13.2.1
  • Swift: 5.5.2

背景

iOSDC 2021でこの発表をみて、SwiftPM中心のプロジェクト構成にしようとする人は多いだろう。

fortee.jp

自分もその一人で、その中ではまった問題。

SwiftPM中心のプロジェクト構成にするとMulti Module化がしやすくなるので、機能ごとや画面ごと(プロジェクトの設計によるが)にModule(Package.swiftでいうTargets)を分割していく。

そうすると自然と .xcassets も分割されていく。上記発表ではBundleに Bundle.module を指定しましょうという話がされていて、基本的にはそれでうまくいく。しかし、SwiftUIのPreviewを使おうとすると特定条件でPreviewができなくなる。

条件とエラー

下記のコードをまとめたリポジトリはこちら

github.com

こういう Package.swift 構成になっている。

let package = Package(
    name: "ImageAssetPackageSample",
    platforms: [.iOS(.v15)],
    products: [
        .library(
            name: "ImageAssetPackageSample",
            targets: ["ImageAssetPackageSample"]),
    ],
    dependencies: [
    ],
    targets: [
        .target(
            name: "ImageAssetPackageSample",
            dependencies: ["ImageAsset"]),
        .target(
            name: "ImageAsset",
            dependencies: [])
    ]
)

この中で、ImageAssetが.xcassetsを持っていて、

public struct ImageAsset {
    public static let blueSquare: UIImage = UIImage(named: "BlueSquare", in: Bundle.module, compatibleWith: nil)!
}

というコードで画像が得られるようになっている。ここで、ImageAssetPackageSample というTargetでSwiftUIのViewを作ってPreviewをすると

unable to find bundle named ImageAssetPackageSample_ImageAsset

----------------------------------------

CrashReportError: XCPreviewAgent crashed

(中略)

ImageAsset/resource_bundle_accessor.swift:27: Fatal error: unable to find bundle named ImageAssetPackageSample_ImageAsset

(後略)

というエラーでクラッシュしてPreviewができない。一方でImageAssetを依存に含んだImageAssetPackageSample を利用して、Project側のアプリコードでSwiftUI Previewを利用すると普通に表示できる。

つまり、

  • .xcassets を含んだTargetを別のTargetで利用してPreviewする:NG
  • .xcassets を含んだTargetをProject内のコードでPreviewする:OK

となっている。

原因

SwiftUI Previewer crashes while in swift package that depends on another's packages Bundle.module reference - Using Swift - Swift Forums

Bundle.module がPackageのPreview時には生成されずにNot foundでクラッシュするらしい。

対策

ワークアラウンドなコードを書く

このエラーで調べると出てくるけど、自分でBundle.moduleに相当するものを書いてあげれば動く。

public let imageBundle = Bundle.myModule
private class CurrentBundleFinder {}
extension Foundation.Bundle {
    static var myModule: Bundle = {
        /* The name of your local package, prepended by "LocalPackages_" for iOS and "PackageName_" for macOS. You may have same PackageName and TargetName*/
        let bundleNameIOS = "LocalPackages_TargetName"
        let bundleNameMacOs = "PackageName_TargetName"
        let candidates = [
            /* Bundle should be present here when the package is linked into an App. */
            Bundle.main.resourceURL,
            /* Bundle should be present here when the package is linked into a framework. */
            Bundle(for: CurrentBundleFinder.self).resourceURL,
            /* For command-line tools. */
            Bundle.main.bundleURL,
            /* Bundle should be present here when running previews from a different package (this is the path to "…/Debug-iphonesimulator/"). */
            Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent(),
            Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent(),
        ]
        
        for candidate in candidates {
            let bundlePathiOS = candidate?.appendingPathComponent(bundleNameIOS + ".bundle")
            let bundlePathMacOS = candidate?.appendingPathComponent(bundleNameMacOs + ".bundle")
            if let bundle = bundlePathiOS.flatMap(Bundle.init(url:)) {
                return bundle
            } else if let bundle = bundlePathMacOS.flatMap(Bundle.init(url:)) {
                return bundle
            }
        }
        fatalError("unable to find bundle")
    }()
}

Providing XCAssets Folder in Swift… | Apple Developer Forums

ただし、Multi Module化して、細かく画面を分けてTargetが分割されると、無限にこのコードを書いていく事になって不毛感がすごい。

ミニアプリを作る

Target内でPreviewせずに、Project側で参照してPreviewすることはできるので、Module毎にPreview用のミニアプリを作っていく。SwiftPM中心のプロジェクト構成にしていれば、Projectを増やすことが容易なのでわりと許容できる対応な気もするが、コードを書いている場所とPreviewする場所が別になるので地味に不便。

SwiftUI Previewを使わない

諦めて全部ビルドしてシミュレーターで確認してしまう。ミニアプリと大体同じ解決方法。

Asset系を1つのTarget(Module)にまとめてしまう

筋が悪い感はすごいが、ワークアラウンドをTarget毎に書かなくても良くなる。ただし、どのAssetがどのTargetで利用されているか分からなくなるので、将来的に負債になることが明白。あまり取りたくない選択肢。

FontやColorだけならまだいいかなぁ……という感覚。

まとめ

実は別の解決方法があるかもしれないので、知っている人がいたら教えてほしい。できれば、Xcode側で解決してほしいなー。