LoginSignup
225

More than 3 years have passed since last update.

SwiftUIのProperty Wrappersとデータへのアクセス方法

Last updated at Posted at 2019-06-13
  • 2019/7/17 @GestureStateについて追記しました。
  • 2019/7/19 BindableObjectがdidChange->willChangeなど変更されていたので修正しました。 https://developer.apple.com/documentation/swiftui/bindableobject
  • 2019/7/30 Xcode11 Beta5でBindableObjectはdeprecatedになりObservableObjectを使うようになったので追記しました。@ObjectBinding@ObservedObjectに名前が変わりました。
  • 2019/7/31 ObservableObjectがCombineに含まれること、IdentifiableはStandard Libraryに移動したことについて記載しました。また@PublishedをつけるとwillChangeなどの記載が不要になりました。
  • 2019/9/18 Property WrappersのwrappedValueとprojectedValueについて追記しました。
  • 2020/6/28 @AppStorage @SceneStorage @StateObjectについて追記しました。
  • 2020/8/8 @State@ObservedObjectの使用方法について一部修正と追記をしました。

SwiftUIの実装をしている中で
@StateなどのProperty Wrappersの違いを理解するために
セッションの動画なども含めて調べてみました。
※ Property Wrappersについては最後の方で紹介しています。

Data Flow Through SwiftUI
https://developer.apple.com/videos/play/wwdc2019/226/

まだ学び始めたばかりの認識ですので
もし勘違いしている点などありましたら
教えていただけますと嬉しいです:bow_tone1:

簡単にまとめると下記のような感じです。

Property Wrapper 使用方法
@State 特定のView内でStringやIntなどのシンプルなデータに使用する
@StateObject ObservableObjectをインスタンス化する唯一の場所として使用する
@ObservedObject 親Viewで所有しているObservableObjectを直系の子Viewでも読み書きできるようにしたい場合に使用する
@EnvironmentObject Viewの階層構造でObservableObjectを広く使用したい場合に利用する(親ViewのObservableObjectを孫Viewでも使用したいなど)

@State

Viewはstructのため
通常の値を更新することができませんが
@Stateを宣言することで
メモリの管理がSwiftUIフレームワークに委譲され
値を更新することができるようになります。

その際にはViewの再構築を自動でしてくれるようになります。

内部的には差分アルゴリズムを使って
関連のあるViewのみ再構築をしてくれるようです。

State
https://developer.apple.com/documentation/swiftui/state


struct ContentView : View {

    @State var value: Bool = false

    var body: some View {
        Toggle(isOn: $value) {
            Text("The value is " + "\($value)")
        }
    }
}

@Stateは宣言したプロパティを保持するViewと
その子Viewからしかアクセスができません。

使いどころ

@Stateは宣言するたびに
新しい状態を生み出し(SwiftUIのメモリ上に新しくアロケートされ)ます。
そのため初期値の設定が必要になります。

スクリーンショット 2019-06-12 5.20.02.png

そのため
外部からのデータを参照するなどには適さず
ローカルでしか使用しないデータのアクセスに適しているようです。

※ ローカルでしか使用しないとは
ButtonをクリックしたときのHighlightなど
そのView内のみで使用されるようなものを想定しています。

また、classなどで利用すると
インスタンス自体は変更がないため
内部のプロパティが変化しても
Viewの再描画は発生しません。

そのためIntやStringなどのシンプルなデータを
扱う場合などに適しています。

structなどのValue Type
値をコピーする性質から状態の共有をすることができないため
Value Typeにも@Stateを活用することはできます。

@Stateのプロパティはprivate修飾子が付けられるような形で
使用することが好ましいようです。

スクリーンショット 2019-06-12 5.00.28.png

BindableObjectと@ObjectBinding(Xcode11 Beta5よりdeprecated)

値の更新を通知するクラスがBindableObjectプロトコルに適合することで
@ObjectBindingを宣言したプロパティへ値の更新通知を行うようになります。

BindableObjectAnyObjectに適合しているため
参照型にしか使用できません。

更新の通知方法などは開発者側で実装する必要があります。

@ObjectBindingを宣言したプロパティは
SwiftUIに依存関係があることを明示的に伝え
値の更新通知を自動で受けとることができるようになります。

動き自体は@Stateに似ていますが、
Viewの階層をまたがって
値の更新通知を受け取ることができます。

@ObjectBinding
https://developer.apple.com/documentation/swiftui/objectbinding

BindableObject
https://developer.apple.com/documentation/swiftui/bindableobject


class ViewModel: BindableObject {

    let willChange = PassthroughSubject<ViewModel, Never>()

    var isEnabled = false {
        willSet {
            willChange.send(self)
        }
    }
}

struct ContentView : View {

    @ObjectBinding(initialValue: ViewModel()) var viewModel: ViewModel

    var body: some View {
        Toggle(isOn: $viewModel.isEnabled) {
            Text("The value is " + "\(viewModel.isEnabled)")
        }
    }
}

子Viewにデータを渡す際はinitを使って
Viewの階層を辿って依存関係を宣言していく必要があります。

スクリーンショット 2019-06-12 4.31.55.png

使いどころ

複数のViewにまたがって参照されるクラスなどに使用するのに適しています。

すでに存在しているViewを表示するためのモデルや
データベースへの保存に必要なモデルなど
SwiftUIの外部のデータを参照する場合に適しているようです。

スクリーンショット 2019-06-12 4.30.44.png

ObservableObjectと@ObservedObject(2019/7/30 追記)

Xcode11 Beta5でBindableObjectは非推奨になり
ObservableObjectを使うようになったようです。
BindableObjectObservableObjectIdentifiableに適合するようになっています。
これはSwiftUIではなくCombineフレームワークに含まれます。
ちなみにIdentifiableはStandard Libraryに移動しました。

また、@ObjectBinding@ObservedObjectに名前が変わりました。
@ObjectBinding@ObservedObjecttypealiasになっています。


public typealias ObjectBinding = ObservedObject

@ObservedObject
https://developer.apple.com/documentation/swiftui/observedobject

ObservableObject
https://developer.apple.com/documentation/combine/observableobject

Identifiable
https://developer.apple.com/documentation/swift/identifiable


class ViewModel: ObservableObject {

    let willChange = PassthroughSubject<ViewModel, Never>()

    var isEnabled = false {
        willSet {
            willChange.send(self)
        }
    }
}

struct ContentView : View {

    @ObservedObject var viewModel: ViewModel

    var body: some View {
        Toggle(isOn: $viewModel.isEnabled) {
            Text("The value is " + "\(viewModel.isEnabled)")
        }
    }
}

(2019/7/31 追記) @Publishedで自動通知をするようになりました。

Xcode11 Beta5より
@Publishedを使用すると上記のようなwillChangeの記載する必要がなくなりました。


import Combine
import SwiftUI
class ViewModel: ObservableObject {
    @Published var isEnabled: Bool = false
}

struct ContentView : View {

    @ObservedObject var viewModel: ViewModel

    var body: some View {
        Toggle(isOn: $viewModel.isEnabled) {
            Text("The value is " + "\(viewModel.isEnabled)")
        }
    }
}

※(2020/8/8 追記)

ObservedObjectで自らインスタンスを生成すると
いくつかの問題が起きる可能性があります。(下記StateObject参照)

また@StateObjectの登場により
少し役割が変わってきた印象があり
@ObservedObject
親Viewで生成された
@StateObjectが宣言されたObservableObject
を子Viewに渡すことで
子Viewでもその値を読み書きできるようにしたい場合などに
使用されます。

こうすることで
親Viewが保持しているものが
唯一の変更点とすることができ
いわゆるSingle Source of Truthが保証できます。

さらに
使用する場合は
親Viewとその直系の子Viewとの間で使用する
のが良いようです。

それ以上に深い階層に渡す必要がある場合は
下記の@EnvironmentObjectを使用します。

environmentObjectと@EnvironmentObject

Environmentとは

「SwiftUI Essentials」のセッションで出てきましたが
SwiftUIの特徴の1つで
Viewの階層の中でコンテキストを共有するコンテナのような存在です。

スクリーンショット 2019-06-13 8.45.15.png

SwiftUI Essentials
https://developer.apple.com/videos/play/wwdc2019/216/

environmentObjectと@EnvironmentObjectを使うと
上記のEnvironmentの中にカスタムな値を設定できます。

environmentObjectを使用したViewのEnvironment内で
引数に渡したObservableObjectの値を
@EnvironmentObjectから参照できます。

@EnvironmentObject
https://developer.apple.com/documentation/swiftui/environmentobject

environmentObject
https://developer.apple.com/documentation/swiftui/view/3308681-environmentobject


class ViewModel: ObservableObject {

    let willChange = PassthroughSubject<ViewModel, Never>()

    var isEnabled = false {
        willSet {
            willChange.send(self)
        }
    }
}

struct ContentView : View {

    @EnvironmentObject var viewModel: ViewModel

    var body: some View {
        Toggle(isOn: $viewModel.isEnabled) {
            Text("The value is " + "\(viewModel.isEnabled)")
        }
    }
}

let view = ContentView().environmentObject(ViewModel())

@ObservedObjectは子Viewへデータを渡すのに
initを使用してViewの階層を辿っていく必要がありましたが

@EnvironmentObjectを使うとその必要なしに値を参照することができます。

内部的に@EnvironmentObjectdynamic member lookupを使用しており
そのEnvironmentのObservableObjectを探してくれます。

スクリーンショット 2019-06-12 4.32.38.png

使いどころ

Environment全体の設定に関わる値など
@ObservedObjectよりも広く
アプリ全体で使用する必要があるようなデータに適しているようです。

@Environment

EnvironmentValuesという事前に定義されているプロパティの値を使って
Environment全体にまたがるような設定の取得、更新ができます。

Environment
https://developer.apple.com/documentation/swiftui/environment

EnvironmentValues
https://developer.apple.com/documentation/swiftui/environmentvalues


struct ContentView : View {

    @Environment(\.isEnabled) var enabled: Bool

    var body: some View {
        Text("The value is " + "\(enabled)")
    }
}

値の変更も可能です。

スクリーンショット 2019-06-13 8.50.21.png

使いどころ

上記でも記載した通り
EnvironmentValuesで定義されている値を参照するために使用します。

@Binding

値を参照する側のプロパティに宣言をすることで
親Viewの@State
下記で紹介する@ObservedObject @Binding
値の更新通知を受け取ることができます。

@Binding
https://developer.apple.com/documentation/swiftui/binding

@Bindingでは値を参照します。

ViewはValue Typeで
親Viewから子Viewへ値を直接渡してもコピーを渡してしまいますが、
@Bindingを使用することで
Value Typeへの参照を渡して親子間でデータの同期を保つことができます。

スクリーンショット 2019-06-12 4.53.48.png

使いどころ

ViewはImmutableであるのが好ましいものの
値の更新をする必要がある場合などは
基本的には@Bindingを使うのが良いとセッションでは言っています。

@GestureState 2019/7/17追記

特定のViewへのジェスチャーイベントを受け取ることができます。
注意点としては
イベント終了後に変化した値は保持されないという点です。

使い方は受け取りたいイベントの変数を作成し
gestureを使用します。


struct ShapeTapView: View {
    var body: some View {
        let tap = TapGesture()
            .onEnded { _ in
                print("View tapped!")
            }

        return Circle()
            .fill(Color.blue)
            .frame(width: 100, height: 100, alignment: .center)
            .gesture(tap)
    }
}

コールバックを受け取るには3つの方法があります。

updating(_:body:)

ジェスチャーの変化ごとにViewを更新していく場合などに使用します。(例えばズームやドラッグなど)
ジェスチャーが完了したりキャンセルされるとコールバックを受け取られなくなります。

この値はジェスチャー中だけ保持され
ジャスチャーが止まると値はリセットされます。

下記の例はロングプレス中は円の色が変わりますが
指を離すと色は元の色に戻ります。


struct CounterView: View {
    @GestureState var isDetectingLongPress = false

    var body: some View {
        let press = LongPressGesture(minimumDuration: 1)
            .updating($isDetectingLongPress) { currentState, gestureState, transaction in
                gestureState = currentState
            }

        return Circle()
            .fill(isDetectingLongPress ? Color.yellow : Color.green)
            .frame(width: 100, height: 100, alignment: .center)
            .gesture(press)
    }
}

onChanged(_:)

ジャスチャーの値が変化した時にコールバックを受け取ることができます。
ジェスチャーが完了した後も変化した値を保持したい場合に@Stateと組み合わせて使用します。

下記の例はロングプレスをしている間はカウントが上がっていきます。
@StatetotalNumberOfTaps
ロングプレスが終わった後も値を保持するようにしています。


struct CounterView: View {
    @GestureState var isDetectingLongPress = false
    @State var totalNumberOfTaps = 0

    var body: some View {
        let press = LongPressGesture(minimumDuration: 1)
            .updating($isDetectingLongPress) { currentState, gestureState, transaction in
                gestureState = currentState
            }.onChanged { _ in
                self.totalNumberOfTaps += 1
            }

        return VStack {
            Text("\(totalNumberOfTaps)")
                .font(.largeTitle)

            Circle()
                .fill(isDetectingLongPress ? Color.yellow : Color.green)
                .frame(width: 100, height: 100, alignment: .center)
                .gesture(press)
        }
    }
}

onEnded(_:)

ジェスチャーが完了後に最終的な値を受け取ることができます。
成功した場合のみコールバックを受け取ることができます。

例えばロングプレスのようなジェスチャーをしたものの
設定しているminimumDurationに到達していない場合や
maximumDistanceを超えて動かした場合は
コールバックを受け取ることはできません。

minimumDuration
https://developer.apple.com/documentation/swiftui/longpressgesture/3270364-minimumduration

maximumDistance
https://developer.apple.com/documentation/swiftui/longpressgesture/3077970-maximumdistance

下記の例は
ロングプレスをしている間はカウントを上げていきますが
指を離すとカウントは上がらなくなります。


struct CounterView: View {
    @GestureState var isDetectingLongPress = false
    @State var totalNumberOfTaps = 0
    @State var doneCounting = false

    var body: some View {
        let press = LongPressGesture(minimumDuration: 1)
            .updating($isDetectingLongPress) { currentState, gestureState, transaction in
                gestureState = currentState
            }.onChanged { _ in
                self.totalNumberOfTaps += 1
            }
            .onEnded { _ in
                self.doneCounting = true
            }

        return VStack {
            Text("\(totalNumberOfTaps)")
                .font(.largeTitle)

            Circle()
                .fill(doneCounting ? Color.red : isDetectingLongPress ? Color.yellow : Color.green)
                .frame(width: 100, height: 100, alignment: .center)
                .gesture(doneCounting ? nil : press)
        }
    }
}

参考ドキュメント
https://developer.apple.com/documentation/swiftui/gestures/adding_interactivity_with_gestures

(2020/6/28追記)@AppStorage

UserDefaultsの値の読み書きができます。
この値が変更されるたびにSwiftUIのViewの再描画が発生します。


struct ContentView: View {
    @AppStorage("name") var name: String = "Someone"

    var body: some View {
        VStack {
            Text("Hello, \(name)!")

            Button("say hello") {
                self.name = "Tom"
            }
        }
    }
}

これは以前と同じ方法でもViewの更新は発生するようです。


UserDefaults.standard.set("Tom", forKey: "name")

デフォルト値やstandard以外のUserDefaultsも指定できます。


@AppStorage("name", store: UserDefaults(suiteName: "shiz.otherSuite")) 
var name: String = "Someone"

@AppStorage(wrappedValue: "Bob", "name") 
var name: String = "Someone"

使いどころ

変更のたびにViewの更新が発生してしまうため
変更がアプリ全体に影響するもので
頻繁に更新が発生しないものなどに良いのではないかと言われています。

参考ドキュメント
https://developer.apple.com/documentation/swiftui/appstorage

(2020/6/28追記)@SceneStorage

Scene単位で値が共有できる仕組みで
フレームワーク側で
SceneStorageの値を管理し
初期化時に前回保持していた値があった場合は
自動で設定をしてくれるようです。

@AppStorageとは異なり
UserDefaultsを利用はせず
内部に値を保持しているようです。

これまでにもあった
state restoration
同じような仕組みであると思っています。

例えば


@SceneStorage("text") var text = ""

と宣言すると
利用している全てのScene
それぞれtextのコピーを保持するようになります。

ただし
注意点もあるようです。

必ず保存されるという保証はない

The system makes no guarantees 
as to when and how often the data will be persisted.

とあり
データが必ず保存されることは保証されていないようです。

大きいデータは保存しない

Ensure that the data you use with SceneStorage is lightweight. 

とあり
大きなデータを保存すると
パフォーマンスに影響があるので避けるように
と書かれています。

Sceneがなくなるとデータも消える

If the Scene is explictly destroyed, 
the data is also destroyed.

とあり
Sceneが解放される(Switcherからプロセスをキルするなど)と
中身データも削除されるようです。

センシティブなデータは保存しない

Do not use SceneStorage with sensitive data.

とあります。

これは外部から見られる可能性があるからだと思います。

使いどころ

マルチウィンドウのアプリの場合
複数のSceneを利用することがありますが
その際に各Sceneの状態を保持することなどに利用します。

参考ドキュメント
https://developer.apple.com/documentation/swiftui/scenestorage

(2020/6/28追記)@StateObject

Viewはstructであるために
参照を保持するために @ObservableObject を使用していました。

しかし
下記のような場合に問題が発生します。


import SwiftUI

final class Counter: ObservableObject {
    @Published var count: Int = 0
}

struct CounterView: View {
    @ObservedObject var counter = Counter()

    var body: some View {
        VStack {
            Text("CounterView")
            Text("\(counter.count)")
            Button("increment") {
                counter.count += 1
            }
        }
        .padding()
        .background(Color.yellow)
    }
}

struct ContentView: View {
    @State var count: Int = 0

    var body: some View {
        VStack {
            Text("ContentView")
            Text("\(count)")
            CounterView()
            Button("increment") {
                count += 1
            }
        }
        .padding()
        .background(Color.red)
    }
}

CounterViewのボタンを押すと
CounterViewcountは増加しますが
その後
ContentViewのボタンを押すと
CounterViewcountはクリアされてしまいます。

これは
ContentViewText
@Stateの値を利用しているため
@Stateの変化に応じて
ContentViewは再描画が発生します。

その際に


@ObservedObject var counter = Counter()

の部分でCounterが初期化されてしまうからです。

この問題を解消するためには
@ObservedObject@StateObjectに置き換えます。

その際に


@StateObject var counter = Counter()

@StateObject
一度初期化されると
フレームワーク側で参照を保持し続けてくれるようになり
再描画ごとに初期化が発生することがなくなります。

そして
内部に@ObservedObjectを保持しており
利用される際には@ObservedObjectの値を利用します。

使いどころ

ローカルの状態を保持したい時に
これまで@ObservedObjectを利用しようとしていたところで
@StateObjectを利用するのが良いのではないかと思います。

一方で
他からイニシャライザを経由して利用するような場合は
これまで通り@ObservedObjectを利用することで
よりコードを読んでいる人へ意図を伝えやすくなるのではないかと思います。

参考ドキュメント
https://developer.apple.com/documentation/swiftui/stateobject

下記のセッションで詳しく紹介されています。
https://developer.apple.com/videos/play/wwdc2020/10040/

セッションの内容を見ていく

ここからはセッションの内容についてもう少し見ていきたいと思います。

データフローの原理

データを読み込む全てのViewは依存関係を構築する

データの更新が発生するとViewが更新され
データとViewの間には依存関係が生まれます。

データを読み込むViewは情報源を持っている

データには発生元が必ず存在し
Viewはそれに対しての参照を持っています。

Single Source Of Truthを目指す

情報源はいくつも生成することができますが
特定の情報に対する情報源は1つであるべきです。

そうしないとデータの不整合などのバグを生み出す要因になります。

変更が加えられたときはその唯一の情報源から変化を伝達させるようにします。

スクリーンショット 2019-06-12 5.21.24.png
スクリーンショット 2019-06-12 5.21.37.png
スクリーンショット 2019-06-12 5.23.39.png

SwiftUIの更新の仕組み

SwiftUIは上記のデータフローの原理を簡単に実現できるようになっています。

Viewstructなので自身の値を更新できません。
その代わりに
CombineフレームワークのPublisher
Property Wrapperを使ってデータの更新をViewに伝達します。

これらを利用することで
データの流れが一方向になり予測可能で理解しやすくします。

スクリーンショット 2019-06-12 5.15.50.png

これまではデータの同期を取るために
UIViewControllerが必要になりましたが
SwiftUIではその必要もなくなりよりシンプルな仕組みになっています。

スクリーンショット 2019-06-12 5.18.25.png

それではそれぞれの伝達方法について見ていきたいと思います。

CombineフレームワークのPublisher

イベント駆動型の非同期処理を簡単に扱えるようにしたフレームワーク
https://developer.apple.com/documentation/combine

PublisherがEmitしたイベントをonReceiveで受け取ることができます。
https://developer.apple.com/search/?q=onReceive

スクリーンショット 2019-06-12 5.23.39.png

Combineフレームワークについてはこちらにも書かせていただきました。
https://qiita.com/shiz/items/5efac86479db77a52ccc

Property Wrappers

Swift5.1で導入された新しい機能で
冒頭で紹介している@State @ObservedObject @BindingなどもProperty Wrappersです。
アノテーション(@)を使って宣言したプロパティへの独自のアクセス方法を定義することができます。

そしてまだ今後に向けて議論は続いているようです。
https://forums.swift.org/t/pitch-3-property-wrappers-formerly-known-as-property-delegates/24961/201

※以前は Property Delegatesと呼ばれていました。

これを使用することで
共通のデータアクセス方法を一箇所で定義でき
重複コードの削減をすることができます。

スクリーンショット 2019-06-12 8.57.22.png

「Modern Swift API Design」セッションで
仕組みが詳しく解説されています。

内部でStored Propertyを持ち
それに対応したComputed Propertyで
Stored Propertyにどうアクセスするのかを定義します。


@propertyWrapper
public struct LateInitialized<Value> {
    private var storage: Value?

    public init() {
        storage = nil
    }

    public var value: Value {
        get {
            guard let value = storage else {
                fatalError("value has not yet been set!")
            }
            return value
        }
        set {
            storage = newValue
        }
    }
}

public struct MyType {

    @LateInitialized public var text: String

    // Compiler-synthesized code...

    var $text: LateInitialized<String> = LateInitialized<String>()
    public var text: String { get { $text.value }
        set { $text.value = newValue } }

}

初期値を渡したり
extensionで新しくinitを定義することなども可能です。


@propertyWrapper
public struct DefensiveCopying<Value: NSCopying> {
    private var storage: Value

    public init(initialValue value: Value) {
        storage = value.copy() as! Value
    }

    public var value: Value {
        get { storage }
        set {
            storage = newValue.copy() as! Value
        }
    }
}

@DefensiveCopying public var path: UIBezierPath = UIBezierPath()

extension DefensiveCopying {
    public init(withoutCopying value: Value) {
        storage = value
    }
}

public struct MyType {
    @DefensiveCopying(withoutCopying: UIBezierPath())
    public var path: UIBezierPath
}

Modern Swift API Design
https://developer.apple.com/videos/play/wwdc2019/415

「What's New in Swift」セッションの中でも
UserDefaultsへのアクセスパターンが紹介されていました。


@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T

    init(_ key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var value: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

@UserDefault("USES_TOUCH_ID", defaultValue: false)
static var usesTouchID: Bool
@UserDefault("LOGGED_IN", defaultValue: false)
static var isLoggedIn: Bool
if !isLoggedIn && usesTouchID {
 !authenticateWithTouchID()
}

What's New in Swift
https://developer.apple.com/videos/play/wwdc2019/402/

追記

この後にvalueはwrappedValueに変更されています。


@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T

    init(_ key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

さらにprojectedValueを定義することで
を通して変数へアクセスをすることができるようになりました。

例えば@Stateの場合
Bindingでラップされた値へアクセスすることができるようになります。


@propertyWrapper public struct State<Value> : DynamicProperty {
    ...
    /// Produces the binding referencing this state value
    public var projectedValue: Binding<Value> { get }
}

まとめ

SwiftUIのProperty Wrappersと
データへのアクセス方法について見てきました。

まずはセッションの動画から
どういう使われ方が期待されているのか
注意するべきところはどういうところなのか
などを少し理解することはできたのかなと思っています。

まだまだチュートリアル程度しか触れていないので
これから実践していく中で色々な発見をするのが楽しみです!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
225