Skip to content

Commit 4b72f06

Browse files
committed
initial proj
1 parent 3d88978 commit 4b72f06

File tree

42 files changed

+1142
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1142
-1
lines changed

.github/workflows/deploy_docc.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Deploy DocC
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches:
7+
- main
8+
paths-ignore:
9+
- README.md
10+
- .gitignore
11+
12+
concurrency:
13+
group: "pages"
14+
cancel-in-progress: true
15+
16+
jobs:
17+
build:
18+
runs-on: macos-14
19+
env:
20+
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Build DocC
26+
run: |
27+
$(xcrun --find docc) convert \
28+
--transform-for-static-hosting \
29+
--hosting-base-path "" \
30+
--output-path ./docs \
31+
Sources/SwiftUIViewCodingGuidelines/Guidelines.docc
32+
33+
- name: Upload artifact
34+
uses: actions/upload-pages-artifact@v3
35+
with:
36+
path: docs
37+
38+
deploy:
39+
runs-on: ubuntu-latest
40+
needs: build
41+
permissions:
42+
pages: write
43+
id-token: write
44+
environment:
45+
name: github-pages
46+
url: "https://cybozu.github.io/swiftui-view-coding-guidelines/documentation/swiftuiviewcodingguidelines/"
47+
48+
steps:
49+
- name: Deploy to GitHub Pages
50+
id: deployment
51+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ playground.xcworkspace
2727
#
2828
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
2929
# hence it is not needed unless you have added a package configuration file to your project
30-
# .swiftpm
30+
.swiftpm
3131

3232
.build/
3333

Package.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// swift-tools-version: 5.10
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "SwiftUIViewCodingGuidelines",
8+
products: [
9+
// Products define the executables and libraries a package produces, making them visible to other packages.
10+
.library(
11+
name: "SwiftUIViewCodingGuidelines",
12+
targets: ["SwiftUIViewCodingGuidelines"]),
13+
],
14+
targets: [
15+
// Targets are the basic building blocks of a package, defining a module or a test suite.
16+
// Targets can depend on other targets in this package and products from dependencies.
17+
.target(
18+
name: "SwiftUIViewCodingGuidelines"),
19+
.testTarget(
20+
name: "SwiftUIViewCodingGuidelinesTests",
21+
dependencies: ["SwiftUIViewCodingGuidelines"]
22+
),
23+
]
24+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# ForEachのIDにはuniqueな値を使う
2+
3+
`ForEach`のIDには描画範囲を特定させるためにuniqueな値を使用します。
4+
5+
## Overview
6+
7+
`ForEach`のIDは繰り返しのうちどの要素であるかを特定するために利用されるView Identityです。
8+
IDが変化すると値の変化の有無にかかわらず描画がアップデートされます。
9+
適切なIDを与えることで不要な描画アップデートを防ぎ、良いパフォーマンスのViewを作成できます。
10+
11+
### IDの有効期間
12+
IDにはViewのライフタイムより長い有効期間を持つuniqueな値を使用します。
13+
例えば表示に用いるデータのArrayのindexはuniqueですが、先頭への要素の追加などの操作によって変化するため、Viewのライフタイムより短い有効期間となります。
14+
ArrayのindexをIDとして用いると、先頭に要素が追加された場合に全ての要素で描画をアップデートすることとなり、パフォーマンスに悪影響を及ぼします。
15+
16+
## See Also
17+
- <doc:WhatISViewIdentity>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Viewのスタイル変更は値の変更で行う
2+
3+
条件によってViewのスタイルを変更する場合は値を変更します。
4+
5+
## Overview
6+
7+
条件によってViewのスタイルを変更する場合、不要な再描画を発生させないよう注意する必要があります。
8+
`if`によるView全体の切り替えはコード上で表現される通りView全体が差し替えられます。
9+
これは全体を完全に再描画することになり、本来不要な計算コストが発生します。
10+
11+
### 値の変更でスタイルを切り替える
12+
13+
単純なスタイルの変更の場合、三項演算子で値の変更を行います。
14+
15+
```swift
16+
struct ContentView: View {
17+
var body: some View {
18+
Text("Hello!")
19+
.foregroundStyle(condition ? .teal : .red)
20+
}
21+
}
22+
```
23+
24+
三項演算子で表現が難しい場合は関数などを用いて値を変更します。
25+
26+
```swift
27+
enum TrafficSignal {
28+
case go
29+
case caution
30+
case stop
31+
}
32+
```
33+
```swift
34+
extension TrafficSignal {
35+
var lightColor: some ShapeStyle {
36+
switch self {
37+
case .go: Color.green
38+
case .caution: Color.yellow
39+
case .stop: Color.red
40+
}
41+
}
42+
}
43+
```
44+
```swift
45+
struct ContentView: View {
46+
@State var signal: TrafficSignal
47+
48+
var body: some View {
49+
Text("Hello!")
50+
.foregroundStyle(signal.lightColor)
51+
}
52+
}
53+
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# 利用できる時はopacityでViewを隠す
2+
3+
レイアウト上選択できる場合は`.opacity(_:)`でViewを隠します。
4+
5+
## Overview
6+
7+
Viewを条件によって隠す必要がある場合に選択肢が複数あります。
8+
`.opacity(_:)`を使うと三項演算子でViewを隠すことができるため、`.hidden()`より効率が良くなります。
9+
10+
```swift
11+
var body: some View {
12+
Text("Hello World")
13+
.opacity(condition ? 0 : 1)
14+
}
15+
```
16+
17+
> <doc:ChangeValueWhenSwitchingStyle>
18+
19+
ただし、レイアウト上の必要性を考慮して`if`文での切り替えを選択する場合があります。
20+
21+
### 他のViewの配置への影響を考慮する
22+
`.opacity(_:)`によるViewの削除は、他のViewの配置へ影響を与えません。
23+
24+
例えば、`VStack`の中で`.opacity(_:)`による削除を行なった場合、表示するために確保されたスペースは削除後も確保されたままとなります。
25+
ユーザー名とパスワードを入力するフォームでエラーメッセージを出す時に、エラーメッセージの有無で全体のレイアウトを変えない時などに利用します。
26+
27+
また、注文フォームで配送先と請求先に異なる住所を使用することを選択した場合に、2つ目の住所フィールドを表示することもできます。
28+
住所欄のようなスクロールが必要なコンテンツには、`if`文を使用してコンテンツが表示されているときだけスペースを確保し、他のコンテンツは表示状態に従って移動するようにします。
29+
30+
31+
> [Choosing the right way to hide a view](https://developer.apple.com/tutorials/swiftui-concepts/choosing-the-right-way-to-hide-a-view)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Proxyを通じてContent Viewにアクセスする
2+
3+
Container ViewからContent Viewの値を取得したり、Content Viewの操作を行う際はProxyを提供します。
4+
5+
## Overview
6+
7+
Container Viewで任意のコンテンツに対して実行中の値を取得したり、programmaticallyに操作を行いたいことがあります。
8+
この時`Proxy`を提供することで利用側は任意のタイミングでアクセスできます。
9+
標準では[`GeometryReader`](https://developer.apple.com/documentation/swiftui/geometryreader)[`ScrollViewReader`](https://developer.apple.com/documentation/swiftui/scrollviewreader)で行われています。
10+
11+
### Proxyを通じてprogrammaticallyにContent Viewを操作する
12+
13+
[WebUI](https://github.com/cybozu/WebUI)の例を見てみましょう。
14+
15+
```swift
16+
struct ContentView: View {
17+
var body: some View {
18+
WebViewReader { proxy in
19+
WebView()
20+
.onAppear {
21+
proxy.load(request: URLRequest(url: URL(string: "https://www.example.com")!))
22+
}
23+
}
24+
.padding()
25+
}
26+
}
27+
```
28+
29+
`WebView``load`をprogrammaticallyに行うために、`proxy`が提供されています。
30+
この場合、`WebView`の構築が終わった後に特定のURLを`load`したいので、`onAppear`のタイミングで`load`を発火するために`proxy`を使っています。
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Viewの重ね合わせの実装を選択する
2+
3+
レイアウトのサイズが決定される状況に応じて、Viewの重ね合わせの実装を選択します。
4+
5+
## Overview
6+
7+
Viewの重ね合わせには`ZStack``.overlay`/`.background`を使うことができます。
8+
最終的なサイズを含まれる全ての子Viewの集計から決定する場合は、`ZStack`を選択します。
9+
レイアウトのサイズがビュー1つだけで決まる場合は、modifierを選択します。
10+
11+
### 重ね合わせにModifierを使う
12+
例えば、以下のコードは`ProfileDetail`ビューを`Image`ビューに重ね合わせます。
13+
```swift
14+
struct ProfileViewWithOverlay: View {
15+
var body: some View {
16+
VStack {
17+
Image("ProfilePicture")
18+
.resizable()
19+
.aspectRatio(contentMode: .fit)
20+
.overlay(ProfileDetail(), alignment: .bottom)
21+
}
22+
}
23+
}
24+
```
25+
26+
## See Also
27+
- [Building layouts with stack views - Add depth in alternative ways](https://developer.apple.com/documentation/swiftui/building-layouts-with-stack-views#Add-depth-in-alternative-ways)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Viewのアップデートをコンソールに出力する
2+
3+
`._printChanges()`でViewのアップデートをコンソールに出力します。
4+
5+
## Overview
6+
`Self._printChanges()`を呼び出してViewのアップデートをコンソールに出力できます。
7+
8+
```swift
9+
var body: some View {
10+
let _ = Self._printChanges()
11+
// View code
12+
}
13+
```
14+
15+
`._printChanges()`はどのプロパティがビューの更新を引き起こしたかを記録し、その情報をコンソールに送信します。
16+
17+
## See Also
18+
19+
- [Debugging an App Playground using the Console - Understand when and why your views change](https://developer.apple.com/documentation/swift-playgrounds/console-print-debugging#Understand-when-and-why-your-views-change)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# コンテンツが利用できないケースをメインケースと分離する
2+
3+
コンテンツが利用できないケースをメインのケースとコードブロックごと分離し、可読性を高めます。
4+
5+
## Overview
6+
7+
リストに表示するアイテムが0件になったり、サーバーとの通信エラーで取得できなかったりして表示すべきコンテンツが利用できないことがあります。
8+
このようなケースでは`.overlay``ContentUnavailableView`を使って主要なケースとコードブロックを分離することで可読性が高まります。
9+
10+
### コンテンツが利用できないケースを分離する
11+
12+
`ContentUnavailableView`のサンプルを見てみましょう。
13+
14+
```swift
15+
struct ContentView: View {
16+
@ObservedObject private var viewModel = ContactsViewModel()
17+
18+
19+
var body: some View {
20+
NavigationStack {
21+
List {
22+
ForEach(viewModel.searchResults) { contact in
23+
NavigationLink {
24+
ContactsView(contact)
25+
} label: {
26+
Text(contact.name)
27+
}
28+
}
29+
}
30+
.navigationTitle("Contacts")
31+
.searchable(text: $viewModel.searchText)
32+
.overlay {
33+
if searchResults.isEmpty {
34+
ContentUnavailableView.search
35+
}
36+
}
37+
}
38+
}
39+
}
40+
```
41+
42+
ここではリスト上にアイテムが1件以上存在するメインケースと、検索結果が0件だった場合のケースを`.overlay`で分離しています。
43+
これによりメインケースのコードブロックではメインケース自身のレイアウトに集中することができます。
44+
45+
また、検索結果が空の場合だけでなく、通信がエラーになるなどコンテンツが利用できない理由が複数存在することも考えられます。
46+
その場合でも`.overlay`のブロックでハンドリングに集中することでメインケースのコードブロックの肥大化を防げます。

0 commit comments

Comments
 (0)