🍣

[iOS18]NavigationLinkのisActiveによる画面遷移でデータが受け渡せない

2024/10/04に公開

要するにこんなコードがある場合、

import SwiftUI

/// ios18だと、StackChildViewのvalueの値が次の画面に伝わらない
struct StackParentView: View {
  @State var isNext: Bool = false
  @State var childValue: String = ""
  
  var body: some View {
    Button(action: {
      childValue = "Good bye."
      isNext = true
    },label: {
      Text("次の画面")
    })
    NavigationLink(
      destination: StackChildView(value: childValue),
      isActive: $isNext
    ) {
      EmptyView()
    } 
  }
}

struct StackChildView: View {
  @State var value: String
  init(value: String) {
    self.value = value
  }
  
  var body: some View {
    VStack {
      Text(UIDevice.current.systemVersion)
      Text(value)
    }
  }
}

ios17.5と、ios18.0の場合それぞれ下記のような動作になります。

ios17.5の場合
https://youtu.be/DuXih7uAR-8
ios18.0の場合
https://youtu.be/R11UdL8hfBk

ios18.0の場合は、1回目の遷移では、値が受け渡せていません。

'init(destination:isActive:label:)' was deprecated in iOS 16.0: use NavigationLink(value:label:), or navigationDestination(isPresented:destination:), inside a NavigationStack or NavigationSplitView

という警告は前から出てましたが、18になって大きく挙動が変わってしまいました。

これを修正する方法がいろいろあると思いますが、単純に解決しようとすると下記のような感じになると思います

/// 色々省略

    @State private var path = NavigationPath()
   
    NavigationStack(path: $path) {
      NavigationLink(destination: NewStackParentView(path: $path)) {
        Text("NewStackParentView")
          .font(.title2)
      }
      .navigationDestination(for: String.self) { selection in
        NewStackChildView(value: selection)
      }
    }

struct NewStackParentView: View {
  @Binding var path: NavigationPath
  
  var body: some View {
    VStack {
        Button(action: {
          path.append("hello")
        }, label: {
          Text("Push!")
        })
      }
  }
}

struct NewStackChildView: View {
  @State var value: String
  
    var body: some View {
      Text("\(value)")
    }
}

↑の動くサンプル

ただ、そんな単純なケースもあまりないと思いますので、EnvironmentとTimerと使ったサンプルを作ってみました。

struct ContentView: View {
  @Environment(PathVM.self) private var pathVM
  
  var body: some View {
    @Bindable var pathVM = pathVM
    NavigationStack(path: $pathVM.path) {
      VStack {
        Text("hello")
        Button("Push!") {
          pathVM.path.append("welcom")
        }
      }
      .navigationDestination(for: String.self) { selection in
        SubView(value: "\(selection)")
      }
      .navigationDestination(for: Route.self) { route in
        switch route {
        case .sub2:
          Sub2View()
        }
      }
    }
  }
}
enum Route: Hashable {
  case sub2
}

@Observable
class PathVM {
  var path = NavigationPath()
  var someVariable: String = ""
}

struct SubView: View {
  @State var value: String?
  @Environment(PathVM.self) private var pathVM
  @State var timer: Timer?
  @State var buttonDisable: Bool = false
  
  var body: some View {
    VStack {
      Text("ここは、SubViewです")
      Button("このボタンを押すと3秒後にSub2Viewに移動します") {
        buttonDisable = true
        // Web APIなどで通信が完了したら画面遷移するイメージ
        timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { timer in
          pathVM.someVariable = "from SubView Variable"
          pathVM.path.append(Route.sub2)
          buttonDisable = false
        }
      }
      .disabled(buttonDisable)
    }
    .onDisappear {
      timer?.invalidate()
      buttonDisable = false
    }
  }
}

struct Sub2View: View {
  @State var value: String?
  @Environment(PathVM.self) private var pathVM
  
  var body: some View {
    VStack {
      Text(pathVM.someVariable)
    }
  }
}

https://youtu.be/TDtkn8ix1Qs?feature=share

↑の動くサンプル

Timerのところは、apiに問い合わせて、レスポンスを待つところを簡易的に表現してみました。

しくみのテックブログ

Discussion