@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でもはみだしてくれるやろと雑に突撃して撃沈した感じです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が報告されますけど、内容がよくわかりませんでした 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 使うことにしました UIViewControllerRepresentableの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)
1$ob.property を省略を減らして書くと _ob.projectedValue[dynamicMember: \.property] という感じになります
1ObservedObject と 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)
1
1EXCLUDED_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.
2// 素早くトグルすれば値が生きている 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)") } } } ↑こう書き換えてみましたが、現象には変化無かったです。
1body の中で 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) }
1Int で起こっているのでそれはそうだと思います.(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の一部になるんで行けるようになる、のかな。
3twitter.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: { ... }) で渡してみても、更新による再実行が起こらないことを確認できたりします。
1
1print 文を仕込んでみたりすれば body が再実行されているかわかりますし、グローバル変数で保持された値のプロパティを無理やり Binding(get: { ... }, set: { ... }) で渡してみても、更新による再実行が起こらないことを確認できたりします。 @Binding については昔こんな記事も書きましたので、よろしければご参考に:https://qiita.com/lovee/items/caef6df4baf1a3085441 そしてObservationへの移行についてはこちらも: https://qiita.com/lovee/items/4909d0166bba973b3bc0
3SomeView() .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を知らない状態でやるの結構怖いなと思いました…
1TextField だけじゃなくて 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でも同じような感じで使われてて初めて触った時結構衝撃受けました
1struct ContentView: View { @State private var path: NavigationPath = .init() @State private var department: Department = .init() var body: some View { NavigationStack(path: $path) { Button("Visit Store") { let newStore: Store = .init() department.stores.append(newStore) path.append(newStore) } .navigationDestination(for: Store.self) { store in let storeIndex = department.stores.firstIndex { $0.id == store.id }! Button("See Item") { let newItem: Item = .init() department.stores[storeIndex].items.append(newItem) path.append(newItem) } .navigationDestination(for: Item.self) { item in VStack { let itemIndex = department.stores[storeIndex].items.firstIndex { $0.id == item.id }! Text("price") TextField( "", value: $department.stores[storeIndex].items[itemIndex].price, format: .number ) .keyboardType(.decimalPad) } } } } } } @Observable final class Department { var stores: [Store] = [] } struct Store: Hashable { var id: UUID = .init() var items: [Item] = [] } struct Item: Hashable { var id: UUID = .init() var price: Float = 100 } (edited)struct ContentView: View { @State private var price = 0.0 var body: some View { TextField("", value: $price, format: .number) .keyboardType(.decimalPad) } }
struct ContentView: View { @State private var path: NavigationPath = .init() @State private var department: Department = .init() var body: some View { NavigationStack(path: $path) { Button("Visit Store") { let newStore: Store = .init() department.stores.append(newStore) path.append(newStore) } .navigationDestination(for: Store.self) { store in let storeIndex = department.stores.firstIndex { $0.id == store.id }! NavigationLink("See Item") {// navigationDestination(...)のネストではなくNavigationLinkを用いると問題ない let newItem: Item = .init() department.stores[storeIndex].items.append(newItem) let itemIndex = department.stores[storeIndex].items.firstIndex { $0.id == newItem.id }! return VStack { Text("price") TextField( "", value: $department.stores[storeIndex].items[itemIndex].price, format: .number ) .keyboardType(.decimalPad) } } } } } } (edited)struct ContentView: View { @State private var price = 0.0 var body: some View { TextField("", value: $price, format: .currency(code: "yen")) .keyboardType(.decimalPad) } } (edited)navigationDestination でネストされていると入力が確定する前にViewのbodyが評価されて、number format が適用されているっぽいですね。 (edited)
1struct ItemView: View { @Binding var department: Department var storeIndex: Int var itemIndex: Int var body: some View { VStack { Text("price") TextField( "", value: $department.stores[storeIndex].items[itemIndex].price, format: .number ) .keyboardType(.decimalPad) } } } struct ContentView: View { @State private var path: NavigationPath = .init() @State private var department: Department = .init() var body: some View { NavigationStack(path: $path) { Button("Visit Store") { let newStore: Store = .init() department.stores.append(newStore) path.append(newStore) } .navigationDestination(for: Store.self) { store in let storeIndex = department.stores.firstIndex { $0.id == store.id }! Button("See Item") { let newItem: Item = .init() department.stores[storeIndex].items.append(newItem) path.append(newItem) } .navigationDestination(for: Item.self) { item in let itemIndex = department.stores[storeIndex].items.firstIndex { $0.id == item.id }! ItemView(department: $department, storeIndex: storeIndex, itemIndex: itemIndex) } } } } }navigationDestination の destination ブロックは特別っぽいです。 (edited)let _ = Self._printChanges()View で全部切り出せっていうのが教えっちゃあ教えなのでそういう想定のAPIなのかなと思います。
1
1let _ = Self._printChanges() を入れて、なぜ再評価されたかみるのが良いかもです。ObservationとObservableObjectを併用した方がパフォーマンスが良いと考えているのですが合ってますでしょうか?import SwiftUI import Observation struct ContentView: View { @State var count = 0 var body: some View { VStack { Text(count.description) Button { count += 1 print("count: \(count)") } label: { Text("count") } ChildView() } } } struct ChildView: View { @State var viewModel = ChildViewModel() var body: some View { Text("child") } } @MainActor @Observable final class ChildViewModel { init() { print("init") } deinit { print("deinit") } }StateObjectを使えば初回のinitだけが有効になるので、以下のようにObservableObjectを適用するとボタンを押すたびにinitとdeinitが発火されることはなくなります。import SwiftUI import Observation struct ContentView: View { @State var count = 0 var body: some View { VStack { Text(count.description) Button { count += 1 print("count: \(count)") } label: { Text("count") } ChildView() } } } struct ChildView: View { @StateObject var viewModel = ChildViewModel() var body: some View { Text("child") } } @MainActor @Observable final class ChildViewModel: ObservableObject { init() { print("init") } deinit { print("deinit") } }StateObject の init(wrappedValue:) がクロージャを受け取るのに対して State の init(wrappedValue:) は値を受け取るから,2 回目以降の (画面が開かれているうちは最初のだけがずっと使いまわされるので) 捨てられる運命にある ChildViewModel の init() と deinit が無駄に発火してしまっているところです.クロージャだとまだ init が実行されていないのでStateObject でも捨てられるのは一緒なんですがより遅延評価的な感じになっているという感じですね.wrappedValue 引数に渡しているクロージャが作られて捨てられます. (edited)init(wrappedValue:) が呼び出されて引数に渡したものが捨てられるという挙動自体は一緒という意味です.import SwiftUI import Observation struct ContentView: View { @State var count = 0 var body: some View { VStack { Text(count.description) Button { count += 1 print("count: \(count)") } label: { Text("count") } ChildView() } } } struct ChildView: View { @StateObject var viewModel: ChildViewModel init() { _viewModel = .init(wrappedValue: { print("called") return ChildViewModel() }()) } var body: some View { Text("child") } } @MainActor @Observable final class ChildViewModel: ObservableObject { init() { print("init") } deinit { print("deinit") } }calledがprintされると思ったのですが、初回しかprintされませんでした。なんでだろ{ { print("called"); return ChildViewModel() }() } を引数に渡しているのと同じことになってます. (edited)@Stateの場合、2回目以降に作られたインスタンスって本当に捨てられてるんですね。ためしにUUID持たせたら更新されるかと思ったらされませんでした。 import SwiftUI import Observation struct ContentView: View { @State var count = 0 var body: some View { VStack { Text(count.description) Button { count += 1 print("count: \(count)") } label: { Text("count") } ChildView() } } } struct ChildView: View { @State var viewModel = ChildViewModel() var body: some View { Text("child: \(viewModel.uuid)") } } @MainActor @Observable final class ChildViewModel { var uuid = UUID() init() { print("init \(uuid)") } deinit { print("deinit") } } (edited)init 89ABAE9C-B5DB-40F7-B710-AE95575C29D3 count: 1 init C4985631-EF43-4AF7-8684-999EE5CFD808 count: 2 init EE559DE9-F089-4EB2-AD93-CBDEC3645992 deinit count: 3 init 31DDF64E-71FB-475D-AE42-1D76FCDA9FBF deinit count: 4 init 65A0C8E2-2AA3-4BF5-B706-746587E5C9A3 deinit count: 5 init 24222547-C0AB-45ED-ABDD-9579D746C752 deinitStateObject でも観測できていないだけで自前クラスのインスタンスの代わりにクロージャのオブジェクトが毎回作られて捨てられているので,その両者の初期化・破棄コストを天秤にかけて判断って感じになるんじゃないでしょうか.