github repo: https://github.com/hrfmmr/swiftui-sandbox
題材
「twsearch」という、Twitter APIのApplication’s only authenticationをつかったシンプルなtweet検索アプリ
モジュール構成
module | type | description |
---|---|---|
twsearch | application | 本体アプリ |
AppFeature | dynamic framework | 各FeaturesをまとめあげるUmbrella的なFeatureモジュール |
SearchStatusFeature | dynamic framework | 検索機能のFeatureモジュール |
APIClient | dynamic framework | Twitter APIクライアント |
SwiftUIHelpers | dynamic framework | SwiftUIのヘルパ |
Core | dynamic framework | ドメインモデル定義など |
Umbrella | static framework | 共通で使用されるライブラリ群がembedされたモジュール |
依存関係
Embedded Frameworkをつかったモジュール構成
-
XcodeGen
project.yml
name: twsearch options: bundleIdPrefix: example.com deploymentTarget: iOS: 15.0 targetTemplates: Application: type: application platform: iOS sources: - path: ${target_name} EmbeddedFramework: platform: iOS type: framework sources: - path: ${target_name} - path: ${target_name} excludes: - Info.plist EmbeddedFrameworkTests: type: bundle.unit-test platform: iOS dependencies: - target: twsearch sources: - path: ${target_name} - path: ${target_name} excludes: - Info.plist buildPhase: resources FeatureFramework: templates: - EmbeddedFramework dependencies: - target: Core - target: APIClient - target: SwiftUIHelpers targets: twsearch: templates: - Application dependencies: - target: AppFeature AppFeature: templates: - FeatureFramework dependencies: - target: SearchStatusFeature SearchStatusFeature: templates: - FeatureFramework SearchStatusFeatureTests: templates: - EmbeddedFrameworkTests Core: templates: - EmbeddedFramework transitivelyLinkDependencies: true dependencies: - target: Umbrella Umbrella: platform: iOS type: framework.static dependencies: - package: swift-composable-architecture product: ComposableArchitecture APIClient: templates: - EmbeddedFramework dependencies: - target: Core APIClientTests: templates: - EmbeddedFrameworkTests dependencies: - target: twsearch SwiftUIHelpers: templates: - EmbeddedFramework schemeTemplates: EmbeddedFrameworkScheme: templates: - TestScheme build: targets: ${scheme_name}: all TestScheme: test: gatherCoverageData: true targets: - ${testTargetName} schemes: twsearch: build: targets: twsearch: all APIClient: templates: - EmbeddedFrameworkScheme templateAttributes: testTargetName: APIClientTests SearchStatusFeature: templates: - EmbeddedFrameworkScheme templateAttributes: testTargetName: SearchStatusFeatureTests packages: swift-composable-architecture: url: https://github.com/pointfreeco/swift-composable-architecture from: 0.33.0
これをベースに移行していく
Swift Packageをつかったモジュール構成への移行
-
Swift Package(モジュール群)とxcodeproj(アプリエントリポイント)双方をXcodeで扱えるように、rootに.xcworkspaceを追加
-
Package.swiftを追加
$ swift package init
- 実行後のrootディレクトリ構成
├── APIClient ├── APIClientTests ├── AppFeature ├── Core ├── Makefile ├── Mintfile └── Package.swift 🆕 ├── project.yml ├── SearchStatusFeature ├── SearchStatusFeatureTests ├── Sources 🆕 ├── SwiftUIHelpers ├── Tests 🆕 ├── twsearch └── twsearch.xcodeproj └── twsearch.xcworkspace 🆕
- 実行後のrootディレクトリ構成
-
App/ディレクトリ下にxcodeprojとエントリポイントとなるファイルを配置
├── App 🆕 │ ├── Package.swift │ ├── project.yml │ ├── twsearch │ │ ├── Assets.xcassets │ │ ├── Info.plist │ │ ├── prefs.plist │ │ ├── Preview Content │ │ └── twsearchApp.swift │ └── twsearch.xcodeproj
- Xcode上からrootのSwift Packageからは認識されないよう、空のPackage.swiftを設置
-
Sources/下にモジュール群を移行
├── Sources │ ├── APIClient │ ├── AppFeature │ ├── Core │ ├── SearchStatusFeature │ ├── SwiftUIHelpers │ └── Umbrella
-
Tests/下にテストターゲット群を移行
├── Tests │ ├── APIClientTests │ └── SearchStatusFeatureTests
-
App/下に.xcodeprojを移行したので、contents.xcworkspacedataのFileRefを編集して参照解決する
<?xml version="1.0" encoding="UTF-8"?> <Workspace version = "1.0"> <FileRef location = "group:App/twsearch.xcodeproj"> </FileRef> <FileRef location = "group:"> </FileRef> </Workspace>
-
rootのPackage.swiftにモジュール間の依存関係を記述
// swift-tools-version: 5.5 import PackageDescription let package = Package( name: "twsearch", platforms: [ .macOS(.v10_15), .iOS(.v15), ], products: [ .library(name: "Core", targets: ["Core"]), .library(name: "APIClient", targets: ["APIClient"]), .library(name: "SwiftUIHelpers", targets: ["SwiftUIHelpers"]), .library(name: "SearchStatusFeature", targets: ["SearchStatusFeature"]), .library(name: "AppFeature", targets: ["AppFeature"]), ], dependencies: [ .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "0.33.0") ], targets: [ .target( name: "AppFeature", dependencies: [ "Core", "SearchStatusFeature", ] ), .target( name: "SearchStatusFeature", dependencies: [ "Core", "APIClient", "SwiftUIHelpers", ] ), .testTarget( name: "SearchStatusFeatureTests", dependencies: [ "SearchStatusFeature", ] ), .target( name: "SwiftUIHelpers", dependencies: [] ), .target( name: "APIClient", dependencies: [ "Core", ] ), .testTarget( name: "APIClientTests", dependencies: [ "APIClient", ] ), .target( name: "Core", dependencies: [ "Umbrella", ] ), .target( name: "Umbrella", dependencies: [ .product(name: "ComposableArchitecture", package: "swift-composable-architecture") ] ), ] )
最終的な構成
.
├── App
│ ├── Package.swift
│ ├── project.yml
│ ├── twsearch
│ └── twsearch.xcodeproj
├── Package.swift
├── Sources
│ ├── APIClient
│ ├── AppFeature
│ ├── Core
│ ├── SearchStatusFeature
│ ├── SwiftUIHelpers
│ └── Umbrella
├── Tests
│ ├── APIClientTests
│ └── SearchStatusFeatureTests
└── twsearch.xcworkspace
├── contents.xcworkspacedata
├── xcshareddata
└── xcuserdata
- Xcode上の見え方
- 最終的なApp/project.yml
name: twsearch options: bundleIdPrefix: example.com deploymentTarget: iOS: 15.0 targetTemplates: Application: type: application platform: iOS sources: - path: ${target_name} targets: twsearch: # 👈 モジュール群をSwift Packageに寄せられたので、Targetはapplicationのみでよくなった templates: - Application dependencies: - package: twsearch product: AppFeature schemes: twsearch: # 👈 schemeも然り build: targets: twsearch: all packages: twsearch: path: .. # 👈 localのSwift Packageを相対パス参照
最終的なモジュール構成図
- xcodeprojで扱うのはapplicationターゲットのみ
ビルド&テスト
(タスクランナに振り切った)Makefile
APP_DIR := App
APP_NAME := twsearch
DESTINATION := "platform=iOS Simulator,name=iPhone 13"
TEST_SCHEMES := SearchStatusFeature
sourcery := Pods/Sourcery/bin/sourcery
xcbeautify := Pods/xcbeautify/xcbeautify
mint-run := mint run
# Set up
default: bootstrap
.PHONY: bootstrap
bootstrap:
mint bootstrap
pod install
"$(MAKE)" build-xcodeproj
# Build
.PHONY: build-xcodeproj
build-xcodeproj:
$(mint-run) xcodegen -s ${APP_DIR}/project.yml
.PHONY: build
build: build-xcodeproj
xcodebuild \
-scheme ${APP_NAME} \
-sdk iphonesimulator \
-destination ${DESTINATION} \
build | ${xcbeautify}
# Test
.PHONY: generate-mocks
generate-mocks:
@find ./Tests -name ".sourcery.yml" -type f | xargs -I {} ${sourcery} --config {}
.PHONY: test
test: generate-mocks
for scheme in ${TEST_SCHEMES}; do \
xcodebuild \
-scheme $$scheme \
-sdk iphonesimulator \
-destination ${DESTINATION} \
test | ${xcbeautify} --report junit; \
done
- Swift標準ライブラリ以外のUIKitなどのiOS SDKへの依存が存在している為、
swift
cliでなくxcodebuild
をつかう swift
で-Xswiftc
オプションでsdkとtargetを指定することも可能- eg.
swift \ build \ -Xswiftc "-sdk" \ -Xswiftc "$(xcrun --sdk iphonesimulator --show-sdk-path)" \ -Xswiftc "-target" \ -Xswiftc "x86_64-apple-ios13.0-simulator"
- なんだけど
swift test
実行するとXCTest
のシンボル解決でコケる🙄(これ原因わからず..)ld: warning: Could not find or use auto-linked framework 'XCTest' Undefined symbols for architecture x86_64: "_OBJC_CLASS_$_XCTestCase", referenced from: _$s14APIClientTests07Twittera11IntegrationB0CN in TwitterAPIClientTests.swift.o
- eg.
ハマりポイント
テストターゲットが依存先ライブラリに紐付かない場合がある
- とは
- たとえばAPIClient schemeにAPIClientTestsがTest actionとして紐付いていないなど
- Edit scheme > Testで左下の「+」からテストターゲットを追加
.swiftpm/xcode/xcshareddata/xcschemes/APIClient.xcscheme
をgit管理下に置く--- a/.swiftpm/xcode/xcshareddata/xcschemes/APIClient.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/APIClient.xcscheme @@ -28,16 +28,6 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "APIClientTests" + BuildableName = "APIClientTests" + BlueprintName = "APIClientTests" + ReferencedContainer = "container:"> + </BuildableReference> + </TestableReference> </Testables> </TestAction> <LaunchAction
所感
- なにがうれしいか
- XcodeGenのproject.ymlの「依存解決」部分を 標準に乗っかるカタチで 分離できるようになった
- TODO
- assetsのリソース管理とかその辺
- SwiftPM非対応/private repoライブラリとの付き合い方