iOSエンジニアがTamguiを使ってわかった"良いところ・微妙なところ"
こんにちは、Findyでモバイルアプリエンジニアをやっている加藤(@Takahiro_Kato)です。
この記事は ファインディエンジニア #2 Advent Calendar 2025の14日目の記事です。
背景
私はこれまでiOSアプリの開発をメインに、エンジニアとしてのキャリアを築いてきました。
Objective-Cから始まり、Swift、SwiftUIと変遷する言語の移り変わりも体験し、時には、専門外であったWebフロントエンドやバックエンドの開発に携わることもありました。
そんな中、今年は、これまでの経験に加えて、「React Native」を用いたクロスプラットフォームでのモバイルアプリ開発にも挑戦してきました。
そして、「React Native」でモバイルアプリ開発をする際に、「Tamagui」をUIライブラリとして利用することにしました。
そこで、この記事では、iOSエンジニア目線で「Tamagui」を使ってみた感想や、良いところ・悪いところを中心に紹介したいと思います。
なぜTamagui?
まず、「なぜ『Tamagui」をUIライブラリとして選定したのか?』から説明したいと思います。
背景で簡単に触れましたが、私はiOSエンジニアではあるものの、業務でWeb技術に携わることがありました。
ただし、業務内容としては...
- 約9年前に、
Reactを半年程、利用 - iOSアプリ内のWebViewで表示するWebページを開発
-
HTML,CSS,JavaScript,jQueryを利用することが多かったです。
-
- 社内管理ツールや、簡単な検証のために、
Node.jsを用いてAPIを開発
といったものであり、お世辞にもReact(それも近年のReact)に精通しているとは言えない状況でした。
(案の定、React Nativeに初めて触れてから、しばらくの間、四苦八苦することになりました...)
そんなiOSエンジニアの私が限られた時間の中でスピード重視でReact Native製のアプリを開発するには、UIライブラリが欠かせないであろうと考えました。
私なりに、React Nativeで利用可能なUIライブラリについて調べてみたところ、次の2つが候補として上がりました。
初めは、近年、存在感を増しているという噂から、gluestack-uiの採用を考えていたのですが、試しに利用してみたところ、次のような取っ付きにくさを感じました。
- 自動生成したファイルを編集(削除 or 追加)して、プロダクトにあった指定に変更するという使い方
- TailwindライクなCSSの指定方法
これはあくまでも、私のバックボーンがiOSエンジニアであることに由来する面が強いと思われます。
(因みに、周囲のWebフロントエンドエンジニアからは、Tailwindは好き嫌いが分かれると聞きました。)
iOSアプリでは、元々プラットフォーム標準のUI部品が提供されているため、それを独自にカスタマイズすることが多く、
-
lottie-ios
- 複雑なアニメーションの導入
-
Charts
- 様々なグラフの導入
のようなピンポイントでサポートしてくれるOSSがよく利用される印象が個人的にはあります。
つまり、OSSとして提供されたUIを「そのまま利用する」 or 「変更が許された範囲で改変して利用する」という使い方に体が馴染んでいるのです。
これに対してTamaguiはというと、
- コンポーネントへのスタイルの直指定や「
styled」を利用してスタイル定義をまとめる方法でベースコンポーネントをカスタマイズできる - スタイルの適用方法も、昔ながらの
CSSの書き方に近い(基本的に同じ)
という使い方が私には直感的でわかりやすく感じられました。
また、
- gluestack-uiよりもTamaguiの方がGitHubのスター数が多い
- gluestack-uiの提供するコンポーネントと比較してもTamaguiの提供するコンポーネントは遜色ない
- 歴史が長い分、Tamaguiの方がネット上の記事が多い
ことから、安心感を持って利用できると感じました。
使ってみてわかった"良いところ・微妙なところ"
少し前置きが長くなりましたが、ここからが本題であるTamaguiを使ってみてわかった "良いところ・微妙なところ"について紹介したいと思います。
※本記事では、tamagui@1.138.0 を利用しています。
良いところ
SwiftUIにUI構造が似ている
SwiftUIもTamaguiも「宣言的UI」という思想で作られているため、UI構造が非常に似ていると感じました。
例えば、「垂直方向にTextとButtonのあるUI構造」を書きたいとします。
SwiftUIでは、次のように、書くことができるのに対して、
import SwiftUI
struct SampleScreen: View {
var body: some View {
VStack {
Text("Hello World!")
Button(action: {
print("Button tapped")
}) {
Text("Press Me")
}
}
}
}
Tamaguiでは次のように書くことができます。
import { YStack, Button, Text } from "tamagui"
export const SampleScreen = () => {
return (
<YStack>
<Text>
Hello World!
</Text>
<Button
onPress={() => console.log("Button tapped")}
>
Press Me
</Button>
</YStack>
)
}
Button(action:) と <Button onPress={...}> に多少の書き方の違いはあれど、比較すると、かなり似ていることがわかると思います。
スタイルの定義方法が似ている
また、スタイルの定義方法に関しても、スタイリングしたいUI部品に対してスタイル定義を加えることでカスタマイズできる点も似ていると感じました。
SwiftUIは「Modifier」と呼ばれるメソッドを利用して、メソッドチェーン記述で書くことができます。
import SwiftUI
struct SampleScreen: View {
var body: some View {
VStack(spacing: 16) {
Text("Hello World!")
.font(.largeTitle)
.fontWeight(.bold)
Button(action: {
print("Button tapped")
}) {
Text("Press Me")
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
.padding()
}
}
一方でTamaguiでは、Reactで標準的に用いられる「Props」形式でスタイル定義を指定することができます。
import { YStack, Button, Text } from "tamagui"
export const SampleScreen = () => {
return (
<YStack gap="$4" p="$4">
<Text fontSize="$8" fontWeight="bold">
Hello World!
</Text>
<Button
bg="$blue100"
color="white"
rounded="$4"
onPress={() => console.log("Button tapped")}
>
Press Me
</Button>
</YStack>
)
}
このようにUIのスタイリングを直接的に指定できることで、ビジネスロジックと混ざりにくく書ける点で似ていることがわかるかと思います。
デザイントークンを定義することで、ライト/ダークテーマの切り替えが容易
Tamaguiのスタイル指定の例で「$blue100」「$4」などの記載がありましたが、これは「デザイントークン」と呼ばれる定義になります。
予め、プロジェクト内でデザイントークンを定めておくと次のメリットがあります。
- プロジェクト内で必要なデザイントークンを
configファイルに書いておき、定義されたデザイン(スタイリング)のみ利用するルールとすることで、統一したデザインを維持することができる -
configファイルでライト/ダーク両方のカラー定義をしておくことで、各コンポーネントファイル側で各々の指定をすることなく、ライト/ダーク両方のデザインを適用することができる
カラーテーマを独自定義し、ライト/ダークテーマに適用する例 👇
import { defaultConfig } from "@tamagui/config/v4;
const customTokens = createTokens({
...defaultConfig.tokens,
color: {
...
black900: "#000000",
...
blue500: "#044BA0",
...
white900: "#FFFFFF",
},
...
});
const customThemes = {
dark: {
...defaultConfig.themes.dark,
accentColor: customTokens.color.white900,
...
},
light: {
...defaultConfig.themes.dark,
accentColor: customTokens.color.black900,
...
},
};
export const tamaguiConfig = createTamagui({
...defaultConfig,
themes: customThemes,
tokens: customTokens,
...
});
公式サイトに詳しく書いてあるので、一読すると理解が進みます。
微妙なところ
スタイル指定時の名称が短縮形でわからない
通常、CSSでスタイルを指定する場合、margin, paddingなどを利用すると思います。
一方でTamaguiのViewなどに指定する場合、短縮形で指定することができます。
| CSS指定 | Tamaguiの指定 |
|---|---|
| margin | m |
| margin-top | mt |
| padding | p |
| padding-bottom | pb |
| left | l |
| right | r |
| justify-content | justify |
| align-items | items |
| zIndex | z |
| max-height | maxH |
| max-width | maxW |
| background | bg |
| border-radius | rounded |
上記に対してCSS指定と同じ名称もしくは近しい(キャメルケースに変えるだけで済む)名称の場合もあります。
| CSS指定 | Tamaguiの指定 |
|---|---|
| color | color |
| height | height |
| width | width |
| border-width | borderWidth |
| font-size | fontSize |
| flex-direction | flexDirection |
これは正直、慣れるまでに時間がかかりましたし、初見だと予想しにくいものもありそうだなと思いました。
Android実機でちょこちょこ不具合がある
iOSは相性が良いのか、iOSでのみ発生する不具合に遭遇することはないのですが、Android(特に実機)でいざアプリを見てみると、思うように動かないということがありました。
幾つか、具体的に例をあげていきたいと思います。
スタイルを独自カスタマイズしにくい場合がある
Tamaguiでは、Tamaguiのベーススタイルを解除するために「unstyled」が用意されていますが、触ってみる限り、必ずしも「unstyled」が効かない(「unstyled」でもベーススタイルを解除できない)ということがありました。
例えば、Tamaguiではタブコンポーネントとして、Tabsが提供されています。
公式ドキュメントのサンプルでは、Tabs.TabにfocusStyleを適用することで、フォーカスが当たっている場合の背景色の調整ができることが示されています。
<Tabs.Tab
focusStyle={{
backgroundColor: '$color3',
}}
flex={1}
value="tab1"
>
確かに、これで一見、想定通りに実装できるように見えるのですが、タブ切り替えを繰り返していくと、
『 アンフォーカス状態にも関わらず、フォーカス状態と判定される 』
という事象が見られました。
試行錯誤したものの、最終的には、focusStyleの利用はやめて、独自にViewを挟み、独自に制御することで落ち着きました。
<Tabs.Tab>
<View
position="absolute"
t={0}
l={0}
r={0}
b={0}
pointerEvents="none"
bg={isFocused ? "$color3" : "transparent"}
>
...
</View>
</Tabs.Tab>
Sheet表示直後に、ボタン等をタップしても反応しない
Tamaguiでは、画面下からアニメーション付きで表示することのできるSheetコンポーネントが提供されています。
Sheetコンポーネントを利用することで、画面全体を覆い尽くす(完全に画面遷移仕切る)ことなく、何かしらのUI表現をユーザに見せることができます。
このSheetは非常に簡単に利用できたのですが、Android実機でのみ、
『 Sheet表示直後に、Sheet内のボタン等をタップしても反応しない 』
ということが発生しました。
初めはダブルタップをしないと反応しないのかなと考えていたのですが、
Sheet表示をしてから少し時間をおいてタップすると反応することに気づきました。
そこで試しに、次のようにanimationを適用してみたところ、解決することがわかりました。
<Sheet
modal
...
snapPoints={[80]}
animation="200ms"
>
200msはデフォルト定義されている割と高速なアニメーション表現です。
200msや100msを指定するとSheet表示直後でもボタンタップが可能となり、quick,lazyなどのアニメーション指定では解決できませんでした。
このことから、Android実機ではTamaguiのUIアニメーションの完了に時間がかかり、その間の操作を受け付けなくなっていると予想しています。
個人的なイメージですが、Androidの場合、iOSよりも比較的パッと切り替わる表現が多い気はしているので、「animation="200ms"」を指定しても、違和感は抑えられる気はしています。
Expo SDK v54への対応に時間を要した
今回、私は、React Nativeでモバイルアプリを開発するにあたりExpoを利用しました。
Expoは、
- 便利で使いやすい様々なモジュールを提供
- Router, WebView, カメラ, カレンダーなど
- iOS/Android両方に対応したプッシュ通知配信機能やテストツールを提供
- iOS/Androidの証明書管理、ビルド、リリース周りの手厚いサポート
など、最早React Nativeでモバイルアプリを開発するのであれば知らない人はいないと言っても過言ではないSDKです。
そんなExpoですが、2025/09/11に、Expo SDK 54のリリースが周知されました。
今回のアップデートで、iOS26のLiquid GlassやAndroid16(API 36)に関する対応も含まれているため、開発者としては早めにアップデートをしておきたいところだと思います。
早速、GitHub - tamagui/tamagui 上でも「いつ頃、Expo SDK 54に対応するの?」と議論が始まっていたり、しばらくして WIP Pull Request が作られたりとコミュニティ内での動きは活発に感じました。
しかしながら、Expo SDK 54への対応版がリリースされたのは「2025/11/15」であり、
事前に対応版リリース日のアナウンスはなかったと記憶しています。
(issue上で、鋭意対応中と回答があったのみに留まっていました。)
とは言え、Tamaguiは「2021/3/11」にv0.1.0が登場しており、5年以上に渡り開発が継続されてきたこともあってか、そう簡単にExpo SDK v54に対応することができなかったのではないかと予想しています。
Tamaguiだけではありませんが、OSSライブラリを利用する限り、こういったリスクとはトレードオフでもあるので、その点だけは理解しておく必要があると改めて思いました。
まとめ
Tamaguiの"良いところ・微妙なところ"を含めて紹介させて頂きましたが、私個人としては、
- 未経験の
React Nativeによるモバイルアプリ開発において、大いに助けられた - UIライブラリにはメリットだけでなく「デメリット」も必ず存在する
という点から、とてもポジティブな気持ちでTamaguiを利用することができています。
今後、私自身がReact Nativeでのモバイルアプリ開発の経験を積み、
iOS/Androidで、完全かつ統一的なデザインが必要というケースでない場合であれば、
素のReact NativeでUIを実装する方針に転換していく可能性はありますが、
第一歩としてTamaguiを選択することはオススメできると考えています。
この記事が、これからReact Nativeを始める方にとって少しでも参考になれば幸いです。
Discussion