20200207のSwiftに関する記事は3件です。

watchOSで〇〇アニメーション

watchOSアプリを開発した際に、いくつかアニメーションをつくりました
その中でうまくいったこと?いかなかったこと?があるので、そのまとめになります

基本のローディングアニメーション

実装方法は以下の2つ

  1. パラパラ画像をつかう
  2. APNGをつかう

パラパラ画像をつかう

Appleのサンプルコード的にも正攻法といって良いでしょう
特徴としては、

  • Asset Catalogを利用できるため、解像度の対応がしやすい
  • 画像枚数が増えると管理が手間(あくまでAPNGにくらべて)

です

シンプルな Duration 指定の場合

// animatedImage: WKInterfaceImage
animatedImage.setImage(UIImage(named: "Bus"))
animatedImage.startAnimating()

startAnimatingはStoryboard上で設定されたDurationでアニメーションを行います

Storyboard
スクリーンショット 2020-02-07 17.18.45.png

Asset Catalog
スクリーンショット 2020-02-07 17.21.39.png

Duration + Repeat Count 指定の場合

animatedImage.setImage(UIImage(named: "Bus"))    
animatedImage.startAnimatingWithImages(in: NSMakeRange(0, 4), duration: 2.0, repeatCount: 3)

APNGをつかう

APNGはアニメーションするPNG画像のことです。

特徴としては、

  • リソースが1つになるので、画像パラパラよりも管理が楽
  • 画像は、Extension ターゲットのリソースに入れる
  • APNGの尺に合わせたDurationにすれば良い
  • watchOSはAPNG対応のライブラリがない(*注)ので自前でソースコードを書く必要がある

*注:下書き時点ではなかったのですが、今ならSDWebImage/SDWebImageSwiftUIが使えるかもしれません:tada:

実装例については、Apple Watch で GIF/APNG を使う (ライブラリ不使用) が参考になりました(ありがとうございました)
こちらでスムーズにアニメーションができました

カウントダウンで遷移アニメーション?

やりたかったのは「3 -> 2 -> 1 -> 画面遷移」でした
前述の「パラパラ画像もしくはAPNGをつかい、画面側でタイマーセットをし一定時間後に画面遷移する」という実装を行いましたが、カウントダウンし切る前に遷移してしまったり・・・とうまくいきませんでした

(こちらについて、シンプルで良い方法があればご教授いただけるとうれしいです?)

より動的なアニメーション

たとえば位置情報に基づいてアニメーションを移動・変更したい等のケースでは、Sprite Kit を使うことになると思います
ただし、watchOSで複雑なアニメーションをする際には、パフォーマンスに気を配るとよさそうです
特にApple Watch Series 1などでは、最悪の場合、いつの間にかアプリがパージされます?

やるといいこと

スレッド管理

断然 ReactiveX/RxSwift の利用が楽です

  • アニメーションの移動についての計算はバックグラウンド処理
  • 描画処理はメインスレッドで実行

という実装にしましょう

SKTextureAtlas の利用

公式ドキュメントは About Texture Atlases
SKTextureAtlasの利用により、メモリ使用量、描画パフォーマンスが向上するようです

まず、使用するテクスチャについては、Asset Catalog > + > New Sprite Atlas で専用フォルダをつくり、格納しましょう

Asset Catalog
スクリーンショット 2020-02-07 18.32.54.png

テクスチャアトラスからテクスチャを作成

static let TextureAtlasName = "Sprite Atlasのフォルダ名"

static let TextureAtlas = SKTextureAtlas(named: TextureAtlasName)

lazy var textures: [SKTexture] = {
    var textures = [SKTexture]()
    let numberOfTextures = TextureAtlas.textureNames.count - 1
    for index in 0...numberOfTextures {
        let name = String(format: "%@%d", TextureAtlasName, index)
        textures.append(TextureAtlas.textureNamed(name))
    }
    return textures
}()

テクスチャの切替のみ(要はパラパラ)でアニメーションする場合は以下のような実装

func animate() {
    let node = SKSpriteNode(..省略..) // アニメーション大賞のSKSpriteNodeオブジェクト生成
    let action = SKAction.animate(with: textures, timePerFrame: 0.03)
    let repeatAction = SKAction.repeatForever(action)

    node.run(repeatAction, withKey: key) // node: SKSpriteNode
}

画像比率の適用

Sprite Kitを使う際にはAutoLayoutやCore Graphicsのような簡単な配置ができません
そのため、全端末でうつくしく表示するためには、画像比率に対してテクスチャサイズの変更等が必要かもしれません

たとえば、背景画像を画面いっぱいに表示したい場合は以下のように SKScene のサイズを指定します

InterfaceController.swift
func setupScene() {
    let image: UIImage = BackgroundImage
    let size = contentFrame.size
    let portraitRatio = size.height / image.size.width
    let landscapeRatio = size.width / image.size.height
    let multiplier = min(portraitRatio, landscapeRatio)
    let sceneSize = CGSize(
        width: image.size.width * multiplier,
        height: image.size.height * multiplier)
    let scene = SKScene(size: sceneSize)
    scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
    scene.scaleMode = .aspectFit
    sceneInterface.presentScene(scene)
}

以降、このScene上で配置するNodeのサイズは、multiplierを使った計算が必要になるでしょう

まとめ

watchOS6よりWatch-only appの開発が可能になりました。
まだアプリ数は少ないようですが、AppleWatch用のApp Storeもリリースされました。
これからwatchOSアプリ市場が盛り上がることを期待!

以上です???
ありがとうございました?

Thanks to

SpriteKit Animations and Texture Atlases in Swift
Apple Watch で GIF/APNG を使う (ライブラリ不使用)
WatchKit のカスタムUI実現方法のまとめ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CA.swift #11 ~3年後のアプリ設計を考えよう~ 参加報告

大変遅くなりましたが
2020年1月30日に開催されたCA.swiftへ
ブログ枠として参加させていただきました際の報告です。
https://cyberagent.connpass.com/event/155392/

ざっと内容や感想を書かせていただきます。

Thinking about Architecture for SwiftUI

登壇者

@d_dateさん

KITASANDO COFFEE - 北参道コーヒー

スライド

https://speakerdeck.com/d_date/thinking-about-architecture-for-swiftui

内容

タイトルはSwiftUIの設計になっているけど

メインはDataflowの話(あと実は時間が30分だと思っていた)

 宣言的にUIが記述できるフレームワークが流行っている

  • SwiftUI
  • Android: Jetpack Compose
  • ReactNative/Flutter

SwiftUIとProperty Wrapper

@Stateなどで修飾されたプロパティと
ユーザーの操作をバインドして画面の状態を更新して再描画するという
Dataflowを構築している。

DataflowといえばCombine

  • 宣言的に非同期なイベントを型として表現できるSwiftのAPI
  • 様々なオペレーターでイベントをハンドリングできる
  • Apple製

RxSwiftに取って代わるみたいな流れになっている。

SwiftUIと組み合わせて使われることが多い

CombineのPublisherをViewでSubscribeして
値が流れてくると
画面の状態の更新や画面の再描画を行う。

View with ObservedObjectを使った例

ObservableObjectプロトコルに適合したViewModelを
@ObservedObjectとしてViewで参照を保持して
SwiftUIのイベントとバインドさせて

単方向のデータの流れ(Unidirectional Dataflow)

を構築する。

Unidirectional Dataflowといえば

  • Flux
  • Redux
  • Composable Architecture

FluxとReduxについては設計についての良い本があるのでそちらまで

https://peaks.cc/books/iOS_architecture

Composable Architecture

pointfree.coで紹介。

  • ReduxとElmアーキテクチャを組み合わせたもの
  • SwiftUIとCombineに最適化(両方使ったアプリ作成を行っている)
  • 関数型プログラミング
  • View / State / Action / Store / Reducerを用いる
  • 副作用はEffect型で全て定義
  • 小さい部品を組み合わせて大きなデータフローを構築している

私も購読しているのですが
最初は仕組みがいまいちわからず
複数回見ないと全体の理解が難しかった印象を持っています。

ただ
全てがパターン化している上に
宣言的に記述できるので
中身をわかっている人には可読性が非常に高いなと感じています。

アプリの実装内容だけではなく
状態の変更を集約するという考えや
関数を型として扱う方法など
色々なことが学べるのでとても参考になると個人的には思ってます?

簡単な流れ

  • ActionをStoreを送る
  • ReducerでActionをハンドリングしてStateを更新する
  • Actionの結果Effect型で定義した副作用が発生する
  • 更新されたStateを用いてViewを更新する

良さそうだけど、iOS13以降ではないと使えないし。。。UIKitとRxSwiftで同じことができないかやってみた

UIKit + RxSwift

  • ユーザのアクションをSubscribeして流れてきた値をStoreに送る
  • ReducerでActionをハンドリングする
  • Effect型はiOSバージョン関係ないのでそのまま使え、Actionの結果Effect型で定義した副作用が発生する
  • 更新されたStateを用いてViewを更新する

ただ一つのStore

Storeはアプリ全体で一つだけ。

理由として

  • 認証情報などが引数で引き回す必要がなくなる
  • 今後の情報の追加も楽

ただStoreが全てを管理していることで

  • distinctUntilChangedしないと処理が何回も呼ばれてしまう
  • 画面遷移時に初期化処理をしないと、前の画面の処理が動いてしまう

こともある(ここ何か聞き間違えていたかもしれません)。

SwiftUIとCombineで実現しているAppleの考えを
iOS13以前でも実現できることを具体的に知ることができて
大変勉強になりました。

AbemaTVにおけるiOSアーキテクチャの課題解決

登壇者

@_yysskkさん

スライド

https://speakerdeck.com/yysskk/solving-problems-in-abematv-ios-architecture

内容

プロダクト背景

2015年3月から開発開始
現在は10名で開発

チーム人数の変遷

4(2015)

6(2017)

14(2019)

設計の変遷

Flux(第1世代)

MVVM+Flux(第2世代) ← 縦化、ビデオ機能開始

MVVM(Unio)+Flux(第3世代) ← アプリ内コイン、投げ銭機能開始

Flux(第1世代)

設計の選定理由

  • 複雑な状態管理をわかりやすくしたい
  • クライアント間(Android/iOS/Web)で同じ設計にしたかった

Fluxについて
https://facebook.github.io/flux/docs/in-depth-overview

大まかな処理の流れ

  • ViewControllerがActionを呼ぶ
  • ActionがAPI通信を行う
  • APIから取得したデータをDispatcherに流す
  • StoreでDispatcherから流れてくるイベントをbind
  • ViewControllerがStoreの状態を監視してUIを更新

良い点

  • View間の依存関係が減る
  • 開発者の実装が統一されやすい
  • データの流れがわかりやすい

MVVM+Flux(第2世代)

縦化とビデオ対応機能開始

第1世代のままだと困ること
- 機能の複雑化でStoreやUIの合成ロジックがViewControllerに増えた


Storeの値が更新された時にリロードしていたものに加えて
縦にした画面サイズの変更時もリロードする必要が出てきたため
combineLatestしなければならなくなった。

そこで

MVVMの導入

実際はRxを使用しているため
ViewModelではなくViewStreamという名前になっている

ViewModelのinitで
Inputのイベントを元にOutputを合成する

良い点

  • ViewControllerをUIイベントの伝播と状態をUIにbindするだけにできた
  • プレゼンテーションロジックをUIから分離したので単体テストを書くのが楽になった
  • グローバルで保持する必要のない画面の一時状態をUIのライフサイクルで破棄できる

MVVM(Unio)+Flux(第3世代)

コイン機能の開発、投げ銭機能の開発
->チームメンバーの数が増えた

第2世代のままだと困ること

  • ViewStreamの実装が実装者でバラバラ


テキスト検索を関数で定義するか
Observableのプロパティを使うか
PublishRelayを使うか
など

そこで

Unioの誕生(ViewModelの構造を統一)
https://github.com/cats-oss/Unio

良かった点

  • ViewModelの実装の統一が実現
  • ViewModelのロジックの流れが単一方向なので可読性が上がった

現在の課題

  • 前世代の実装が残っている
  • 第1世代の残っている実装でStore to Storeな実装があり処理が複雑になっている
  • 画面の単体起動ができない
  • Actionが直接Resourceを参照している

まとめ

変更を入れるタイミングは全機能の刷新や大きな機能開発が入る時に行う。
発生した課題に対して都度解決しながら変化していくことができた。

AbemaTVは比較的新しいので
最初の設計からシンプルでわかりやすいなと感じました。

変更に応じて設計を都度見直すというのも
変化の多い現在のアプリ開発ではとても自然な流れだと思っています。

ただ
前世代の実装が残っているということもおっしゃっていたので
やはりずっと負債になって手が出せないような部分も結構あるんだろうなとも感じ取れました。

そういったところに対して
どういう状態で手が出せずに
今後どうしていくのかといった点も聞いてみたいですね?

Amebaの設計とこれから ~カオスからの脱却と再びカオスを生み出さない努力~

登壇者

@shun_sakuuuuさん

スライド

https://speakerdeck.com/shunsakumiki/caswift-11

内容

2017年中途入社

カオスすぎた設計

2010年にリリースされて以来
10年以上を運営する中でたくさんの転換

  • WebViewアプリからフルネイティブに
  • Objective-CからSwiftに
  • メンバーの入れ替わり
  • 当時デファクトと呼ばれて今は古くなっている設計
  • リアーキテクチャを試みようとしたが道半ばになっている痕跡

基本はMVC

俗にいうMassiveViewController

ViewControllerからAPI通信を行っているものが多いものの
それはよくないという風潮からHogeHogeManagerなど名前から役割がいまいちわからない
クラスが作られていることもある。

テストはない。

なぜか一部VIPER

おそらくカオスなMVCから抜け出そうとして痕跡かもしれないが
ドキュメントがないのでいまいちわからない。

VIPERでもテストはない。

このままだと

新規画面を実装するにしても

  • どの設計にすればいいか毎回0から考えて大変
  • 人によって異なる設計をする状態でレビューが大変
  • 設計をしたエンジニアがいなくなると後から変更加えるときに調査が必要になったり負担が重くなりそう

既存の実装に手を加える場合

  • 設計がぼんやり&密結合&テストがないので何が正しいのかがわからず、工数が通常よりもかかってしまう状態

なのでリアーキテクチャ

方針として

  • 最終的にはMVVM+Flux
  • bindingとして利用するためにRxSwiftの導入も進める
  • テスタビリティも考慮する
  • CI環境を整えて自動テストを回していく
  • 設計に関してはチームみんなで考える

特に最後の設計をチームで考えるが大事なことで
これがないとリアーキテクチャのモチベーション維持が難しくなる。

まずはMVVM

採用した理由として

  • ロジックが複雑な画面がそこそこあった
  • 画面間で同期が必要な仕様があまりなかった

bindingにはRxSwiftを利用

  • 社内で使用しているプロジェクトが多かったため

初期のMVVM

ViewModelのinit内でPublishSubjectを生成して
ObserverとObservableのプロパティにセットして購読していた

簡単な画面ならこれで困ることはない

しかし

  • Input,Outputのインターフェイスの形を実装者に委ねていたためレビューコストが肥大
  • 複雑な初期化処理が入ってinitが肥大してメンテナンスコストが爆増

Usecaseの導入

ViewModelのinitで行っていた下記の処理をUsecaseに移動

  • Observableの加工
  • API通信
  • ModelからView表示用Modelへ変換するTranslatorの呼び出し

MVVMの改良

  • Input,OutputをProtocolに切り出しインターフェイスを統一
  • initで行われていた処理を責務ごとに分離して肥大化の防止+テスタビリティの向上 ※ 2つ目に関しては Usecaseに切り出したことで肥大化を防いだのか また別で責務の分離を行ったのかは 確認できませんでした??‍♂️

Flux導入

仕様変更によりMVVMの仕組みだと煩雑になってしまうようになった

例えば

  • ユーザーのログイン、ログアウト状態によってアプリ全体の状態を変更
  • あるタブの画面上の値の更新を別のタブの画面でも受け取らなければいけない など

最初はObserverとObservableを持つ
Singletonのオブジェクトを利用して解決

しかし
Singletonで何でもできてしまうという点や
さらに監視する項目が増えた時のスケーラビリティが良くないと感じる点から
Fluxを導入

使い方は

  • データフローはFacebookのものと同様
  • StoreとViewModelを直接bindさせることで非Rx依存に(ExtensionでStoreをObservableに変換)

効果

  • 利用者側は必要なイベントやStateを持つStoreを見ればよくなり、余計なObservableの購読がなくなった
  • Singletonのときに感じていた処理の肥大化がする感じがしなくなった
  • データフローが単方向でAPI通信が発生する処理でもキレイな構成を保てた

でも注意点も

利用箇所を考えずに入れてしまうと簡単な画面だとむしろ煩雑になってしまう可能性がある

使い所

MVVMではキレイに作れない処理の流れがあるような時に選択肢として利用

登壇者個人としては
NotificationCenterやObserverパターンで毎回独自の便利クラスを作るよりは
断然Fluxを使った方が良いと考えている。

現在までの結果(良かった点)

  • 新規画面の実装は設計がある程度統一されているのでレビューが楽
  • チームで認識を合わせて進めてきたので設計を考える大切さの共通認識が生まれた
  • 設計ドキュメントを社内に残せるようになった
  • テストを考慮したことでCI環境の見直しができた(メンテナー属人化したJenkinsからBitrise)

現在までの結果(悪かった点)

  • 試行錯誤しながらだったため必要のない負債も生んでしまった
  • もう少し検討する時間が必要だった
  • 全画面でリアーキテクチャするまでのマイルストーンがまだ引けていない

これから

不安なこと

  • ViewModelの先にあるUsecaseやRepositoryの作りをどうするか
  • もし現メンバーがいなくなったらまた同じカオスを生み出してしまうのではないか

Layered Architectureの考え方を導入したい

しかし
人によってLayered Architectureの考え方が異なるので
Amebaに取り入れるにはどうしたら理想かを考えていきたい。

Layered Architectureとは?
https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html

まとめ

長くサービスを運営しているとカオスは生まれてしまうものの
この状況を伸びしろだとチームで捉えることができればどんどんリアーキテクチャを進めていった方が良いと思った。
設計に銀の弾丸はないので新しいものでもサービスの特性にマッチすればどんどん取り入れていきたい。
大事なのはチーム全体で設計へのコミットをしていくこと。

聞いていると
色々設計パターンが混ざっていて
コードを理解するのが大変そうだなと思いました。

そんな中で
どのように現在の動作を保証しながら行っているのか?

どのくらいの期間でどこまで達成できているのか?
といった話もぜひ聞いてみたいですね。

完全に達成するまでの道のりは険しそうですが
ここからどう変化しどこまで達成できたのかの次回の報告が楽しみですね?

5年目に突入したAWAのアプリ設計

登壇者

@sasakky_jさん

スライド

https://speakerdeck.com/jinsasaki/awa-caswift-11

内容

現在

v2.0
iOS9までサポート
iOSエンジニア3名

History

Objective-CからSwiftへ
開発体制の変更で人数など色々あったが

v1.0 -> v2.0で
フルスクラッチ開発

プロダクトの特性

  • オフラインでも利用できる・再生できるようにするためにキャッシュが必要
  • プレイヤーはどの画面からでもアクセスできる

など

v1の設計

MVCでRealmを利用。

問題点

  • 2000~3000行のMassiveViewController
  • Viewがかなり状態を持っている
  • Controllerの責務が巨大
  • Entityがロジックを持っている
  • 万能なManager
  • 神Util
  • 全ての層でRealmに依存

v2でフルスクラッチ開発

背景

これまでの機能を全て見直し
全画面に手を入れる必要があったことから
作り直す良い機会だと捉えた

まっさらな状態から設計を考えることができた。

目標

  • 責務の分離
  • 状態の一元管理
  • Realmへの依存からの脱却
  • Reactive Programming

検討結果

Flux(with RxSwift) + レイヤードアーキテクチャー

やってみた結果

新規開発はやすくなったが
v1とv2の同時並行開発していたのでだいぶ大変だった。。。
加えていつ終わるのかが見えないので
モチベーションの維持が難しい。

Apple Watchにも対応

watchOS6.0以降対応ということで

MVVM with SwiftUI + Combine

で開発。

開発してみた結果

  • まだまだ使えないライブラリが多い(XCTestやFirebaseなど)
  • バイナリのサイズが大きくなるとアプリの転送速度がかなり遅い
  • watchOSとSwiftUI起因のバグ(Betaだったからかもしれない)

これからの目標

  • ViewControllerに状態やロジックを持っているものがあるのでそれを分離したい
  • iOS/Android/PCで共通に利用できる共通の設計を導入する
  • テストをもっとやりやすくする

v1とv2を同時に開発していたとおっしゃっていましたが
どっちの実装をしていたのか混乱しないのかなと
タスクやスケジュールの管理方法なども気になりました。

長年運用してきたアプリなので
仕様がいまいちわからない部分もあるのかなと思い
v1と同じ動作が期待される機能を
v2ではどうやって動作の保証をしていったのかなどの
話も聞いてみたいですね。

ディスカッション

自分でまとめようと思いましたが
とても良いまとめ記事がありましたので
そちらのリンクを貼らせていただきます??‍♂️

https://note.com/fromkk/n/n986d20a31708

最後に

大規模なアプリを長年運用していくにあたり
どのような状況でどういう判断をしていったのかが
具体的に伝わってくる内容ばかりで
自分がこういう状況にあったらどうしていただろうと
あれこれ考えながら楽しく聞くことができました。

そして
機能の変更や追加
人員の入れ替えなどがある中で
どうやって設計を保ち
どのようによりゴールに近づいていくのか
そういった部分への取り組みについても
聞いてみたいですね。

また
半年、1年後といった経過の報告を
聞く機会がありましたら
ぜひ参加したいなと思います。

運営のみなさま
登壇されたみなさま
素敵な会をありがとうございました?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

XcodeでiOSアプリを作成中にiOSのバージョンをアップしたらエラがー出た話

 ※2020年1月29日時点の情報になります。記事投稿日時点の環境ではエラーは発生していません。開発中にむやみやたらと新しいものに飛びつくと良くないよという自身の忘備録として、また同じような状況に今後陥った方へ向けて書いています。

先日Xcode、iOS13.3のiPhone実機で開発をしていた際にiOS13.3.1にバージョンアップするとエラーが発生し、対応に困ったため記事にします。この手の文章を書き慣れていないため、練習も兼ねて・・・

結論

iOSのバージョンを13.3に戻すと解決しました。知識がない状態で開発中に環境いじるのは良くないですね。

経緯

環境
・macOS Catalina 10.15.2
・iOS:13.3 実機:iPhone11
・Xcode 11.3.1
.Apple Developer無料アカウント

上記環境にてiPhone実機で動作確認をしながらiOSアプリを開発していました。

ここで新しいもの好きの私は
iOS13.3.1のアップデートが来た!更新!

macOS10.15.3も来た!更新!

macOS更新したらXcodeでも何か更新してる・・・(ここあやふやです)

環境を一気に更新し、数時間後に実機ビルドをかけ新しく作成した機能の動作確認しようとすると・・・

スクリーンショット 2020-01-29 19.44.05.png

このようなエラーが出ました。
手元に他にアプリは無く、Apple Developer無料枠は1つの端末に3個
まで同時に利用できるという認識なので見に覚えがない。という状況でした(調べてXcodeの設定を色々いじったりしましたが、解決策は見つかりませんでした)

原因として真っ先に考えられるのが各種バージョンアップ(Xcodeは何か更新した?)による可能性が高かったため、元のバージョンにすること。

やったこと

Xcode→iOS→macOS(面倒くさい、できればやりたくない・・・)の順でバージョンを戻すことに。
※ダウングレード、アップグレード等は自己責任の範囲で行ってください。

Xcodeを再インストール → 変化見られず
iOS → ヒット。エラーが解消しました。

iOSのダウングレードですが、詳しく解説しているサイトが多数ありますので詳細はそちらに譲ります("iOS ダウングレード"で検索すれば引っかかります)

私はmacにiOS、iPadOSなどのIPSWをダウンロードできるサイトからiOS13.3をダウンロードし、Finder経由でiPhoneにインストールする形でダウングレードを実施。
再度実機ビルドをするとエラーは発生しなくなり、元通りになりました。

最後に

それから4、5日経ちアプリの開発に一区切りしたため、再度iOSを13.3.1に更新して試して見るとエラーは出なくなっていました。
配布後、即アップデートしたのが問題だったのか、他に原因がありiOSを戻すことで"たまたま"解決できたのかという点で少しもやもやしています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む