@ViewBuilder content: () -> Content
ってなってるstatic func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)>
buildDo
ってのもあるpublic protocol Identifiable
差分計算みっけextension Optional : Publisher where Wrapped : Publisher {
ええんかwVStack
のtrailing closure で if
文かけるのなぜかわからないのですが説明できる人います? var body: some View { VStack(alignment: .center) { Text("1") if true { Text("2") } Text("3") } }
Text?
になるってコンパイル時にわからなくない。というかTupleの要素数決定できないなと思っていて。それが if
をかいて buildif
が使われる言語機能?って心当たりがあるプロポーザルとかあります? きっとSwiftUIだから特別構文を用意しているとかではないと思っているのですがdiv { var v0_opt: [HTML]? if useChapterTitles { let v0: [HTML] = HTMLBuilder.buildExpression(h1(chapter + "1. Loomings.")) v0_opt = v0 } let v0_result = HTMLBuilder.buildOptional(v0_opt) let v1 = HTMLBuilder.buildExpression(p { "Call me Ishmael. Some years ago" }) let v2 = HTMLBuilder.buildExpression(p { "There is now your insular city" }) return HTMLBuilder.buildBlock(v0_result, v1, v2) }
Jetpack Compose
について調査しました。本記事ではその中でも特徴的な宣言的UI
の構築を行うための文法についてまとめてみます。 edgesIgnoringSafeArea
IgnoringのためのAPIがあるということは、ScrollViewとか関係なく、全てViewは標準だとSafeAreaの中にレンダされるのかなList
でも普通にやったら SafeArea 考慮されてて、 edgesIgnoringSafeArea
で SafeArea 無視されてましたね。fn main() { let x = 5; let x = x + 1; let x = x * 2; println!("The value of x is: {}", x); }
https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing
mut let
のときと let
でシャドーイングのときを区別しないといけなくて、 Swift 的思考だとハマりそう。 (edited)padding
)はインスタンス生成してないとできない。 Text(string) .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background(Color.white) .cornerRadius(16) .padding(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10))
GeometryReader
使ってできそう。もっといいやり方ありそうだけど。 struct ContentView : View { var body: some View { VStack { GeometryReader { geometry in VStack { Spacer() .frame( width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.safeAreaInsets.top ) .background(Color.blue) ZStack { Spacer() .frame( width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: 100 ) .background(Color.red) Text("Custom Bar") } } .edgesIgnoringSafeArea(.init([ .top, .leading, .trailing ])) } } } }
(edited)layoutPriority
とかもあるみたいだから、かなりのことはできそう。 https://developer.apple.com/documentation/swiftui/view/3278584-layoutpriorityGeometryReader
胸熱struct ContentView : View { @State var int = 0 var body: some View { let red = int > 0 Text("Hello World") .background(red ? Color.red : Color.blue) } }
↑はビルドエラーになるけど struct ContentView : View { @State var int = 0 var red: Bool { int > 0 } var body: some View { Text("Hello World") .background(red ? Color.red : Color.blue) } }
ならOKですねImage("owl") .resizable() .edgesIgnoringSafeArea([.top, .horizontal])
^ これを Image("owl") .edgesIgnoringSafeArea([.top, .horizontal]) .resizable()
^ このように入れ替えられない、とかけっこう難しいと思うんですよね。@State
なpropertyが中身のオブジェクトのアクセサ持ってるの、これ使ってるんですね https://twitter.com/v_pradeilles/status/1136755695427018752?s=21List
row in #SwiftUI that forwards the tap events to controls in the cell? (i.e. List { VStack { Text(..) Button(action: ..) { Text("Press me") } } }
` Trying to tap the button just selects the celldynamic
がPure Swiftのメソッド・プロパティにも付けられるようになってて、_@dynamicReplacememt
で差し替えられる機構がホットリロードに使われてるようで面白い! https://forums.swift.org/t/how-does-the-hot-reloading-work-in-xcode11/25312
https://forums.swift.org/t/dynamic-method-replacement/16619dynamic
と宣言したモノだけか。Binding<Value>
の .value
-> wrappedValue
に変更されているみたいだけど、Docに反映されていなそう? https://developer.apple.com/documentation/swiftui/binding?changes=latest_beta (edited)SwiftUI.framework/Versions/A/Modules/SwiftUI.swiftmodule/x86_64.swiftinterface
を比較しても、その辺り変わっていない様な。-fprofile-instr-generate
を追加する必要がある」この原理を考察してみました(多分間違ってる)。 考察に納得できてないので、もし知ってることがあれば教えていただきたいです!(この窓で良いのかな…) https://qiita.com/AkkeyLab/items/a698545f2423b9b1dec9$ lipo -extract x86_64 FirebaseCore -output FirebaseCoreX64 $ nm -a FirebaseCoreX64 | grep -i llvm
ダウンロードしたFirebaseCoreには含まれてなさそう。Firebase/Analytics
を pods で入れてます。___llvm_profile_runtime_user
in FirebaseCore
なので、英文的には、見つからなかったのはFirebaseCoreじゃなくて、 FirebaseCoreの中から 参照している llvm_profile_runtime_user
が、見つからなかった、ですね。import SwiftUI struct ContentView: View { var message: String @State var isOn = true var body: some View { VStack { VStack { Toggle(isOn: $isOn) { Text("Switch") .font(.title) .foregroundColor(Color.white) } } .padding() .background(isOn ? Color.purple : Color.orange) Text(message) .font(.largeTitle) .foregroundColor(.blue) } } } struct ContentView_Preview: PreviewProvider { static var previews: some View { ContentView(message: "Hello SwiftUI Playground!") } }
import SwiftUI struct ContentView: View { var body: some View { VStack { // 1. Rectangle() .stroke(Color.blue, lineWidth: 10) .frame(width: 100, height: 100) // 2. Circle() .fill(Color.red) .frame(width: 100, height: 100) // 3. Capsule() .fill(Color.green) .overlay( Capsule() .stroke(Color.black, lineWidth: 10) ) .frame(width: 200, height: 100) // 4. RoundedRectangle(cornerRadius: 20) .fill(Color.yellow) .frame(width: 100, height: 100) } } } struct ContentView_Preview: PreviewProvider { static var previews: some View { ContentView() } }
struct ContentView_Preview: PreviewProvider { static var previews: some View { ContentView() .environment(\.locale, .init(identifier: "ja")) } }
とすると手元ではOKだけどサーバーでは変わらない。 日本語にフォールバックされる要件が相変わらずよくわからない。.environment(\.locale, .init(identifier: "ja"))
関係なかった。PreviewProvider.previews
をMirrorを使ってリフレクションで構造を読み取ります。 modifierの指定を集めるのと、Groupを探してGroupがあったらXcodeの挙動と一致するように分割します。some View
の方がわからないと渡せないので上記のリフレクションで構造を読む際に実際の型名を書き込んだソースコードを生成して、再度ビルドして実行します。 2回目の実行で画像が出力されます。Text("\(Date())")
みたいなコードがあったら文字列としては一致してるけど結果は変わっちゃうとか。import SwiftUI extension View { func when<NewView: View>(_ cond: Bool, @ViewBuilder then apply: (Self) -> NewView) -> some View { Group { if cond { apply(self) } else { self } } } } struct MyView: View { var body: some View { Text("a") // .when(true) { // $0 // } // .when(Bool.random()) { // $0.padding(10).background(Color.yellow) // } // .when(Bool.random()) { // $0.padding(10).background(Color.green) // } .when(true) { view in VStack { Text("begin") view Text("end") } } } } struct MyView_Preview: PreviewProvider { static var previews: some View { MyView() } }
import SwiftUI extension View { func when<NewView: View>(_ cond: Bool, @ViewBuilder then apply: (Self) -> NewView) -> some View { Group { if cond { apply(self) } else { self } } } } struct MyView: View { var body: some View { Text("a") .when(true) { $0 } .when(true) { view in VStack { Text("begin") view Text("end") } } } } struct MyView_Preview: PreviewProvider { static var previews: some View { MyView() } }
22:17: error: cannot convert value of type 'VStack<Content>' to closure result type '_' VStack { ^~~~~~~~
Combine.swiftmodule/x86_64.swiftinterface
のdiffを見ると、いくつかのextensionに@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
が追加されてて、 +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension Subject where Self.Output == Swift.Void { public func send() }
ってのも含まれてる。 (edited)nm -mg
でも (undefined) weak external (extension in Combine):Combine.Subject< where A.Output == ()>.send() -> () (from Combine)
になる。swiftinterface
内で@available
が付いていないのを全部チェックしないといけないのですね。@available
よりextensionの@available
が緩かったらコンパイラでエラーにして欲しいな。edgesIgnoringSageArea
にすると(そもそも Safe Area がなくなるので)スクロールしてない状態でも最初から Safe Area の外にはみ出てしまうという状況です。 SwiftUI でうまくやるにはどうすれば良いでしょうか?View
の再利用とかめちゃくちゃやりやすいですしねぇ。いいところはいいんですが・・・。今回は、 Table View ( List
)のセルの操作が結構入りそうな箇所があり、そこを差分計算でやってもらった方が楽そうだなと。そこだけ SwiftUI 埋め込みする方針で検討してみます。UITableViewDiffableDataSource
とかできてたんですね。把握できてませんでした。部分的にでも SwiftUI 使ってみたい気持ちもありますが、これも検討してみます。UIViewController
ベースにして、使えそうなところは UIHostingViewConroller
に入れて使う形を試してたら、これまで GeometryReader
の中にあった NavigationView
を UINavigationController
として外に出すことになって、そうすると geometry が変化しまくって GeometryReader
以下の再描画が走りまくることに・・・。UIHostingViewController
使わずに UIView
ベースにするとなんか解決しそうですね...!import Combine class Foo: ObservableObject { @Published var a: Int = 0 @Published var b: String = "" static let shared: Foo = .init() }
import SwiftUI struct ContentView: View { @ObservedObject var foo: Foo = .shared let cancellable = Foo.shared.objectWillChange.sink { _ in print("change") } var body: some View { print("body") return VStack { Text("a: \(foo.a)") Button("+") { Foo.shared.a += 1 } Text("b: \(foo.b)") Button("+") { Foo.shared.b += "+" } Button("++") { Foo.shared.a += 1 Foo.shared.b += "-" } } } }
↑のようにして、 ++
ボタンを押したときに Foo
の objectWillChange
には 2 回値が流れる( "change"
は 2 回表示される)けど、 "body"
は 1 回しか表示されない。なんとなく、 @State
とか @ObservedObject
とかは変更が生じる度に body
が実行されるのだと思ってました。当たり前かもしれないけど、ちゃんと setNeedDisplay
みたいな仕組みが備わってるんですね。Binding
(のようなもの)が欲しくなることってないですか?変更を通知して子ビューに反映させたいけど、子ビュー側から変更してほしくないようなケースで。 set
を空にする extension
でも書けば良い? あと、 class Foo: ObservableObject
があったときに、 @ObservedObject var foos: [Foo]
的なことしたくなりませんか? @ObservedArray
とかがあればいいのかな? もしくは、僕が知らないだけで、↑を実現する方法が標準で提供されていたりしますか?軽くググっても見つけられなかったんですが・・・。@Binding
なのを見せないでなにか違うものを見せるのかなあ?import SwiftUI extension Binding { static func readOnly<Value>(_ binding: Binding<Value>) -> Binding<Value> { .init(get: { binding.wrappedValue }, set: { _ in }) } }
ObservableObject
の方で↓みたいに read-only にしたい場合は、 $foo.a
みたいにして Binding
が取れないから不便ですね・・・。 class Foo: ObservableObject { @Published private(set) var a: Int = 0 ... }
@ObservedObject
に projectedValue
以外の read-only のための API を作って、 _foo.readOnly.a
みたいに取得できるようにすればいいのかな。 (edited)import SwiftUI extension ObservedObject { var readOnly: ReadOnlyWrapper { .init(self) } @dynamicMemberLookup struct ReadOnlyWrapper { private let object: ObservedObject<ObjectType> init(_ object: ObservedObject<ObjectType>) { self.object = object } subscript<Subject>(dynamicMember keyPath: KeyPath<ObjectType, Subject>) -> Binding<Subject> { Binding(get: { self.object.wrappedValue[keyPath: keyPath] }, set: { _ in assertionFailure("Read-only") }) } } }
Wrapper
に readOnly
生やして $foo.readOnly.a
みたいな方がいいか。KeyPath
を Wrapper
のための ReferenceWritableKeyPath
に変換できない。 import SwiftUI extension ObservedObject.Wrapper { var readOnly: ReadOnly { .init(self) } @dynamicMemberLookup struct ReadOnly { private let wrapper: ObservedObject<ObjectType>.Wrapper init(_ wrapper: ObservedObject<ObjectType>.Wrapper) { self.wrapper = wrapper } subscript<Subject>(dynamicMember keyPath: KeyPath<ObjectType, Subject>) -> Binding<Subject> { get { wrapper[keyPath: keyPath] } // ⛔ set { assertionFailure("Read-only") } } } }
Wrapper
がラップしてる元のオブジェクトを参照できればいいんだけど・・・。subscript
の get
, set
じゃなくて get
で Binding
の set
をつぶさなきゃいけないか。どっちにしろダメだけど。 (edited)Wrapper
に readOnly
生やして $foo.readOnly.a
は諦めて、少々ブサイク(?)だけど ReadOnlyWrapper
方式で _foo.readOnly.a
にするか。 (edited)$foo.a.readOnly
やんね?たしかにそれならできるかも。a
が read-only のときにそもそも $foo.a
が作れないんだった。.lazy
とのアナロジーで考えるとやっぱ Wrapper
に readonly
付けたい気がするなぁ。大元に付けることで下流の性質が変かする。まあ、 _foo.readOnly
でも同じなのかもしれないけど。$foo.readOnly.a
できた。この方法絶対ダメだけどw import SwiftUI extension ObservedObject.Wrapper { var readOnly: ReadOnly { let object: ObjectType = unsafeBitCast(self, to: ObjectType.self) return ReadOnly(object) } @dynamicMemberLookup struct ReadOnly { private let object: ObjectType init(_ object: ObjectType) { self.object = object } subscript<Subject>(dynamicMember keyPath: KeyPath<ObjectType, Subject>) -> Binding<Subject> { Binding<Subject>( get: { self.object[keyPath: keyPath] }, set: { _ in assertionFailure("Read-only") } ) } } }
Wrapper
はどう考えても元のオブジェクトをラップしてるだけだろうと考えて unsafeBitCast
したら動いたwFoo
のような型に対して @ObservedObject var foo: Foo
を作って、 $foo.readOnly.a
のようにして read-only な片方向のバインディングを作りたいということです。 import Combine class Foo: ObservableObject { @Published private(set) var a: Int = 0 func incrementA() { a += 1 } }
incrementA
が露出してるけど、アップデートのための関数は生成者や internal
にしか暴露されないとかもあり得る。そうでなくても、↑の incrementA
のようにアップデートの方法を限定したいからプロパティが private(set)
になるケースもあって、そのようなケースでもバインディングしたいというニーズも考えられる。NumberDisplay
)は let number: Int
で受け取るから書き込みできなくない?Int
は書き込めない型じゃないかな?Binding<Int>
は書き込めるけど。ContentView
が @ObservedObject
として counter
を保持しているから、counter
が変更されたら ContentView
の body
が走って、NumberDisplay(counter.count)
が作り直されて、NumberDisplay
も更新される。ContentView
が @Published var numbers: [Int]
を持つ ObservableObject
を @ObservedObject
で保持していて、NumberDisplay(numbers[0])
とかで表示していると良くなさそう。numbers
全体を表示してる( ContentView
が numbers.count
個の NumberDisplay
を表示している)なら必要な更新だから問題ないと思う。class Counter: ObservedObject { @Published var count: Int @Published var foo: Foo }
のようにfooも持っていてもこれはclassだから分離されているから良いけど・・・ (edited)class Counter: ObservedObject { @Published var count: Count } struct Count { var value: Int var foo: Foo }
(edited)$counter.count.value
でバインディングしたときに、$counter.count.foo
でバインディングしている側には影響が無いようなシステムを期待しているイメージだった。$counter.count.foo
をバインドしてるView Tree側で、勝手に変更チェックをしてfooが変化していなければ実View(FooView)の更新はしないという事をやるのだと思うけど。 (edited)$counter.count.foo
が書き込みインターフェースを提供しない型になるというのがやりたいことだったけど、 counter.count.foo
($を外す) を使えばいいよねという話で解決したという事か@ObservedObject var counter
で掴んでる根っこのところから、毎回出発するからそれはそうなのか。List { ForEach(text, id: \.self) { user in Text(user) }.onDelete{ offset in self.text.remove(atOffsets: offset) }.onMove{ source, destination in self.text.move(fromOffsets: source, toOffset:destination) } } .onTapGesture { UIApplication.shared.endEditing() }
(edited)extension UIApplication { func endEditing() { sendAction( #selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil ) } }
(edited)clipsToBounds
や masksToBounds
がデフォルト false
なので cornerRadius
を掛けたとしてもちゃんとはみ出すので、それと同じノリでSwiftUIでもはみだしてくれるやろと雑に突撃して撃沈した感じです (↑のSwiftUIに条件を揃えたpaddingによるはみ出しではないですが) (edited)import SwiftUI struct ContentView: View { @ObservedObject var counter: Counter = .shared var body: some View { print("ContentView.body") return NavigationView { VStack { HStack { Text("\(counter.count)") Button("+") { self.counter.increment() } } NavigationLink(destination: AView(count: counter.count)) { Text("Open A") } } } } } struct AView: View { let count: Int var body: some View { print("AView.body") return VStack { HStack { Text("\(count)") Button("+") { Counter.shared.increment() } } NavigationLink(destination: BView(count: count)) { Text("Open B") } } } } struct BView: View { let count: Int var body: some View { print("BView.body") return HStack { Text("\(count)") Button("+") { Counter.shared.increment() } } } } final class Counter: ObservableObject { @Published private(set) var count: Int = 0 func increment() { count += 1 } static let shared: Counter = .init() }
@ObservedObject
の影響範囲を調べてて、↑で AView
を開いてから + ボタン押しても変更が反映されるのがおどろきだったんですが、さらに BView
を開いて + ボタンを押しても変更は反映されないし、そこから Back して AView
に戻ってから + ボタンを押しても今度は変更が反映されないんですが、挙動がおかしくないですか? (edited)AView
を開いたときに変更が反映されるのがおかしいのか、それとも AView
に戻ってきたときや BView
で変更が反映されないのがおかしいのか。Counter.shared
ってSwiftUIシステムから感知できない形で依存性をもってしまっている事じゃないですかねCounter.shared
ってSwiftUIシステムから感知できない形で依存性をもってしまっている事じゃないですかね Counter.shared
はそれをエミュレートしてるのと変わらない気がするので、納得いかないです
@ObservedObject
として引数で渡すとか、 @EnvironmentObject
だっけ? として渡さないといけないのではimport SwiftUI struct ContentView: View { @ObservedObject var counter: Counter = .shared var body: some View { print("ContentView.body") return NavigationView { VStack { HStack { Text("\(counter.count)") Button("+") { self.counter.increment() } } AView(count: counter.count) } } } } struct AView: View { let count: Int var body: some View { print("AView.body") return VStack { HStack { Text("\(count)") Button("+") { Counter.shared.increment() } } BView(count: count) } } } struct BView: View { let count: Int var body: some View { print("BView.body") return HStack { Text("\(count)") Button("+") { Counter.shared.increment() } } } } final class Counter: ObservableObject { @Published private(set) var count: Int = 0 func increment() { count += 1 } static let shared: Counter = .init() } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
@EnvironmentObject
等に限定されているべきで、一般のグローバル変数は触ってはいけないんじゃないかなあ。@EnvironmentObject
でCounterを参照していたとしても、このパターンだとやっぱり更新されない気がしてきた BViewにとってのcountがletなので。@EnvironmentObject
経由で値を見ているべきで、 ContentView → AView → BView の過程の値の加工パスは前提にしちゃいけないような気がするstruct ContentView: View { var body: some View { print("ContentView.body") return NavigationView { XView() } .environmentObject(Env()) } } struct XView: View { @EnvironmentObject var env: Env var body: some View { NavigationLink(destination: YView(), isActive: $env.isRootNavitaionActive) { Text("To Y") } .navigationBarTitle("X") } } struct YView: View { @EnvironmentObject var env: Env var body: some View { VStack { NavigationLink(destination: ZView()) { Text("To Z") } Button(action: { self.env.isRootNavitaionActive = false }) { Text("Tap this") } } .navigationBarTitle("Y") } } struct ZView: View { @EnvironmentObject var env: Env @State var text: String = "(Before tap)" var body: some View { VStack { Text(text) Button(action: { self.text = "(After tap)" self.env.isRootNavitaionActive = false }) { Text("Tap this") } } .navigationBarTitle("Z") } } final class Env: ObservableObject { @Published var isRootNavitaionActive = false }
BView
でボタン押すと count は増えないけど、 ContentView.body
は発火してるんですよね。print
で確認できる。AView
にcount
プロパティを作らなかったら、変更が反映されない。これは理解できる。count
プロパティがないと View
の差分がないので AView
の再描画が走らないのかと。NavigationLink
の場合、クロージャではなく引数に直接 AView(count: counter.count)
が渡されてるから、そこでつながってるのも理解できる。ContentView
が再描画されるんだったら(これは iOS 13 & BView
からでも確認済み)、 AView
が新しい count
で作られて、差分が発生するから再描画されてもおかしくない。struct ContentView2: View { @ObservedObject private var counter: Counter = .shared var body: some View { print("ContentView2.body") return VStack { HStack { Text("\(counter.count)") Button("+") { self.counter.increment() } } AView() .padding() .border(Color.gray, width: 1) .padding() } } } private struct AView: View { var body: some View { print("AView.body") return VStack { HStack { Text("\(Counter.shared.count)") Button("+") { Counter.shared.increment() } } BView() .padding() .border(Color.gray, width: 1) .padding() } } } private struct BView: View { var body: some View { print("BView.body") return HStack { Text("\(Counter.shared.count)") Button("+") { Counter.shared.increment() } } } } private final class Counter: ObservableObject { @Published private(set) var count: Int = 0 func increment() { count += 1 } static let shared: Counter = .init() }
↑これだと同一画面だけど AView
や BView
は更新されない( iOS 13 )。おもしろい。 (edited)AView
に diff がないから body
の発火まで行かないのかと。 (edited)AView
と BView
を展開して ContentView2
に埋め込んだ場合は更新されますよね?body
が呼ばれてないんですが、 body
が呼ばれるかどうかの判断に、ツリー全体じゃなくて AView
としての比較が入ってるってことですよね。Equatable
でないプロパティ持ってることもありますよね・・・。Equatable
の ==
は関係なさそう。import SwiftUI struct ContentView2: View { @ObservedObject private var counter: Counter = .shared var body: some View { print("ContentView2.body") return VStack { HStack { Text("\(counter.count)") Button("+") { self.counter.increment() } } AView(value: .nan) .padding() .border(Color.gray, width: 1) .padding() } } } private struct AView: View { let value: Double var body: some View { print("AView.body") return VStack { HStack { Text("\(Counter.shared.count), \(value)") Button("+") { Counter.shared.increment() } } BView(value: value) .padding() .border(Color.gray, width: 1) .padding() } } } private struct BView: View { let value: Double var body: some View { print("BView.body") return HStack { Text("\(Counter.shared.count), \(value)") Button("+") { Counter.shared.increment() } } } } private final class Counter: ObservableObject { @Published private(set) var count: Int = 0 func increment() { count += 1 } static let shared: Counter = .init() }
AView
と BView
は value
を持っていて、そこに .nan
を渡してるから==
で比較すると常に false
になるんだけど再描画されない。.nan
で常にfalseにする技だwvalue
に Double(counter.count)
を渡したら再描画される。Equatable
なものを比較してたらという仮説に対してですね。AView
と BView
に Equatable
を付けてみても結果は変わりませんでした。AView(value: Bool.random() ? 1 : 2)
import SwiftUI struct ContentView2: View { @ObservedObject private var counter: Counter = .shared var body: some View { print("ContentView2.body") return VStack { HStack { Text("\(counter.count)") Button("+") { self.counter.increment() } } AView(value: Integer(42)) .padding() .border(Color.gray, width: 1) .padding() } } } private struct AView<Value: CustomStringConvertible>: View { let value: Value var body: some View { print("AView.body") return VStack { HStack { Text("\(Counter.shared.count), \(value.description)") Button("+") { Counter.shared.increment() } } BView(value: value) .padding() .border(Color.gray, width: 1) .padding() } } } private struct BView<Value: CustomStringConvertible>: View { let value: Value var body: some View { print("BView.body") return HStack { Text("\(Counter.shared.count), \(value.description)") Button("+") { Counter.shared.increment() } } } } private final class Counter: ObservableObject { @Published private(set) var count: Int = 0 func increment() { count += 1 } static let shared: Counter = .init() } final class Integer: Hashable, CustomStringConvertible { let value: Int init(_ value: Int) { self.value = value } static func == (lhs: Integer, rhs: Integer) -> Bool { lhs.value == rhs.value } func hash(into hasher: inout Hasher) { value.hash(into: &hasher) } var description: String { value.description } }
Equatable
を見てるかも?final class Integer: Hashable, CustomStringConvertible { let value: Int init(_ value: Int) { self.value = value } static func == (lhs: Integer, rhs: Integer) -> Bool { true } func hash(into hasher: inout Hasher) { 42.hash(into: &hasher) } var description: String { value.description } }
に変えて、 AView(value: Integer(Bool.random() ? 1 : 2))
にしたら更新されなくなった。Integer
変更前は AView(value: Integer(Bool.random() ? 1 : 2))
で更新された。Equatable
を見てる??import SwiftUI struct ContentView2: View { @ObservedObject private var counter: Counter = .shared var body: some View { print("ContentView2.body") return VStack { HStack { Text("\(counter.count)") Button("+") { self.counter.increment() } } AView(value: Box<Double>(.nan)) .padding() .border(Color.gray, width: 1) .padding() } } } private struct AView<Value: CustomStringConvertible>: View { let value: Value var body: some View { print("AView.body") return VStack { HStack { Text("\(Counter.shared.count), \(value.description)") Button("+") { Counter.shared.increment() } } BView(value: value) .padding() .border(Color.gray, width: 1) .padding() } } } private struct BView<Value: CustomStringConvertible>: View { let value: Value var body: some View { print("BView.body") return HStack { Text("\(Counter.shared.count), \(value.description)") Button("+") { Counter.shared.increment() } } } } private final class Counter: ObservableObject { @Published private(set) var count: Int = 0 func increment() { count += 1 } static let shared: Counter = .init() } final class Box<Value>: Equatable, CustomStringConvertible where Value: Equatable, Value: CustomStringConvertible { let value: Value init(_ value: Value) { self.value = value } static func == (lhs: Box<Value>, rhs: Box<Value>) -> Bool { lhs.value == rhs.value } var description: String { value.description } }
↑これだと常に更新される。やっぱ ==
が見られてるっぽい。AView(value: Box<Double>(.nan))
を AView(value: Box<Double>(42))
に変更すると更新されなくなる。Equatable
な参照型は ==
が見られるのが確かみたい。Integer
を持つようにしても同じなのかなあとちょっと思っただけです。AView(value: Box<Double>(.nan))
を AView(value: Double.nan)
に変えるだけで参照型を値型にして試せるので。Double.nan
のときは変更が反映されません。Equatable
でないクラス Container
↓を作って final class Container<Value: CustomStringConvertible>: CustomStringConvertible { let value: Value init(_ value: Value) { self.value = value } var description: String { value.description } }
AView(value: Container<Double>(42))
でも AView(value: Container<Double>(.nan))
でも常に更新が反映されました。Equatable
な参照型→ ==
Equatable
でない参照型→アドレス比較(アドレスのビット比較)Equatable
な参照型だけ例外的と言えるかも。AG::LayoutDescriptor::compare_heap_objects(void const*, void const*, unsigned int, bool) AG::LayoutDescriptor::compare_indirect(AG::ValueLayout&, AG::swift::metadata const*, AG::swift::metadata const*, unsigned int, unsigned char const*, unsigned char const*) AG::LayoutDescriptor::compare_existential_values(AG::swift::existential_type_metadata const*, unsigned char const*, unsigned char const*, unsigned int)
#3 0x00007fff4a9b636e in AG::LayoutDescriptor::compare(unsigned char const*, unsigned char const*, unsigned char const*, unsigned long, unsigned int) ()
からジャンプしうるシンボル3種類↑Box<Double>
をジェネリックでない Number
↓に変えても同じだった。 final class Number: Equatable, CustomStringConvertible { let value: Double init(_ value: Double) { self.value = value } static func == (lhs: Number, rhs: Number) -> Bool { print("Box.==") return lhs.value == rhs.value } var description: String { value.description } }
@Environment
や @EnvironmentObject
以外に、コンテクスト的に渡して使えるけど、イミュータブルなものを扱う方法って何かありますか?ダミーのイミュータブルな ObservableObject
を作れば良い?@EnvironmentObject
って型でマッチされるの?? import SwiftUI struct ContentView: View { var body: some View { SubView() .environmentObject(Foo(2)) .environmentObject(Bar(3)) .environmentObject(Bar(5)) } } struct SubView: View { @EnvironmentObject var foo: Foo @EnvironmentObject var bar1: Bar @EnvironmentObject var bar2: Bar var body: some View { VStack { Text("\(foo.value)") Text("\(bar1.value)") Text("\(bar2.value)") } } } final class Foo: ObservableObject { @Published var value: Int init(_ value: Int) { self.value = value } } final class Bar: ObservableObject { @Published var value: Int init(_ value: Int) { self.value = value } }
↑の実行結果は 2 3 3
(edited)import SwiftUI struct ContentView: View { var body: some View { SubView() .environmentObject(Foo(2)) } } struct SubView: View { @EnvironmentObject var foo: Foo var body: some View { VStack { SubsubView() .environmentObject(Foo(5)) Text("\(foo.value)") } } } struct SubsubView: View { @EnvironmentObject var foo: Foo var body: some View { VStack { Text("\(foo.value)") } } } final class Foo: ObservableObject { @Published var value: Int init(_ value: Int) { self.value = value } }
5 2
期待通り@EnvironmentObject
にできるかは一度おいておいて、) NSNumber
二つは紐付けられないけど Int
と Double
ならできるとか、型でマッチするのって違和感があるというか。@EnvironmentObject
は紐付け忘れたらクラッシュだから、そもそも安全なわけじゃないからなぁ。ミスする箇所が減るとは言っても。
@EnvironmentObject
を使う箇所を通過しないとクラッシュしないことないかな?たとえば、ボタン押したクロージャの中から参照してるとか。@ObservedObject
との比較で言えば、 @EnvironmentObject
って @ObservedObject
を都度渡してチェーンするのの簡略版だと思うんだけど、 @ObservedObject
なら同じ型のものを複数持てるけど、 @EnvironmentObject
だとそれができないというところに、「簡略版」以上の不必要な意味を持ってしまっているというか・・・。@EnvironmentObject
で管理しようとすると破綻しない?class Style: EnvironmentObject { var textColor: Color var fontSize: Int }
(edited)@EnvironmentObject
を前提とするとそういう使い方になるんだけど、@EnvironmentObject
が型でマッチする設計になっていることに違和感を感じているという話です。EnvironmentKey
の便利版みたいな位置づけだといいのかなぁ・・・。@EnvironmentObject
で管理しようとすると破綻しない? @Environment
でやることを想定してるんじゃないかなあ。
@EnvironmentObject
を使う場合は、型でマッチすることを割り切って使う場合って感じでしょうか・・・。@Environment
だとデフォルト値があるので、付与し忘れてクラッシュがないという違いはありそうですね。List
と NavigationLink
使ってるときに、子側をトリガーとして変更が発生したときに、親の List
が再描画されると子が pop されて親 View
まで戻される現象が発生した・・・。これは許容し難い。どういうケースで起こるのか最小構成作って試してみないと・・・。NavigationLink
の発行元が消滅したから?List
で表示してて、アイテムを選択してアイテムページに遷移して、そこで閲覧ボタンを押すと履歴が書き換わって pop される・・・。NavigationLink
の同一性みたいなのがあるのかな・・・。PassthroughSubject
経由で更新通知を発火してるから、入れ替えてる途中に消えてる瞬間もないはずなんだよなぁ。body
が一度しか発火されてないことを確認してみるか。NavigationLink
使わずに UINavigationController
経由で push
させれば回避できた。NavigationView
にせずに UINavigationController
にしておいて、カスタム @Environment
に渡してある。NavigationLink
鬼門ですね・・・ (edited)NavigationLink
の isActive:
のBindingをとる版とか、 tag:
と selection:
版とかだとどれが開いているかっていうStateがこっちが与えたBindingに保持されますが、ないやつはどこが覚えてるんでしょうね。isActive
に用語を合わせるなら、どれがアクティブか) (edited)NavigationLink
ってそれ自体にはID渡さないですよね。body
はたしかに走ってるんだけど、 pop されない。Identifiable
の id
でも見ないと判定できなくないですか?id
同じ NavigationLink
があれば pop しないでほしいけど・・・。@State
で持たせた変数って、そのViewが List
の中に入ってて、そのリスト上の位置が変わったときって、状態は保持されてるんですかね。@State
で状態を持っていて、その親のViewの関係ないところに更新が走ったとしても、 @State
で持っている状態は保持されないと困りますよね。でも、親のViewの body
は呼ばれるはずだから、子Viewだってイニシャライザが走って別物になっているはず。でも、 @State
はどこかで同一性を判定して覚えてくれてるはず。List
じゃないケースだと、イニシャライザが走っても @State
が保持されてるのは確認したことがあります。import SwiftUI import Foundation import Combine struct ContentView: View { @ObservedObject var history: History = .shared let dateFormatter: DateFormatter = { let value = DateFormatter() value.dateFormat = "yyyy-MM-dd HH:mm:ss" return value }() let items: [Item] = [ Item(id: "A"), Item(id: "B"), Item(id: "C"), ] var body: some View { NavigationView { VStack { VStack { ForEach(items) { item in NavigationLink(destination: ItemView(item: item)) { Text("Open \(item.id)") .padding(4) } } } .padding() Text("履歴") .font(.headline) .padding() Divider() List(history.records.reversed()) { record in NavigationLink(destination: ItemView( item: self.items.first(where: { $0.id == record.id })! )) { VStack(alignment: .leading) { Text("\(record.id)") .font(.headline) Text("\(self.dateFormatter.string(from: record.lastPlayed))") .font(.subheadline) .foregroundColor(.secondary) } } } } } } } struct ItemView: View { let item: Item var body: some View { VStack { Text("\(item.id)") Button("Play") { History.shared.record(for: self.item.id) } } } }
struct Item: Identifiable { let id: String } final class History: ObservableObject { private(set) var records: [Record] = [] private let subject: PassthroughSubject<Void, Never> = .init() var objectWillChange: AnyPublisher<Void, Never> { subject.eraseToAnyPublisher() } func record(for id: Item.ID) { records.removeAll(where: { $0.id == id }) records.append(Record(id: id, lastPlayed: Date())) subject.send(()) } static let shared: History = .init() struct Record: Identifiable { let id: Item.ID let lastPlayed: Date } }
struct ContentView: View { var body: some View { NavigationView { FirstView() } .environmentObject(Env(items: [ Item(id: "C"), Item(id: "B"), Item(id: "A"), ])) } } struct FirstView: View { @EnvironmentObject var env: Env var body: some View { List(env.items) { item in NavigationLink(destination: SecondView(id: item.id)) { Text("\(item.id)") } } } } struct SecondView: View { @EnvironmentObject var env: Env let id: String init(id: String) { self.id = id } var body: some View { Button("\(id)") { self.env.items = [ Item(id: "A"), Item(id: "C"), Item(id: "B"), ] } } } struct Item: Identifiable { let id: String } final class Env: ObservableObject { @Published var items: [Item] init(items: [Item]) { self.items = items } }
↑だいぶ単純化しましたが再現しますね。(Aの行をタップして、遷移先でAをタップ)struct ContentView: View { var body: some View { NavigationView { FirstView() } .environmentObject(Env(items: [ Item(id: "C"), Item(id: "B"), Item(id: "A"), ])) } } struct FirstView: View { @EnvironmentObject var env: Env var body: some View { VStack { Button("move") { self.env.items = [ Item(id: "A"), Item(id: "C"), Item(id: "B"), ] } List(env.items) { item in ItemView(id: item.id) } } } } struct ItemView: View { let id: String @State var flag: Bool = false var body: some View { HStack { Text("\(id)") Button(flag ? "true" : "false") { self.flag.toggle() } } } } struct Item: Identifiable { let id: String } final class Env: ObservableObject { @Published var items: [Item] init(items: [Item]) { self.items = items } }
↑ぼくの疑問だった @State
は並び替えてもちゃんと保たれている様子。(falseの部分をタップするとtrueとfalseがトグルするので適当に変更してからmoveを押して並び替えてもtrue, falseは保たれて並び変わる)import SwiftUI struct ContentView: View { var body: some View { NavigationView { FirstView() } .environmentObject(Env(items: [ Item(id: "C"), Item(id: "B"), Item(id: "A"), ])) } } struct FirstView: View { @EnvironmentObject var env: Env var body: some View { VStack { Button("move") { self.env.items = [ Item(id: "A"), Item(id: "C"), Item(id: "B"), ] } List(env.items) { item in ItemView(id: item.id) } } } } struct ItemView: View { let id: String @State var flag: Bool = false var body: some View { NavigationLink(destination: SecondView(id: id)) { HStack { Text("\(id)") Text(flag ? "true" : "false") .onTapGesture { self.flag.toggle() } } } } } struct SecondView: View { @EnvironmentObject var env: Env let id: String init(id: String) { self.id = id } var body: some View { Button("\(id)") { self.env.items = [ Item(id: "A"), Item(id: "C"), Item(id: "B"), ] } } } struct Item: Identifiable { let id: String }
NavigationLink
で遷移して強制的にpopされるパターンだと @State
で覚えてるやつもクリアされる。どうも完全にリセットされてるっぽいですね。 (edited)@State
で覚えてるようなものがリセットされるからpopされるんだとは思いますが。FirstView
の List
のところを、 List { ForEach(env.items) { item in ItemView(id: item.id) } }
とすると、いったんpopされるけどすぐにpushされてtrue、falseの状態も保たれてます。 List
と ForEach
の違いはなに? List
を消すと(つまり ForEach
だけにすると)、TableView表示はされなくなるんですが、AとかBとかの部分を押すと画面遷移はします。ただ、そうすると強制popは起こらなくなります。List
と NavigationLink
の組み合わせでこの現象は起こってるぽいですね。List
は UITableView
と同じで lazily に動作して見えてない部分は作らない機構があって、見えてないとプログラムから遷移させられないという問題が書かれているんですが、今回のもそのlazilyなための仕組みが関係あるかもしれませんね。List
が表示されていない間は流れをせき止める ObservableObject
でラップすることで、遷移先からの更新の反映を遅らせることができました。 final class ObservableDam<WrappedValue: ObservableObject>: ObservableObject { typealias ObjectWillChangePublisher = AnyPublisher<WrappedValue.ObjectWillChangePublisher.Output, Never> let wrappedValue: WrappedValue private var cancellable: AnyCancellable? = nil var isActive: Bool = true { didSet { if isActive { let outputs = self.outputs self.outputs = [] for output in outputs { subject.send(output) } } } } private var outputs: [ObjectWillChangePublisher.Output] = [] private let subject: PassthroughSubject<ObjectWillChangePublisher.Output, Never> = .init() var objectWillChange: ObjectWillChangePublisher { subject.eraseToAnyPublisher() } init(_ wrappedValue: WrappedValue) { self.wrappedValue = wrappedValue cancellable = wrappedValue.objectWillChange.sink { [weak self] output in guard let self = self else { return } if self.isActive { self.subject.send(output) } else { self.outputs.append(output) } } } }
struct ContentView: View { @ObservedObject var history: ObservableDam<History> = .init(.shared)
↑こうして } .onAppear { self.history.isActive = true } .onDisappear { self.history.isActive = false }
↑こうする。List
は内部的には UITableView
を使って実現されていて、裏で、 UITableView
の reloadData()
なり beginUpdates()
〜 endUpdates()
をしてるんじゃないかと推測。NavigationLink
も存在しなくなって戻るのではないかと思いました。LazyVStack
ではどうなるのかがちょっと気にはなってます。前画面でスクロール範囲から見えなくなったらpopされたりすることがあるのだと嫌だなあと。気になってるだけで試してませんが。NavigationLink
使わずに navigationController
を取り出して push
したら回避はできるんですが、今度は List
のセルに disclosure indicator を出すのが NavigationLink
の有無と連動してたり、セル選択のときの UI の挙動(セルの背景がグレーになるとか)が異なってたりでちょっと微妙です・・・。ObservableObject
をかまそうかと思ってますが、 iOS 14 だと N 階層でも伝わるので、事故を回避するためには List
以下で変更される可能性のあるもの全部に付けないといけなくてかなり面倒です・・・。static var shared: History
だからトリッキーなことに見えますけど、普通に Core Data で @FetchRequest
使ってる場合とか同じじゃないかと思うんですよねぇ・・・。GeometryReader
を使う方が親のサイズに合わせることができていいのかなあと思いました。たぶん、そうしたからといって質問の件はぜんぜん解決しないとは思いますが NavigationLink
で再描画が波及するのが、 .sheet
だとしないかなと思って試してみましたが、 .sheet
でも波及しますね。( "Navigation" または "Sheet" ボタンを押してから "+" ボタンを押したときに結果がビューに反映される) struct ContentView: View { @ObservedObject var counter: Counter = .shared @State var isSheetPresented: Bool = false var body: some View { NavigationView { VStack { NavigationLink(destination: CounterView(count: counter.count)) { Text("Navigation") } Button("Sheet") { self.isSheetPresented = true } .sheet(isPresented: $isSheetPresented) { CounterView(count: self.counter.count) } } } } } struct CounterView: View { let count: Int var body: some View { VStack { Text("\(Counter.shared.count)") Button("+") { Counter.shared.count += 1 } } } } final class Counter: ObservableObject { @Published var count: Int = 0 static let shared: Counter = .init() }
(edited).sheet
では @EnvironmentObject
は引き継がれないけど再描画は影響する?無関係の二つかもしれないけど、一貫性がないような・・・。onChanged(_:)
とonEnded(_:)
がありますが、キャンセルされた場合入らないみたいです。updating(_:body:)
使っていればキャンセルされた時@GestureState
の初期値になります。クロージャ通らなくて直接に初期値になるので、使い方によって困る場合があるかもしれません。 (edited)updating(_:body:)
では一応できるところから見ると多分無理かもしれません。 ↑の方法はタップと一緒に使えませんし、タップではなくて0秒のLongPressGesture
で時に変になります。 現時点SwiftUIにおいてジェスチャーを避けた方が良さそうですかね。SecondView
を開く度に count
が 0
に戻るんですが、どういう挙動でしょう? import SwiftUI struct ContentView: View { var body: some View { NavigationView { NavigationLink(destination: SecondView()) { Text("Open") } } } } struct SecondView: View { @State var count: Int = Counter.shared.count var body: some View { VStack { Text("\(Counter.shared.count)") Text("\(count)") Stepper("", value: $count) .labelsHidden() } .onDisappear { Counter.shared.count = self.count } } } struct Counter { var count: Int = 0 static var shared = Counter() }
@State
が保持し続けられているにしても、 Counter.shared.count
で初期化されるにしても、 0
になるのは変だと思うんですが・・・。var count: Int = 0 { didSet { // ここでスタックトレース } }
したら何が起きてるかわかりそうonDisappear
のところで更新するだけなんですけど、 @State var count
の方に didSet
付けても何も出ないんですよね。 Property Wrapper だから?import SwiftUI struct ContentView: View { var body: some View { NavigationView { NavigationLink(destination: SecondView(Counter.shared.count)) { Text("Open") } } } } struct SecondView: View { @State var count: Int var body: some View { VStack { Text("\(Counter.shared.count)") Text("\(count)") Stepper("", value: $count) .labelsHidden() } .onDisappear { Counter.shared.count = self.count } } } struct Counter { var count: Int = 0 static var shared = Counter() }
こうしないといけないかな (edited)NavigationLink
を最初に作ったときに一度だけ SecondView.init
が走る?Counter
をクラスかつ ObservableObject
にして SecondView
では @ObservedObject
として持つとかだと簡単なんですけど、そのためだけに Value Semantics 捨ててクラスにするの嫌なんですよねぇ・・・。UINavigationController
掴ませて、それ経由で UIHostingController
に次の View
をラップして遷移させるとかはありますが)。destination
が @autoclosure @escaping
じゃないところがそもそも変? (edited)View
自体を lazy にすることで @autoclosure
相当にできるわけですね(本文読めませんでしたが)。onAppear
を一つにまとめたいところですが。 struct LazyView<Content: View>: View { private let content: () -> Content @State private var _body: Content? init(_ content: @escaping () -> Content) { self.content = content } @ViewBuilder var body: some View { if _body != nil { _body!.onAppear { self._body = self.content() } } else { EmptyView().onAppear { self._body = self.content() } } } }
LazyView
を挟めばちゃんと動きました。 struct ContentView: View { var body: some View { NavigationView { NavigationLink(destination: LazyView { SecondView() }) { Text("Open") } } } }
LazyView
の init
は @autoclosure
も付けた方がいいかな。struct LazyView<Content: View>: View { let build: () -> Content init(_ build: @autoclosure @escaping () -> Content) { self.build = build } var body: Content { build() } }
になってるけど、 onAppear
で更新されてないからこれだと初回しかうまくいかない?init
は NavigationLink
を作るときに呼ばれるけど、 body
は View が表示されたときにしか呼ばれず、しかも毎回呼ばれるからか。 (edited)View
の body
が無限に呼ばれて固まってしまうという現象が発生して、 @ObservedObject
が怪しいからコメントアウトしたら無限コールはなくなったんですけど、原因調査のためにその @ObservedObject
を購読してみても何も流れてこずに body
だけ呼ばれてるという現象が発生したんですが、何か知見のある方いませんか?最小再現構成はまだ作れてません。 (edited)import SwiftUI import Combine struct ContentView: View { @ObservedObject private var foo: Foo = .init() var body: some View { print("body") return Text("Hello, World!") } } final class Foo: ObservableObject { private let subject: CurrentValueSubject<Void, Never> = .init(()) var objectWillChange: AnyPublisher<Void, Never> { subject.eraseToAnyPublisher() } }
"body"
が無限に print
される。import SwiftUI import Combine struct ContentView: View { @ObservedObject private var foo: Foo = .init() var body: some View { print("body") return Text("Hello, World!") } } final class Foo: ObservableObject { private let subject: CurrentValueSubject<Void, Never> = .init(()) let objectWillChange: AnyPublisher<Void, Never> init() { objectWillChange = subject.eraseToAnyPublisher() } }
CurrentValueSubject
だからsubscribe毎に値が流れて無限につなげ直しちゃうのかな…?objectWillChange
が何度も呼ばれてますね。init
の際に subscribe されてるイメージだったから、 init
が呼ばれずに何度も subscribe されてるという発想がなかった・・・。 objectWillChange
を動的に生成するのは良くないのか。onAppear
で create して onDisappear
で release したいようなものってどうやって保持するのがいいですか?import SwiftUI struct ContentView: View { var body: some View { NavigationView { FirstView() } } } struct FirstView: View { var body: some View { return NavigationLink(destination: SecondView()) { Text("Go") } .onAppear { // ここで create } .onDisappear { // ここで release } } } struct SecondView: View { var body: some View { Text("Second") } }
FirstView
のプロパティに Box
的なクラスのインスタンスを保持しておいて、 onAppear
で create されたインスタンスを Box
に格納して、その Box
を介して onDisappear
で release ?FirstView
がコンポーネント的な View
で、親 View
の body
が頻繁に再実行されたときにうまく動くか心配。struct FirstView: View { var box = Box<Foo>() var body: some View { NavigationLink(destination: SecondView()) { Text("Go") } .onAppear { self.box.value = Foo() } .onDisappear { self.box.value?.release() self.box.value = nil } } }
みたいな。 (edited)struct ContentView: View { @State var count: Int = 0 var body: some View { NavigationView { VStack { Stepper(count.description, value: $count) .frame(maxWidth: 160) FirstView() } } } }
Stepper
の値を増減させる度に body
が再実行されて FirstView
の box
が再生成されるから。box
の代わりに struct FirstView: View { // var box = Box<Foo>() @State var foo: Foo?
として保持させる裏技もあるけど、 @State
のようなライフサイクルがほしいだけで、変更検知(して body
再実行)はいらない・・・。@State
みたいな Property Wrapper を作ればいいのかな・・・。@State
と @ObservedObject
のライフサイクルって違うんですね。 @State
は init
が呼ばれても初期化されないけど、 @ObservedObject
はされるみたい。struct ContentView: View { @State var count: Int = 0 var body: some View { VStack { Stepper("\(count)", value: $count) SView() CView() } .padding(30) } } struct S { var value: Int init(value: Int) { self.value = value } } struct SView: View { @State var s: S = .init(value: 42) var body: some View { Stepper("S: \(s.value)", value: $s.value) } } class C: ObservableObject { @Published var value: Int init(value: Int) { self.value = value } } struct CView: View { @ObservedObject var c: C = .init(value: 42) var body: some View { Stepper("C: \(c.value)", value: $c.value) } }
1つ目の Stepper
を増減したときに SView
はリセットされないけど CView
はリセットされる。SView
では S
のイニシャライザは呼ばれてるけど、 @State
への書き込みが行われない模様。@StateObject
使わないといけないのか…。class C: ObservableObject { @Published var value: Int init(value: Int) { self.value = value } } struct C1View: View { @StateObject var c: C = .init(value: 0) var body: some View { Stepper("C1: \(c.value)", value: $c.value) } } struct C2View: View { @ObservedObject var c: C = .init(value: 0) var body: some View { Stepper("C2: \(c.value)", value: $c.value) } } struct C3View: View { @State var c: C = .init(value: 0) var body: some View { InnerView(c: c) } private struct InnerView: View { @ObservedObject var c: C var body: some View { Stepper("C3: \(c.value)", value: $c.value) } } }
@State
と @ObservedObject
を組み合わせて @StateObject
相当の挙動を実現できました。↑の C1View
と C3View
が同じ挙動します。 (edited)@EnvironmentObject
が 1OS 14 & Xcode 12 だと自動的に渡されなくなってしまったんですが、似たような現象が発生した方いませんか?NavigationLink
でも自動で渡されて再現条件がわかってないんですが、何かわかりますか?実行時エラーとしてしか検出できないんで、アプリ全体のすべての遷移のパスを確認するには現実的でなく・・・。.environmentObject
付けまくればいいのか・・・。 (edited)Environment
でも起こっていますね。普通に NavigationLink
を書いているだけで起こっている箇所もあり、再現条件がよくわかっていないです。ただ、必ず同じ箇所で起こるので何か条件があるのだと思うのですが・・・。NavigationLink
で普通に引き継がれて再現できないんですよね。一方実アプリでは特定の NavigationLink
で引き継がれない現象が発生していて( iOS 13 では引き継がれた)、何が条件なのかわからず。 (edited).environment
, .environmentObject
付ければ解決するので、それで対応してしまいました。原因不明なのは気持ち悪いですが・・・。UIWindow
とか、カスタムの TabBarController
と NavigationController
が入っていて NavigationView
はないとか、色々怪しそうな箇所はあるんですが・・・。struct TextInputView: View { @State var inputText: String = "" var body: some View { HStack { TextField("Type here to input text", text: $inputText, //Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update. onCommit: { [self] in self.endEditing(from: self) }) } } }
TextField
の text
に $inputText
渡すと、実行時にコメントに書いてるとおりのIssueが報告されますけど、内容がよくわかりませんでした Stateの値をViewにインストールされる以外のところでアクセスしてるってどういうことだろう?FooViewController
の viewDidAppear
が呼ばれない現象が発生しているんですが、何かわかる方いませんか? 1. ContentView: View
2. UIHostingController<PageView>
3. PageView: UIViewControllerRepresentable
4. PageViewController: UIPageViewController
5. UIHostingController<FooView>
6. FooView: UIViewControllerRepresentable
7. FooViewController: UIViewController
5, 6 を抜くと viewDidAppear
が呼ばれます。 UIHostingController
が悪いのかと思ったんですが、 2, 3, 4 を抜いた状態でも viewDidAppear
が呼ばれます。再現コードは↓です。 https://github.com/koher/page-view-did-appear-experimentoverride var shouldAutomaticallyForwardAppearanceMethods: Bool { return true }
これをUIPageViewController(のサブクラス)に足してみてください。 (edited)viewDidAppear
が呼ばれるようになりました!!ありがとうございます。shouldAutomaticallyForwardAppearanceMethods
を override
したらうまくいった方がしっくりきますね・・・。shouldAutomaticallyForwardAppearanceMethods
が true
のようでした。 4 を true
にしても 5 を false
にすると表示されなくなりました。shouldAutomaticallyForwardAppearanceMethods
を false
にしても、 UIHostingController
を介さずに( 5, 6 を省略して)直接 7 を表示した場合は呼ばれました。UIPageViewController
あまりこれまで使ったことがなく-enable-private-imports
でビルドされてるんだっけなFoo
から $foo.bar
でプロパティごとに分解して Binding<Bar>
が得られるのの逆がやりたくなることありませんか?複数のバラバラの @State
などから結合して一つの Foo
として子 View
の @Binding
に渡したいなど。struct Foo
の bar
プロパティの実体だけ @State var bar: Bar
にするとかできないのでどうしたもんかなと。 @Binding var foo: Foo
が変更検知して @State var bar: Bar
に書き戻すというのもうまく書けず・・・。Binding
のイニシャライザの get
, set
で中継できないかと思ったんだけど、その @Binding
なプロパティを初期化するときに self
をキャプチャできないし、イニシャライザから同期的に get
が呼ばれるようで get
の中から呼び出す実体を初期化直後に IUO で差し替えようとしたらその前に nil
の Forced Unwrapping で死んだ・・・。List { ForEach(timeline.tweets) { tweet in TweetView(tweet) } }
TweetView
からいいねが押されたかを tweet.isLiked = true
みたいにして変更できるとすると tweet
を Binding
で渡さないといけませんが、 ForEach
を噛ましてるのでできなくてこういうパターン辛くないですか?Binding<Value: RandomAccessCollection>
を Element == Binding<Value.Element>
な RandomAccessCollection
にすることで、 ForEach
を介して要素を Binding
として取り出して子 View に渡せないかな?List { ForEach($timeline.tweets) { tweet in TweetView(tweet) } }
(edited)Binding<Value: RandomAccessCollection>
を Element == Binding<Value.Element>
な RandomAccessCollection
にすることで、 ForEach
を介して要素を Binding
として取り出して子 View に渡せないかな? List { ForEach(0..<timeline.tweets.count) { i in TweetView($timeline.tweets[i]) } }
や List { ForEach(0..<timeline.tweets.count) { i in TweetView( .init( get: { timeline.tweets[i] }, set: { timeline.tweets[i] = $0 } ) ) } }
みたいな形をよく使いますが、前者は onDelete
で timeline.tweets
の要素を削除したときなど条件によっては Index out of range を引き起こしたり、後者は NavigationLink
を使った画面遷移を壊したり (親ビュー側が再描画されることにより) するので微妙だったんですよね。koher さんのは良さそうな? (edited)struct ScrumsView: View { @Binding var scrums: [DailyScrum] var body: some View { List { ForEach(scrums) { scrum in NavigationLink(destination: DetailView(scrum: binding(for: scrum))) { CardView(scrum: scrum) } .listRowBackground(scrum.color) } } .navigationTitle("Daily Scrums") .navigationBarItems(trailing: Button(action: {}) { Image(systemName: "plus") }) } private func binding(for scrum: DailyScrum) -> Binding<DailyScrum> { guard let scrumIndex = scrums.firstIndex(where: { $0.id == scrum.id }) else { fatalError("Can't find scrum in array") } return $scrums[scrumIndex] } }
ForEach
の O(N) と binding(for:)
の O(N) の組み合わせで O(N^2) になるのが辛そうですね。 N が大きい場合。順序を保持して ID から O(1) で子を引けるコンテナがほしいですね。標準であればベストですが、 Array
と Dictionary
を組み合わせて作るのが良さそう。UIViewControllerRepresentable
に frame
を指定しなかったときのサイズがどうやって決まるか知ってる方いませんか? constraints
を見る限り、 SwiftUI によって View Controller の view
に constraint
が設定されているようなんですが、 View Controller 側に preferredContentSize
を指定しても負けてしまうことがあり、また、 AutoLayout の heightAnchor
等で設定しても SwiftUI が設定した constraint
と conflict してしまい・・・。Binding
で戻して frame
に食わせるという方法なんですが、もう少しいい方法がある気がしています(この例だと preferredContentSize
でうまくいくんですが、もう少し複雑な例ではうまくいきませんでした)。 https://gist.github.com/koher/13506ea9c45ebfd8b04249a07b23828fScrollView
関連のサイズ決定がバグってる気がします。Binding
で戻す方法はList
の中身をForEach
で回す時に厳しくて、いい方法があるなら知りたいです。(今のところpreferredContentSize
で動いてるので、うまくいかない例が知りたいです)onAppear
が呼ばれるバグ(?)が再現してるんですが(僕の場合は TabView
ではなく NavigationView
のルートの onAppear
が呼ばれるというパターンですが)、これ結構きつくないですか? onAppear
でページ表示されたことをトリガーとして何かやりたいパターンとか。 https://stackoverflow.com/questions/64027482/onappear-calls-unexpectedly-when-keyboard-appears-in-swiftuiimport SwiftUI extension View { func onAppear2(_ perform: @escaping () -> Void) -> some View { OnAppearView(content: self, perform: perform) } } private struct OnAppearView<Content>: UIViewControllerRepresentable where Content: View { let content: Content let perform: () -> Void func makeUIViewController(context: Context) -> OnAppearViewController<Content> { OnAppearViewController(content: content, perform: perform) } func updateUIViewController(_ viewController: OnAppearViewController<Content>, context: Context) { viewController.rootView = content } } private final class OnAppearViewController<Content>: UIHostingController<Content> where Content: View { let perform: () -> Void init(content: Content, perform: @escaping () -> Void) { self.perform = perform super.init(rootView: content) view.backgroundColor = .clear } @objc required dynamic init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) perform() } }
ワークアラウンド思い付きました。 onAppear
の代わりに↑の onAppear2
を使えば回避できました。PageTabViewStyle
を使ったときって縦スクロール強制なんでしょうか。それだと使えなさすぎるんですが・・・。ググっても↓みたいなむちゃくちゃな方法しか出てこず。 https://stackoverflow.com/questions/64091415/how-to-disable-vertical-scroll-in-tabview-with-swiftui
UIViewControllerRepresentable
と UIPageViewController
使えばいいんですが、面倒だなと・・・。struct ContentView: View { var body: some View { if Bool.random() { Text("abc") } else { Color.red } } }
(edited)if
は使えるけど、昔は中身同じ型じゃないとダメだったはず https://yanamura.hatenablog.com/entry/2019/09/05/150849body
が@ViewBuilder
になってますね。いつからでしょう。 (edited)UIInterfaceOrientation
を取得する方法ありますか?( UIDeviceOrientation
ではない方)AVCaptureVideoPreviewLayer
を使わないで純粋にPixelBufferをCGImageに直してImageを作る方法でカメラアプリを作ってますが、そのためにはデバイスの向きよりもUIの向きを取得しないといけないので UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
無理矢理感がありますが、これダメですか?CVImageBuffer
を直接使う系のアプリで UIDevice.current.orientation
から switch
で UIInterfaceOrientation
を生成する方法なら使ったことがありますね。 (edited)UIDevice
から取れますね。extension Hoge
から UIApplication
使えないってことです?extension Hoge
から UIApplication
使えないってことです? CGImage
使って Image
でプレビュー出すと遅延が結構出る(0.5秒くらい?)ので、諦めて素直に AVCaptureVideoPreviewLayer
使うことにしました
https://github.com/el-hoshino/MetadataScannerUIViewControllerRepresentable
のframe
問題、iOS14だとmakeUIViewController
の中でintrinsicContentSize
でいいサイズを計算してpreferredContentSize
に設定してやったVCをreturnするとうまくSwiftUI側に認識されそうですね。ScrollView
のスクロール方向のConstraintをsystemLayoutSizeFitting
で作ったらうまくいきました。iOS13はダメです。List
の中においてる Button
ですが、表示は正しくできてるのに、押した時の動作が違うボタンのものになるし、ハイライトもその間違ったボタンが反応しますけどなんで? .tag
は関係ないはずです、あっても外しても動作変わらなかったのと、あれそもそも ScrollViewReader
の scrollTo
メソッド用のものなのでwithAnimation
って使いづらくないですか?たとえば、 ObservableObject
に適合した ViewModel が非同期的に状態を変更するようなときに withAnimation
を適用しようとすると、 ViewModel 側に withAnimation
書かないといけなくないですか? ViewModel に SwiftUI を漏れ出させたくないですが・・・。// これは問題ない if let isFoo = viewModel.isFoo { Text("Hello") } Button("Toggle Foo") { withAnimation { viewModel.isFoo.toggle() } }
// これはどうする? if let imageData = viewModel.imageData { Image(UIImage(data: imageData)!) } Button("Load Image") { viewModel.loadImage() // 非同期なのでここでは withAnimation で囲めない }
(edited)// これは問題ない if let isFoo = viewModel.isFoo { Text("Hello") } Button("Toggle Foo") { withAnimation { viewModel.isFoo.toggle() } }
// これはどうする? if let imageData = viewModel.imageData { Image(UIImage(data: imageData)!) } Button("Load Image") { viewModel.loadImage() // 非同期なのでここでは withAnimation で囲めない }
(edited)withAnimation
の相性が悪いのは同意です。 ただ、その例の場合って if let imageData = viewModel.imageData { Image(UIImage(data: imageData)!) .animation(.easeInOut) }
とかでいけません? (あるいはカスタムの AnyTransition
を作っておくなど) (edited)withAnimation
の相性が悪いのは同意です。 ただ、その例の場合って if let imageData = viewModel.imageData { Image(UIImage(data: imageData)!) .animation(.easeInOut) }
とかでいけません? (あるいはカスタムの AnyTransition
を作っておくなど) (edited)if
で分岐して現れるのはアニメーション利かないんですよね。 .opacity(isFoo ? 1.0 : 0.0).animation(.easeInOut)
とかなら利くんですが。@ViewBuilder
内で直接使っているとかだと動かないんですが、例えば @ViewBuilder var mainBody: some View { if someCondition { ... // ここで .animation(.easeInOut) はアニメーション効かず (分岐先が変わると View そのものが置き換わるため) } else { ... // ここで .animation(.easeInOut) はアニメーション効かず (分岐先が変わると View そのものが置き換わるため) } } var body: some View { VStack { self.mainBody } .animation(.easeInOut) // これは効く }
とかだと効くんですよね。@ViewBuilder
をワンショットで使えるクロージャを作っておくと便利そう (edited)func make<V: View>(_ f: @ViewBuilder () -> V) -> V { f() } ... var body: some View { make { if { ... } else { ... } }.animation(.easeInOut) }
これで動くんじゃないかしら (edited)Group { if isFoo { Text("Foo") } } .animation(.easeInOut)
と違いはありますか?Group
でも利かないんですよね・・・。Group { if isFoo { Text("☔") .font(.system(.largeTitle)) } } .animation(.easeInOut(duration: 4.0))
View
でなんか思い通りに動かないの踏んだことありますね。.animation
付けたら実体化してほしい。@ViewBuilder var mainBody: some View { if someCondition { ... } else { ... } } var body: some View { self.mainBody .onAppear(...) .onDisappear(...) }
で someCondition
の状態が変わるたびに onDisappear
と onAppear
が呼ばれるところからだったんですよね。多分 Group
だとそれと同じことが起きてる気がします。.animation
が返す some View
が実体化されるものになるだけでもダメですか?@ViewBuilder var mainBody: some View { if someCondition { ... } else { ... } } var body: some View { self.mainBody .onAppear(...) .onDisappear(...) }
で someCondition
の状態が変わるたびに onDisappear
と onAppear
が呼ばれるところからだったんですよね。多分 Group
だとそれと同じことが起きてる気がします。 onAppear
, onDisappear
が分岐先の実体まで展開されちゃうのか・・・。CGSize
)のことですか?レイアウトへの影響の話?RealView
の標準提供ほしいですね。AnySequence
みたいな。@available(*, unavailable)
つけて封印とかってできましたっけ (edited)AnyView(Group { // ViewBuilder 利用するために Group をかます if isFoo { Text("☔") .font(.system(.largeTitle)) } }) .animation(.easeInOut(duration: 4.0))
(edited)case .foo(let bar)
か case let .foo(bar)
かの話をしてて、 https://discord.com/channels/291054398077927425/291054454793306112/339971613309141024 は引き出せたんですけどねー。 Discord 何年も前の会話が残ってるから、自分がいかに覚えてないか気付かされますねlet
に改宗してそれで覚えてました。struct ContentView: View { var body: some View { VirtualScreen(resolution: CGSize(width: 1280, height: 960)) { // AnyView(Color.white) Color.white } .background(Color.gray) } } struct VirtualScreen<Content: View>: View { var resolution: CGSize var content: Content init(resolution: CGSize, @ViewBuilder content: () -> Content) { self.resolution = resolution self.content = content() } var body: some View { GeometryReader { context in // GeometryReader { _ in self.content // } .frame(width: self.resolution.width, height: self.resolution.height) .scaleEffect(min(context.size.width / self.resolution.width, context.size.height / self.resolution.height)) } } }
Self._printChanges()
inside the body of a view to print out the changes that have triggered the view update.init<S: StringProtocol>(_ title: S, text: Binding<String>)
が優先されてinit(_ title: LocalizedStringKey, text: Binding<String>)
が優先されなくなった?import SwiftUI import SpriteKit struct ContentView: View { var scene: SKScene { let scene = GameTitleScene() scene.size = CGSize(width: UIScreen.main.bounds.size.width , height: UIScreen.main.bounds.size.height) return scene } var body: some View { SpriteView(scene: scene) .frame(width: UIScreen.main.bounds.size.width , height: UIScreen.main.bounds.size.height) .ignoresSafeArea() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
body
が走る度に scene
が再生成されてしまうので、 @StateObject
の中に scene
を持たせるなどした方が良いと思います。たとえば↓のように(これで問題が解決するかわかりませんが)。 struct ContentView: View { @StateObject private var state: ContentViewState = .init() var body: some View { SpriteView(scene: scene) .frame(width: UIScreen.main.bounds.size.width , height: UIScreen.main.bounds.size.height) .ignoresSafeArea() } }
import Combine final class ContentViewState: ObservableObject { let scene: SKScene init() { self.scene = GameTitleScene() scene.size = CGSize(width: UIScreen.main.bounds.size.width , height: UIScreen.main.bounds.size.height) } }
(edited)SpriteView
が UIViewControllerRepresentable
等であればそちらで持たせる方が良いかもしれません。StateObject
の objectWillChange
が活きてませんが、他の色々があると仮定して持たせるならそこかなと。SpriteView
は SwiftUI 標準ですね。koher さんの回答と似たような感じですが、単に元のコードから scene
を @State
な stored プロパティにするだけでも解決する気がします。SpriteView
は SwiftUI 標準ですね。koher さんの回答と似たような感じですが、単に元のコードから scene
を @State
な stored プロパティにするだけでも解決する気がします。 @State
でも良さそうですね。import SwiftUI import SpriteKit struct ContentView: View { @StateObject private var state: ContentViewState = .init() var body: some View { SpriteView(scene: state.scene) .frame(width: UIScreen.main.bounds.size.width , height: UIScreen.main.bounds.size.height) .ignoresSafeArea() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
import Combine import SwiftUI import SpriteKit final class ContentViewState: ObservableObject { @Published var scene: SKScene init() { self.scene = GameTitleScene() scene.size = CGSize(width: UIScreen.main.bounds.size.width , height: UIScreen.main.bounds.size.height) } }
scene
を @Published
で保持する必要ないですね。 SKScene
はクラスで、そのインスタンスを差し替えるような場合にしか @Published
は意味がないです。なので、普通に let
で保持すれば良いと思います。 @StateObject
にしたのはライフサイクルのためなので。scene
が初期化されてしまったためでしょうか?それであれば、 ContentView
の上位に原因がある可能性があるかと思います。
scene
を @Published
で保持する必要ないですね。 SKScene
はクラスで、そのインスタンスを差し替えるような場合にしか @Published
は意味がないです。なので、普通に let
で保持すれば良いと思います。 @StateObject
にしたのはライフサイクルのためなので。 scene
が初期化されてしまったためでしょうか?それであれば、 ContentView
の上位に原因がある可能性があるかと思います。
SKScene
があってインスタンスを切り替えているということですか?GameTitleScene
の中で override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { if self.atPoint(self.firstTouch).name == "start" { guard let t = touches.first else { return } self.lastTouch = t.location(in: self) let touchPoint = self.atPoint(self.lastTouch) if touchPoint.name == "start" { self.startButton.fillColor = UIColor.clear self.view?.presentScene(GameScene(size: self.size)) } } }
のように呼び出しています.environment(\.colorScheme, .dark)
を使っているんですが、同じ用にハイコントラストモードの見た目を確認したくて .environment(\.colorSchemeContrast, .increased)
のようにすると Key path value type 'WritableKeyPath<EnvironmentValues, ColorSchemeContrast>' cannot be converted to contextual type 'KeyPath<EnvironmentValues, ColorSchemeContrast>'
となって、 API リファレンスを確認すると UIAccessibilityContrast.high
に相当するのがあるかと思ったけどなさそうだった。ないのかな。ColorSchemeContrast.increased
があるんですが、 "Your app cannot override the user’s choice." で set できないようです。ColorSchemeContrast.init?(UIAccessibilityContrast)
のイニシャライザがあるみたいなのでそれに UIAccessibilityContrast.high
渡したら動いたりしないかな。ColorSchemeContrast.increased
自体は取得できるんですが、 .environment(\.colorSchemeContrast, .increased)
しようとすると https://developer.apple.com/documentation/swiftui/environmentvalues/colorschemecontrast が read-only なので上書きできない状況ですね。 (edited)ContentView() .environment(\.colorScheme, .dark) .environment(\._colorSchemeContrast, .increased)
Underscore付けたらセットできました。。。struct ContentView: View { var body: some View { VStack { Text("Hello, world!") .padding() .background(Color("sampleColor")) .environment(\.colorScheme, .light) Text("Hello, world!") .padding() .background(Color("sampleColor")) .environment(\.colorScheme, .dark) Text("Hello, world!") .padding() .background(Color("sampleColor")) .environment(\.colorScheme, .light) .environment(\._colorSchemeContrast, .increased) Text("Hello, world!") .padding() .background(Color("sampleColor")) .environment(\.colorScheme, .dark) .environment(\._colorSchemeContrast, .increased) } } }
colorSchemeContrast
が混ざるのはさすがにおかしい気がして、僕がやっているのは previews
の中でそれぞれ指定した場合(プレビュー上で別の iPhone に表示される場合)ですね。init<S: StringProtocol>(_ title: S, text: Binding<String>)
が優先されてinit(_ title: LocalizedStringKey, text: Binding<String>)
が優先されなくなった? struct ContentView: View { @State private var text = "" var body: some View { if #available(iOS 15, *) { TextField("foo", text: $text) // "foo"はLocalizedStringKey扱い } else { TextField("foo", text: $text) // "foo"はString扱い } } }
(edited)public init(_ titleKey: LocalizedStringKey, text:, onEditingChanged:) @_disflavoredOverload public init<S: StringProtocol>(_ title: S, text:, onEditingChanged:)
iOS15から // 1 @_disflavoredOverload public init(_ titleKey: LocalizedStringKey, text:, onEditingChanged:) // 2 @_disflavoredOverload public init<S: StringProtocol>(_ title: S, text:, onEditingChanged:) // 3 @available(iOS 15.0) public init(_ titleKey: LocalizedStringKey, text:, prompt:) // 4 @_disflavoredOverload @available(iOS 15.0) public init<S: StringProtocol>(_ title: S, text:, prompt:)
こうなった。 (edited)@available(iOS 15.0)
を満たさない場合、 _disflavoredOverload
同士の戦いになって、スコアが同じなので_disflavoredOverload
で勝敗を捻じ曲げる前の本来のオーバーロード解決で評価されて、 2が勝ってるんですね。 (edited)extension View { func task( priority: TaskPriority = .userInitiated, _ action: @Sendable @escaping () async -> Void ) -> some View { var task: Task<Void, Never>? return self .onAppear { task = Task(priority: priority) { await action() } } .onDisappear { task?.cancel() } } }
body
が走る度に走ってうまくいかなさそうな気がしてきた・・・。ObservableObject
の普通のプロパティが$ob.property
でアクセスすると、Bind<PropertyType>
として扱えるのは、ObservableObject.Wrapper
の仕組みと認識しているのですけれど、この辺りについてのドキュメントなどご存知の方いたら教えてほしいです。少し探したのですけれど、見当たらず… (もしくはこの認識が間違っていたら教えてほしいです) https://developer.apple.com/documentation/swiftui/observedobject/wrapper$ob
で ObservableObject.Wrapper
が返ってくるところまでは理解されていると思います。これはpropertyWrapperのprojectedValue
の仕組みでObservableObject<T>.Wrapper
が返ってきているためです。 このWrapper
にはさらにKeyPath Dynamic Member Lookupが実装されており、Tに生えてる任意のプロパティを https://developer.apple.com/documentation/swiftui/observedobject/wrapper/subscript(dynamicmember:) を経由して取り出しています。見え方が普通のプロパティアクセスになっているところが特殊です。projectedValue
→ https://github.com/apple/swift-evolution/blob/96a70cd71258d24ad3972c6433a2a45ee255ccf6/proposals/0258-property-wrappers.md#projections KeyPathMemberLookup → https://github.com/apple/swift-evolution/blob/0c2f85b3ae42539a7cd47fca2473a0bf6f345566/proposals/0252-keypath-dynamic-member-lookup.mdObservableObject.Wrapper
(StateObject<Type>
と認識してますObservableObject<Type>.Wrapper
が正ですね)にプロパティが存在するわけではなく、動的にプロパティを生成して存在しているように見せているという感じなのですね…! (edited)$ob.property
を省略を減らして書くと _ob.projectedValue[dynamicMember: \.property]
という感じになりますObservedObject
と ObservableObject
が混ざってしまっている気がしますので(両者は別物なので)ご注意下さい。ObservedObject
, StateObject
, Binding
, ObservableObject
, Published
)も多いのでこの辺りややこしいです。URL
を返す async 関数を Task
のクロージャで await
するようなボタンを Xcode Previews 上でクリックしたら必ずプレビューのプロセスがクラッシュする (実機だと問題なし) という謎の問題を発見して,特に困っているわけではないのですが気になるので情報をお持ちの方を探しています. 以下ミニマル再現コードです.(macOS 12.3 & Xcode 13.3 や iPadOS 15.4 & Swift Playgrounds 4.0.2 で再現することを確認) import SwiftUI func foo() async -> URL { URL(fileURLWithPath: "/") // 戻り値が String 等では問題なし } struct ContentView: View { var body: some View { Button("Press Me") { Task { _ = await foo() } } } }
スタックトレース: Thread 4 Crashed:: Dispatch queue: com.apple.root.user-initiated-qos.cooperative 0 libsystem_kernel.dylib 0x1cba39e60 __pthread_kill + 8 1 libsystem_pthread.dylib 0x1cba8d3c0 pthread_kill + 256 2 libsystem_c.dylib 0x1801023f4 abort + 124 3 libswift_Concurrency.dylib 0x1cb9db604 swift::swift_Concurrency_fatalError(unsigned int, char const*, ...) + 12 4 libswift_Concurrency.dylib 0x1cb9dd350 swift_task_dealloc + 124 5 ContentView.1.preview-thunk.dylib 0x104a2b06c (1) await resume partial function for closure #1 in closure #1 in ContentView.__preview__body.getter + 80 (ContentView.swift:17) 6 ContentView.1.preview-thunk.dylib 0x104a2b5d9 (1) await resume partial function for thunk for @escaping @callee_guaranteed @Sendable @async () -> (@out A) + 1 7 ContentView.1.preview-thunk.dylib 0x104a2b73d (1) await resume partial function for partial apply for thunk for @escaping @callee_guaranteed @Sendable @async () -> (@out A) + 1 8 libswift_Concurrency.dylib 0x1cb9dced1 completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*) + 1
(edited)@preconcurrency import Foundation
書いたら変化しますか?Foundation.URL
が使われているのか SwiftUI.URL
が使われているのかわからないので念のために @preconcurrency import struct Foundation.URL import struct SwiftUI.Button import protocol SwiftUI.PreviewProvider import protocol SwiftUI.View
みたいなのも試してみましたが,こうするとファイルを開き直した時にプレビューが表示されなくなっちゃうのでプレビューが残っている間に試しました. ただ,-warn-concurrency
つけていても警告は出ていなかったのであんまり関係ないかなという気はしてます. ちなみに foo()
の定義だけ別のファイルに持っていくとクラッシュしなくなるんですよね. (edited)Sendable
かどうかでなにか変わってるのかと思いましたが、関係ないようですね。不思議。.isDetailLink(false)
をつけなければ最初のView戻ることはできないと言及されています。 しかし、.isDetailLink(false)
をつけない場合や、.isDetailLink(true)
にした場合でも同様に最初のViewに戻ることができました。これは、SwiftUIの仕様変更等が原因なのでしょうか??何かご存知の方がいられましたらご教授いただければ幸いです。 https://www.yururiwork.net/archives/148
https://stackoverflow.com/questions/57334455/how-can-i-pop-to-the-root-view-using-swiftui/59662275#59662275 以下ソースコードです。 struct FirstView: View { @State private var isActive = false var body: some View { NavigationView { NavigationLink(destination: SecondView(isFirstViewActive: $isActive), isActive: $isActive) { Button(action: { self.isActive = true }, label: { Text("Forward to Second View.") }) } .navigationBarTitle("First View") } } } struct SecondView: View { @State private var isActive = false @Binding var isFirstViewActive: Bool var body: some View { NavigationLink(destination: ThirdView(isFirstViewActive: $isFirstViewActive), isActive: $isActive) { Button(action: { self.isActive = true }, label: { Text("Forward to Third View.") }) } .isDetailLink(false) //ここをコメントアウトしてもFirstViewに戻れる。 .navigationBarTitle("Second View") } } struct ThirdView: View { @Binding var isFirstViewActive: Bool var body: some View { Button(action: { self.isFirstViewActive = false }, label: { Text("Back to First View.") }) .navigationBarTitle("Third View") } }
(edited)EXCLUDED_ARCHITECTURES
にiOS Simulatorターゲットを指定してしまうと基本的に動かないのではないかと思います。(なぜアプリターゲットなら動くのかはわかりませんが…) 最小再現プロジェクトを提供していただけるとスムーズにトラブルシュートできると思います EXCLUDED_ARCHITECTURES
にiOS Simulatorターゲットを指定してしまうと基本的に動かないのではないかと思います。(なぜアプリターゲットなら動くのかはわかりませんが…) 最小再現プロジェクトを提供していただけるとスムーズにトラブルシュートできると思います .blendMode(.colorBurn) .blendMode(.colorDodge)
って感じで重ねがけできて、簡易的なシェーダービルダーDSLとしての側面もあるなという感じURL
を返す async 関数を Task
のクロージャで await
するようなボタンを Xcode Previews 上でクリックしたら必ずプレビューのプロセスがクラッシュする (実機だと問題なし) という謎の問題を発見して,特に困っているわけではないのですが気になるので情報をお持ちの方を探しています. 以下ミニマル再現コードです.(macOS 12.3 & Xcode 13.3 や iPadOS 15.4 & Swift Playgrounds 4.0.2 で再現することを確認) import SwiftUI func foo() async -> URL { URL(fileURLWithPath: "/") // 戻り値が String 等では問題なし } struct ContentView: View { var body: some View { Button("Press Me") { Task { _ = await foo() } } } }
スタックトレース: Thread 4 Crashed:: Dispatch queue: com.apple.root.user-initiated-qos.cooperative 0 libsystem_kernel.dylib 0x1cba39e60 __pthread_kill + 8 1 libsystem_pthread.dylib 0x1cba8d3c0 pthread_kill + 256 2 libsystem_c.dylib 0x1801023f4 abort + 124 3 libswift_Concurrency.dylib 0x1cb9db604 swift::swift_Concurrency_fatalError(unsigned int, char const*, ...) + 12 4 libswift_Concurrency.dylib 0x1cb9dd350 swift_task_dealloc + 124 5 ContentView.1.preview-thunk.dylib 0x104a2b06c (1) await resume partial function for closure #1 in closure #1 in ContentView.__preview__body.getter + 80 (ContentView.swift:17) 6 ContentView.1.preview-thunk.dylib 0x104a2b5d9 (1) await resume partial function for thunk for @escaping @callee_guaranteed @Sendable @async () -> (@out A) + 1 7 ContentView.1.preview-thunk.dylib 0x104a2b73d (1) await resume partial function for partial apply for thunk for @escaping @callee_guaranteed @Sendable @async () -> (@out A) + 1 8 libswift_Concurrency.dylib 0x1cb9dced1 completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*) + 1
(edited)ProgressView
を使うのが良くないのかもしれませんが。Gauge
の方が良いかもしれないですね.あるいは ProgressViewStyle
を自作するなど.Gauge
の方が良いかもしれないですね.あるいは ProgressViewStyle
を自作するなど. Gauge
の方がかなり太いので .gaugeStyle
を試していたら、 .accessoryLinearCapacity
で ProgressView
と同じ細さになったんですが、 ProgressView
同様に 0 付近でリニアじゃなくなってしまいました
GaugeStyle
の名前から考えると、 ProgressView
で 0 付近で見えているのは accessory なのかもしれないですね。 0 で消えるのは謎ですが。UIProgressView
altogether and write a fake one leveraging UISlider
instead. I removed the knob with [setThumbImage:[UIImage new] forState:UIControlStateNormal]
and disabled user interaction.// 素早くトグルすれば値が生きている struct ContentViewE: View { @State var toggle = false var body: some View { if toggle { VStack { TextView() Button { toggle.toggle() } label: { Text("toggle if") } } } else { VStack { TextView() Button { toggle.toggle() } label: { Text("toggle if") } } } } } final class ViewModel: ObservableObject { @Published var count = 0 } struct TextView: View { @StateObject var viewModel: ViewModel init() { _viewModel = .init(wrappedValue: ViewModel()) } var body: some View { Button { viewModel.count += 1 } label: { Text("count: \(viewModel.count)") } } }
// 必ずリセットされる struct ContentViewD: View { @State var toggle = false var body: some View { VStack { if toggle { TextView() } else { TextView() } Button { toggle.toggle() } label: { Text("toggle if") } } } }
class DataModel: ObservableObject { @Published var name = "Some Name" @Published var isEnabled = false } struct MyView: View { @StateObject private var model = DataModel() // Create the state object. var body: some View { Text(model.name) // Updates when the data model changes. MySubView() .environmentObject(model) } }
struct TextView: View { @StateObject var viewModel = ViewModel() var body: some View { Button { viewModel.count += 1 } label: { Text("count: \(viewModel.count)") } } }
↑こう書き換えてみましたが、現象には変化無かったです。body
の中で if
を使うと条件式の値が変わった時に画面のライフタイム的にも一度画面が消えて再表示された扱いになる (onDisappear
が呼ばれて onAppear
が呼ばれ直す) ので,画面のライフタイムと一致するはずの @StateObject
は初期化される方が正しい挙動な気がします.これを避けるために私はある程度複雑な View
では body
の中で if
/switch
をトップレベルで使わないように VStack
で囲んだりしてますね.body
の中で if
を使うと条件式の値が変わった時に画面のライフタイム的にも一度画面が消えて再表示された扱いになる (onDisappear
が呼ばれて onAppear
が呼ばれ直す) ので,画面のライフタイムと一致するはずの @StateObject
は初期化される方が正しい挙動な気がします.これを避けるために私はある程度複雑な View
では body
の中で if
/switch
をトップレベルで使わないように VStack
で囲んだりしてますね. // 素早くトグルすれば値が生きている struct ContentViewE2: View { @State var toggle = false var body: some View { VStack { if toggle { VStack { TextView() Button { toggle.toggle() } label: { Text("toggle if") } } } else { VStack { TextView() Button { toggle.toggle() } label: { Text("toggle if") } } } } } }
if
があるのは 「トップレベル」でこそないですが、同じです。 (edited)condition
が変わるたびに ContentViewA
に対応づく画面は一度非表示になって再度表示され直した扱いになりますが,後者の場合はそうはならないという意味です.if
や else
のブロックの中に書いたものは条件が変わるたびに一度死にますよ. struct ContentViewA: View { var body: some View { if condition { Text("A") } else { Text("B") } } } struct ContentViewB: View { var body: some View { VStack { if condition { Text("A") } else { Text("B") } } } }
condition
が変わるたびに ContentViewA
に対応づく画面は一度非表示になって再度表示され直した扱いになりますが,後者の場合はそうはならないという意味です.if
や else
のブロックの中に書いたものは条件が変わるたびに一度死にますよ. struct ContentViewA: View { var body: some View { if condition { Text("A") } else { Text("B") } } } struct ContentViewB: View { var body: some View { VStack { if condition { Text("A") } else { Text("B") } } } }
condition
が変わったら Text("A")
と Text("B")
の表示/非表示が切り替わるのではないのでしょうか (edited)ContentViewE2
は生き続けますが TextView
は死にます.body
直下の VStack
がなかった場合は ContentViewE2
も死にます. (edited)body
を提供してる 外側の View が変わるんですかstruct MidViewA: View { let condition: Bool @State var count = 0 var body: some View { if condition { Counter(name: "T", count: $count) } else { Counter(name: "F", count: $count) } } } struct Counter: View { var name: String @Binding var count: Int var body: some View { Button { count += 1 } label: { Text("\(name)=\(count)") } } } struct ContentViewF: View { @State var condition = false var body: some View { VStack { MidViewA(condition: condition) Button { condition.toggle() } label: { Text("toggle") } } } }
これでおっしゃっている状況を構成できてると思ったんですがどうでしょうか? 「MidViewA
に関して、 condition
が変化した場合、 MidViewA.count
がリセットする」 (MidViewA
が、 ContentViewA
を再現しようとしたものです) という話だと思います。 しかし、動かしてみるとtoggleボタンを押してもカウントがリセットしませんでした。@ViewBuilder
の扱いが変わった可能性もあるので,ちょっとシミュレータを落として古い OS で試してみます.ContentViewF
側で MidViewA
に onDisappear
を生やした場合,iOS 16.5, iOS 15.0 時点では condition
が変わっても呼ばれることはありませんが,iOS 14.0.1 では呼ばれてました.なので MidViewA
が死ぬというのは iOS 14.x 時代の記憶だったようです. struct ContentViewF: View { ... var body: some View { VStack { MidViewA(condition: condition).onDisappear { print("onDisappear") } Button { condition.toggle() } label: { Text("toggle") } } } }
ただ,MidViewA
の @State
がリセットされることはなかったので,onDisappear
が呼ばれる条件と @State
がリセットされる条件はそもそも違っていたみたいですね.struct Counter: View { let name: String @State var count = 0 var body: some View { Button("\(name)=\(count)") { count += 1 } .onAppear { print("onAppear \(name)") } .onDisappear { print("onDisappear \(name)") } } } struct ContentView: View { @State var condition = false var body: some View { VStack { if condition { Counter(name: "T") } else { Counter(name: "F") } Button("toggle") { condition.toggle() } } } }
(edited)// 素早くトグルすれば値が生きている struct ContentViewE2: View { @State var toggle = false var body: some View { VStack { if toggle { VStack { TextView() Button { toggle.toggle() } label: { Text("toggle if") } } } else { VStack { TextView() Button { toggle.toggle() } label: { Text("toggle if") } } } } } }
onAppear
と onDisappear
は TextView
に生やした前提ContentViewF
側で MidViewA
に onDisappear
を生やした場合,iOS 16.5, iOS 15.0 時点では condition
が変わっても呼ばれることはありませんが,iOS 14.0.1 では呼ばれてました.なので MidViewA
が死ぬというのは iOS 14.x 時代の記憶だったようです. struct ContentViewF: View { ... var body: some View { VStack { MidViewA(condition: condition).onDisappear { print("onDisappear") } Button { condition.toggle() } label: { Text("toggle") } } } }
ただ,MidViewA
の @State
がリセットされることはなかったので,onDisappear
が呼ばれる条件と @State
がリセットされる条件はそもそも違っていたみたいですね. deinit 0
が 1 回,それ以降の連打時には deinit 0
が 2 回ずつ出力されるので,@State
の挙動としては condition
が変わって Counter
が変わると @State
のための変数が新しく作られるけれど,前に使っていた変数がまだ破棄されず生きていたらそれを使い回すが新しく作った方も画面表示に使わないだけで裏では保持し続ける,みたいな挙動になっていそうですね. @State
に参照型を持たせるか値型を持たせるかで挙動が変わりそうな気もするので Swift 5.9 ツールチェーンで @_moveOnly
struct を作って使おうとしてみたんですが,State<T>
みたいなジェネリクスでは noncopyable struct は使えないみたいで弾かれてしまいました... final class Count { let count: Int init(_ count: Int) { self.count = count } deinit { print("deinit \(count)") } } struct Counter: View { let name: String @State var count = Count(0) var body: some View { Button("\(name)=\(count.count)") { count = Count(count.count + 1) } } } struct ContentView: View { @State var condition = false var body: some View { if condition { VStack { Counter(name: "T") Button("toggle") { condition.toggle() } } } else { VStack { Counter(name: "F") Button("toggle") { condition.toggle() } } } } }
(edited)@StateObject
と @State
だと挙動が違うので,上の例ではあくまでも @State
の挙動を知るために @State
に参照型を持たせてます.struct Count
にして確認しました ) (edited)Int
で起こっているのでそれはそうだと思います.(Int
の生存期間が知りたかったので class でラップした感じです) (edited)deinit 0
が 1 回,それ以降の連打時には deinit 0
が 2 回ずつ出力されるので,@State
の挙動としては condition
が変わって Counter
が変わると @State
のための変数が新しく作られるけれど,前に使っていた変数がまだ破棄されず生きていたらそれを使い回すが新しく作った方も画面表示に使わないだけで裏では保持し続ける,みたいな挙動になっていそうですね. @State
に参照型を持たせるか値型を持たせるかで挙動が変わりそうな気もするので Swift 5.9 ツールチェーンで @_moveOnly
struct を作って使おうとしてみたんですが,State<T>
みたいなジェネリクスでは noncopyable struct は使えないみたいで弾かれてしまいました... final class Count { let count: Int init(_ count: Int) { self.count = count } deinit { print("deinit \(count)") } } struct Counter: View { let name: String @State var count = Count(0) var body: some View { Button("\(name)=\(count.count)") { count = Count(count.count + 1) } } } struct ContentView: View { @State var condition = false var body: some View { if condition { VStack { Counter(name: "T") Button("toggle") { condition.toggle() } } } else { VStack { Counter(name: "F") Button("toggle") { condition.toggle() } } } } }
(edited)T
の事 )は、Copyable が暗黙に含まれているからです。 func foo<T>(a: T) { let b = a // ←これが常に許されている print(b) }
Int
で起こっているのでそれはそうだと思います.(Int
の生存期間が知りたかったので class でラップした感じです) (edited)T
の事 )は、Copyable が暗黙に含まれているからです。 func foo<T>(a: T) { let b = a // ←これが常に許されている print(b) }
Copyable
を除去する文法 <T: ~Copyable>
が議論されてます。実装状況は追ってないです。// 素早くトグルすれば値が生きている struct ContentViewE: View { @State var toggle = false var body: some View { if toggle { VStack { TextView() Button { toggle.toggle() } label: { Text("toggle if") } } } else { VStack { TextView() Button { toggle.toggle() } label: { Text("toggle if") } } } } } final class ViewModel: ObservableObject { @Published var count = 0 } struct TextView: View { @StateObject var viewModel: ViewModel init() { _viewModel = .init(wrappedValue: ViewModel()) } var body: some View { Button { viewModel.count += 1 } label: { Text("count: \(viewModel.count)") } } }
Text("toggle if") .onTapGesture { toggle.toggle() }
Button { withAnimation(nil) { toggle.toggle() } } label: { Text("toggle if") }
Button { withAnimation(nil) { toggle.toggle() } } label: { Text("toggle if") }
#Preview
怒られるので available つけてみたらプロジェクトのビルドは問題ないけど Xcode Preview は Syntax error になるんですね
@available(iOS 17, *) #Preview { ContentView() }
#Preview
が生成するコードが17+なんですよね。自分でマクロ作って中で分岐するコードを生成するとよさそう。@Observable struct Foo { var foo: ... }
の展開処理の結果として、 @ObservableTracked var foo: ...
がプロパティに付与される部分。ERROR(macro_recursive,none, "recursive expansion of macro %0", (DeclName))
https://github.com/apple/swift/blob/c30f7dcdd8287376732edcba2c8307d00552ec31/include/swift/AST/DiagnosticsSema.def#L7105-L7106#Fib(2)
→ #Fib(1) + #Fib(0)
→ 1 + 1
みたいな パターンは作れそうだけど作れないってことかなあMacros.swift:9:14: error: macros are not supported in this compiler public macro assert(
同じエラーだった。@ViewBuilder
をつけるとResult BuilderになるからSwitch式じゃなくてResult BuilderのSwitchの一部になるんで行けるようになる、のかな。twitter.com
のプレビューが表示されない対策として、ドメイン部分をfxtwitter.com
へ書き換えるというのがあるそうです。 https://fxtwitter.com/ojun_9/status/1715208317994483880_ViewModifier_Content
というやつっぽいのですが、どうやってこの型を隠匿しているのかが謎ですprotocol P { associatedtype Foo typealias Bar=Text }
public protocol ViewModifier { static func _makeView(modifier: SwiftUI._GraphValue<Self>, inputs: SwiftUI._ViewInputs, body: @escaping (SwiftUI._Graph, SwiftUI._ViewInputs) -> SwiftUI._ViewOutputs) -> SwiftUI._ViewOutputs static func _makeViewList(modifier: SwiftUI._GraphValue<Self>, inputs: SwiftUI._ViewListInputs, body: @escaping (SwiftUI._Graph, SwiftUI._ViewListInputs) -> SwiftUI._ViewListOutputs) -> SwiftUI._ViewListOutputs @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) static func _viewListCount(inputs: SwiftUI._ViewListCountInputs, body: (SwiftUI._ViewListCountInputs) -> Swift.Int?) -> Swift.Int? associatedtype Body : SwiftUI.View @SwiftUI.ViewBuilder @_Concurrency.MainActor(unsafe) func body(content: Self.Content) -> Self.Body typealias Content = SwiftUI._ViewModifier_Content<Self> } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public struct _ViewModifier_Content<Modifier> : SwiftUI.View where Modifier : SwiftUI.ViewModifier { public static func _makeView(view: SwiftUI._GraphValue<SwiftUI._ViewModifier_Content<Modifier>>, inputs: SwiftUI._ViewInputs) -> SwiftUI._ViewOutputs public static func _makeViewList(view: SwiftUI._GraphValue<SwiftUI._ViewModifier_Content<Modifier>>, inputs: SwiftUI._ViewListInputs) -> SwiftUI._ViewListOutputs @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) public static func _viewListCount(inputs: SwiftUI._ViewListCountInputs, body: (SwiftUI._ViewListCountInputs) -> Swift.Int?) -> Swift.Int? @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) @_alwaysEmitIntoClient public static func _viewListCount(inputs: SwiftUI._ViewListCountInputs) -> Swift.Int? { _viewListCount(inputs: inputs) { _ in nil } } public typealias Body = Swift.Never }
_ViewModifier_Content
はSwiftUI.Viewにconformしてるのに、body
の定義がないですね (edited)body
自体がないと怒られるんですよね struct MyModifier: ViewModifier { func body(content: Content) -> some View { // Value of type 'MyModifier.Content' (aka '_ViewModifier_Content<MyModifier>') has no member 'body' let s = content.body return EmptyView() } }
private struct MyView: View { typealias Body = Never }
struct TwerkModifier: ViewModifier { func body(content: Content) -> some View { Wrapper(view: content) } struct Wrapper<T: View>: View { var view: T var body: some View { return view.body } } } struct UsageView: View { var body: some View { // SwiftUI/DynamicProperty.swift:338: Fatal error: ViewWrapper<_ViewModifier_Content<TwerkModifier>> may not have Body == Never Text("Foo") .modifier(TwerkModifier()) } }
(edited)@_disfavorerdOverload
とかも見えてないような@_inheritActorContext
とかも_ViewModifier_Content
はSwiftUI.Viewにconformしてるのに、body
の定義がないですね (edited)_tree
を持って中身を管理してそうです @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) @frozen public struct VStack<Content> : SwiftUI.View where Content : SwiftUI.View { @usableFromInline internal var _tree: SwiftUI._VariadicView.Tree<SwiftUI._VStackLayout, Content> @inlinable public init(alignment: SwiftUI.HorizontalAlignment = .center, spacing: CoreFoundation.CGFloat? = nil, @SwiftUI.ViewBuilder content: () -> Content) { _tree = .init( root: _VStackLayout(alignment: alignment, spacing: spacing), content: content()) } public static func _makeView(view: SwiftUI._GraphValue<SwiftUI.VStack<Content>>, inputs: SwiftUI._ViewInputs) -> SwiftUI._ViewOutputs public typealias Body = Swift.Never }
こういう純粋にvar body: some View
で表現できない事情がある時にBody = Neverになってそうです Icemanさんがいう通りSwift.NeverはViewでbodyではなんかを返してそう @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension Swift.Never { public typealias Body = Swift.Never public var body: Swift.Never { get } }
(edited)var body
の実装がない型は他にもあるけどなんでだろう (edited)body: some View
だけでは実現不可能でゴチャゴチャやってるのかなと思ったりしています (edited)var body: Never { ... }
は書かないとダメですよね?@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension Swift.Never { public typealias Body = Swift.Never public var body: Swift.Never { get } }
って定義がswiftinterfaceにあるのに private struct MyView: View { typealias Body = Never }
がエラーになるのは言語的にどうなってるんでしょう...? 特定のモジュール内でのみ有効とかできるんですか? (edited)@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension Swift.Never { public typealias Body = Swift.Never public var body: Swift.Never { get } }
って定義がswiftinterfaceにあるのに private struct MyView: View { typealias Body = Never }
がエラーになるのは言語的にどうなってるんでしょう...? 特定のモジュール内でのみ有効とかできるんですか? (edited)tree
みたいなのがあったりしてinternal var _tree: SwiftUI._VariadicView.Tree<SwiftUI._VStackLayout, Content>
(edited)@usableFromInline
だから internal だけどswiftinterfaceに表示されているけどbody
が直接呼び出せない理由は未解決ですか……?@Bindable
は @Binding
の置き換えではありません。 @Bindable
はProperty WrapperのProjected Value( $
)を使えるようにするためのものです。@Observable class Book: Identifiable { var title = "Sample Book Title" var isAvailable = true } struct BookEditView: View { @Bindable var book: Book @Environment(\.dismiss) private var dismiss var body: some View { Form { TextField("Title", text: $book.title) Toggle("Book is available", isOn: $book.isAvailable) Button("Close") { dismiss() } } } }
BookEditView
自体においては Book
が @Bindable
である必要はありません。もし book
のプロパティが変更されれば BookEditView
は再レンダリング( body
が再実行)されますし、 BookEditView
の中で book
のプロパティを変更すれば、 BookEditView
に book
を渡した元の View
等にも反映されます。TextField("Title", text: $book.title)
のように、 book
のプロパティをさらに先の View
に渡そうとするときに困ります。もし let book: Book
だと( Book
はクラスなので、Property Wrapper( @Bindable
等)を使わないのであれば var
である必要がありません。単に let book: Book
としても、 @observable
なクラスのプロパティの更新は追跡され、再レンダリングされます)、 $
を使うことができません。 (edited)TextField("Title", text: Binding( get: { book.ttile }, set: { book.title = $0 } ))
と書くことになってしまいます。$
が使えるように @Bindable
を利用します。$
が使えるように @Bindable
を利用します。 @Binding
は渡した先と元で、値の変更を伝え合うためのものです。たとえば、 Book
が @ObservableObject
の場合、単純に book.title
を渡したのでは、変更を伝え合うことはできませんよね? book.title
がその後変更されても渡した先には伝わりませんし、渡した先で title
を変更しても book.title
が変更されるわけではありません。@Binding
は渡した先と元で、値の変更を伝え合うためのものです。たとえば、 Book
が @ObservableObject
の場合、単純に book.title
を渡したのでは、変更を伝え合うことはできませんよね? book.title
がその後変更されても渡した先には伝わりませんし、渡した先で title
を変更しても book.title
が変更されるわけではありません。 Binding
はそんな感じです。 Binding
自体は全く特殊な型ではなく、 @Binding
なプロパティが変更されても body
を再実行するような機能も持ちません。ただ get
と set
を持つだけです。@Binding
なプロパティが変更されたときにそのViewの body
が再実行されるのは、 Binding
を渡した側のViewで body
が再実行され、それに伴って渡された側の View
も更新されるからです。struct FooView: View { @State var user: User var body: some View { Checkbox(isOn: $user.isPublic) } } struct Checkbox: View { @Binding var isOn: Bool var body: some View { Button { isOn.toggle() } label: { Image(isOn ? "Checkbox-On" : "Checkbox-Off") } } }
とした場合、 user.isPublic
が更新されるとそれを @Binding
で保持する Checkbox
の body
が再実行されるのではなく、 user
を @State
で保持する FooView
の body
が再実行されます。その結果として、 Checkbox(isOn: $user.isPublic)
が新しい isPublic
に対して実行され、 Checkbox
が更新されます。struct FooView: View { @State var user: User var body: some View { Checkbox(isOn: $user.isPublic) } } struct Checkbox: View { @Binding var isOn: Bool var body: some View { Button { isOn.toggle() } label: { Image(isOn ? "Checkbox-On" : "Checkbox-Off") } } }
とした場合、 user.isPublic
が更新されるとそれを @Binding
で保持する Checkbox
の body
が再実行されるのではなく、 user
を @State
で保持する FooView
の body
が再実行されます。その結果として、 Checkbox(isOn: $user.isPublic)
が新しい isPublic
に対して実行され、 Checkbox
が更新されます。 print
文を仕込んでみたりすれば body
が再実行されているかわかりますし、グローバル変数で保持された値のプロパティを無理やり Binding(get: { ... }, set: { ... })
で渡してみても、更新による再実行が起こらないことを確認できたりします。print
文を仕込んでみたりすれば body
が再実行されているかわかりますし、グローバル変数で保持された値のプロパティを無理やり Binding(get: { ... }, set: { ... })
で渡してみても、更新による再実行が起こらないことを確認できたりします。 @Binding
については昔こんな記事も書きましたので、よろしければご参考に:https://qiita.com/lovee/items/caef6df4baf1a3085441 そしてObservationへの移行についてはこちらも: https://qiita.com/lovee/items/4909d0166bba973b3bc0SomeView() .onLongPressGesture(minimumDuration: 2) { // ... } onPressingChanged: { isPressing in print(isPressing) // ←途中で指離してもちゃんと呼ばれて `isPressing` が `false` になる }
LongPressGesture(minimumDuration: 2) .onChanged { isPressing in print(isPressing) // ←指離しても呼ばれない、押した瞬間だけ `isPressing` が `true` になって呼ばれる } .onEnded { // ... }
ObservableObject
ベースでの議論で、その仕組みはSwiftUIのレイアウトエンジンは ObservableObject
全体を objectWillChange
Publisherの購読によって監視してるので、何か一つでもプロパティーに更新があったら ObservableObject
の全てのプロパティーに比較処理が走ってしまいます; ところが今iOS 17以上なら使える Observation
フレームワークの @Observable
なら、一つ一つのプロパティーが監視対象になるので、余計な比較処理や再レンダリング処理が省かれてだいぶパフォーマンスが良くなってるはずですので、iOS 17未満の対応が必要なければ是非こちらの利用を強くお勧めします protocol
の定義で Self
使えなかったりみたいなKotlin起因の制限がやはりあるので、Kotlinを知らない状態でやるの結構怖いなと思いました…TextField
だけじゃなくて Picker
とか Menu
とか TextEditor
でも同様みたいですね. ただ,List
が NavigationStack
の中にあれば問題ないみたいなので,SwiftUI の List
って初期の頃から触ってると NavigationView
(今の NavigationStack
) の中で使われること前提で作られている部分がかなり多かったので,この問題に関しても List
は NavigationStack
の中で使ってねってことなのかもしれません. https://discord.com/channels/291054398077927425/291054454793306112/1235477558863069234TextField
だけじゃなくて Picker
とか Menu
とか TextEditor
でも同様みたいですね. ただ,List
が NavigationStack
の中にあれば問題ないみたいなので,SwiftUI の List
って初期の頃から触ってると NavigationView
(今の NavigationStack
) の中で使われること前提で作られている部分がかなり多かったので,この問題に関しても List
は NavigationStack
の中で使ってねってことなのかもしれません. https://discord.com/channels/291054398077927425/291054454793306112/1235477558863069234 @Environment
でクラスを扱う場合に、 @EnvironmentObject
みたいに $foo.bar
で Binding
取り出せてほしいと思うんですけど、(iOS 17で)試してみたところできなそうだったんですが、何かいい方法ありますか?ドキュメント( https://developer.apple.com/documentation/swiftui/environment )見ても Environment
に projectedValue
は見当たらず・・・ 仕方なく、↓を作って $foo.bar
の代わりに _foo.bindings.bar
みたいにしてます。 import SwiftUI extension Environment where Value: AnyObject { var bindings: Wrapper { .init(environment: self) } @dynamicMemberLookup struct Wrapper { fileprivate let environment: Environment<Value> subscript<T>(dynamicMember keyPath: ReferenceWritableKeyPath<Value, T>) -> Binding<T> { Binding( get: { environment.wrappedValue[keyPath: keyPath] }, set: { newValue in environment.wrappedValue[keyPath: keyPath] = newValue } ) } } }
struct TitleEditView: View { @Environment(Book.self) private var book var body: some View { @Bindable var book = book TextField("Title", text: $book.title) } }
(edited)@Bindable @Environment
がうまくいかなくて諦めてました。Property Wrapperってローカル変数にも使えるんですね。プロパティじゃないといけないのかと思ってました。Environment
に projectedValue
生やしてくれたら一番いいんですけどねー。条件によって実装が異なる( Value
がクラスかどうかとか)から無理なのかな・・・$foo
が単に _foo.projectedValue
に読み替えられるならできるかと思ったけど@Bindable
結構独特な使い方ですよね、SwiftDataでも同じような感じで使われてて初めて触った時結構衝撃受けました@Bindable
結構独特な使い方ですよね、SwiftDataでも同じような感じで使われてて初めて触った時結構衝撃受けました