SwiftUI - Xcode 11 beta 5 での変更点

# はじめに
`Xcode 11 beta 5`が公開されたので、SwiftUI Tutorialsで確認できたSwiftUIの`beta 4` → `beta 5`での変更点について、以下にまとめていきます。
（文末に`beta 3`、`beta 4`での変更点も記載しています。）

# beta 4 → beta 5 での変更点

本記事内で触れていない変更点などもありますので、詳細を知りたい場合は公式のリリースノートを参照してみてください。

 - [iOS & iPadOS 13 Beta 5 Release Notes](https://developer.apple.com/documentation/ios_ipados_release_notes/ios_ipados_13_beta_5_release_notes)
 - [SwiftUI Latest Beta](https://developer.apple.com/documentation/swiftui?changes=latest_beta)

## SwiftUI APIs deprecatedの削除

> SwiftUI APIs deprecated in previous betas are now removed. (52587863)

beta 4 までに deprecated になった SwiftUI関連のAPI は beta 5 で削除されました。
deprecatedを放置していた場合、ビルドエラーになるので対応が必要になります。

## BindableObjectの仕様変更

> BindableObject is replaced by the ObservableObject protocol from the Combine framework. (50800624)
You can manually conform to ObservableObject by defining an objectWillChange publisher that emits before the object changes. However, by default, ObservableObject automatically synthesizes objectWillChange and emits before any @Published properties change.

`BindableObject`がdeprecatedに変更され、`Combine.ObservableObject`と`Swift.Identifiable`の利用が推奨となりました。
この変更により、お決まりのコードは自動生成されるようになったので以下のように非常にスッキリとしたコードで書けるようになっています。

`@Published`の詳細については、CombineのPublisherに関する公式ドキュメントを参照してみてください。
（まだ、あまり理解できてません。。。）

 - [Publisher protocol](https://developer.apple.com/documentation/combine/publisher)

### Landmarks/Models/UserData.swift
```diff
- final class UserData: BindableObject {
-     let willChange = PassthroughSubject<Void, Never>()
-     
-     var showFavoritesOnly = false {
-         willSet {
-             willChange.send()
-         }
-     }
- 
-     var landmarks = landmarkData {
-         willSet {
-             willChange.send()
-         }
-     }
+ final class UserData: ObservableObject {
+     @Published var showFavoritesOnly = false
+     @Published var landmarks = landmarkData
  }
```

## Lengthの仕様変更

> The Length type is replaced by CGFloat. (50654095)

`Length`が`CGFloat `に変名されました。

### Landmarks/Hike/GraphCapsule.swift
```diff
  var index: Int
- var height: Length
+ var height: CGFloat
  var range: Range<Double>
  var overallRange: Range<Double>
- var heightRatio: Length {
-     max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
+ var heightRatio: CGFloat {
+     max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
  }
```

## SegmentedControlの仕様変更

> SegmentedControl is now a style of Picker. (51769046)

`SegmentedControl`がdeprecatedに変更され、`Picker `と`.pickerStyle(.segmented)`の利用が推奨となりました。

```diff
  VStack(alignment: .leading, spacing: 20) {
      Text("Seasonal Photo").bold()      
-     SegmentedControl(selection: $profile.seasonalPhoto) {
+     Picker("Seasonal Photo", selection: $profile.seasonalPhoto) {
          ForEach(Profile.Season.allCases, id: \.self) { season in
              Text(season.rawValue).tag(season)
          }
      }
+     .pickerStyle(SegmentedPickerStyle())
  }
  .padding(.top)
```

## その他の変更点
 - リファクタリング
   - ファイルのフォルダ構成がXcodeプロジェクトのフォルダ構成と同じになるように変更されていました。
   - 初期化処理が、`.init()`から`XXX()`に変更されていました。（可読性を向上させるためかと思われます。）

---
# beta 3 → beta 4 での変更点

## ~~BindableObjectの仕様変更~~

※ BindableObjectは`beta5`でdeprecatedになりました。


> The BindableObject protocol’s requirement is now willChange instead of didChange, and should now be sent before the object changes rather than after it changes. This change allows for improved coalescing of change notifications. (51580731)

`didChange`が`willChange`に変更されました。
仕様自体もオブジェクトが変更された後ではなく、変更される前に送信されるようになり、変更通知の統合が改善されたようです。

### Landmarks/Models/UserData.swift
```diff
 final class UserData: BindableObject {
-    let didChange = PassthroughSubject<UserData, Never>()
+    let willChange = PassthroughSubject<Void, Never>()
     
     var showFavoritesOnly = false {
-        didSet {
-            didChange.send(self)
+        willSet {
+            willChange.send()
         }
     }
 
     var landmarks = landmarkData {
-        didSet {
-            didChange.send(self)
+        willSet {
+            willChange.send()
         }
     }
 }
```

## Textの仕様変更

> The color(_:) modifier for Text is renamed foregroundColor(_:) for consistency with the more general foregroundColor(_:) view modifier. (50391847)

`color`が`foregroundColor`に変名されました。

### Landmarks/CategoryRow.swift
```diff
  Text(landmark.name)
-     .color(.primary)
+     .foregroundColor(.primary)
      .font(.caption)
```

## Collectionの仕様変更

> The identified(by:) method on the Collection protocol is deprecated in favor of dedicated init(_:id:selection:rowContent:) and init(_:id:content:) initializers. (52976883)

`identified(by:)`メソッドがdeprecatedに変更され、`ForEach(_:id:)`または`List(_:id:)`の利用が推奨となりました。

### Landmarks/Home.swift
```diff
- ForEach(categories.keys.sorted().identified(by: \.self)) { key in
+ ForEach(categories.keys.sorted(), id: \.self) { key in
      CategoryRow(categoryName: key, items: self.categories[key]!)
  }
  .listRowInsets(EdgeInsets())
```

# DatePickerの仕様変更

> 'init(_:minimumDate:maximumDate:displayedComponents:)' is deprecated: DatePicker labels are now required

labelを引数に取らない初期化メソッドがdeprecatedになりました。

### Landmarks/Profiles/ProfileEditor.swift
```diff
  DatePicker(
-     $profile.goalDate,
+     "Goal Date",
+     selection: $profile.goalDate,
      minimumDate: Calendar.current.date(byAdding: .year, value: -1, to: profile.goalDate),
      maximumDate: Calendar.current.date(byAdding: .year, value: 1, to: profile.goalDate),
      displayedComponents: .date)
```

なお、beta 4のSwiftUI Tutorialでは、仕様変更の対応以外に次のリファクタリングも行われていました。

```diff
+ var dateRange: ClosedRange<Date> {
+     let min = Calendar.current.date(byAdding: .year, value: -1, to: profile.goalDate)!
+     let max = Calendar.current.date(byAdding: .year, value: 1, to: profile.goalDate)!
+     return min...max
+ }
    
  DatePicker(
-     $profile.goalDate,
-     minimumDate: Calendar.current.date(byAdding: .year, value: -1, to: profile.goalDate),
-     maximumDate: Calendar.current.date(byAdding: .year, value: 1, to: profile.goalDate),
+     "Goal Date",
+     selection: $profile.goalDate,
+     in: dateRange,
      displayedComponents: .date)
```
## AnyTransitionの仕様変更

> Cannot invoke 'scale' with no arguments

`scale`がメソッドからプロパティに変更されました。

### Landmarks/HikeView.swift
```diff
-        let removal = AnyTransition.scale()
+        let removal = AnyTransition.scale
```

## presentation(_:), Sheet, Modal, PresentationLink

> Added improved presentation modifiers: sheet(isPresented:onDismiss:content:), actionSheet(isPresented:content:), and alert(isPresented:content:) — along with isPresented in the environment — replace the existing presentation(_:), Sheet, Modal, and PresentationLink types. (52075730)

presentation(_:), Sheet, Modal, PresentationLinkがdeprecatedになりました。
併せて、これらの機能に相当する機能がViewのメソッドとして追加されています。

 - sheet(isPresented:onDismiss:content:)
 - actionSheet(isPresented:content:)
 - alert(isPresented:content:)

SwiftUI Tutorialsでは`PresentationLink`が利用されていましたが、`sheet(isPresented:onDismiss:content:)`に変更されていました。

### Landmarks/Home.swift
```diff
+ @State var showingProfile = false
+ 
+ var profileButton: some View {
+     Button(action: { self.showingProfile.toggle() }) {
+         Image(systemName: "person.crop.circle")
+             .imageScale(.large)
+             .accessibility(label: Text("User Profile"))
+             .padding()
+     }
+ }
+
  var body: some View {
      NavigationView {
          List {
              FeaturedLandmarks(landmarks: featured)
                  .scaledToFill()
                  .frame(height: 200)
                  .clipped()
                  .listRowInsets(EdgeInsets())
              
              ForEach(categories.keys.sorted(), id: \.self) { key in
                  CategoryRow(categoryName: key, items: self.categories[key]!)
              }
              .listRowInsets(EdgeInsets())
              
              NavigationLink(destination: LandmarkList()) {
                  Text("See All")
              }
          }
          .navigationBarTitle(Text("Featured"))
-         .navigationBarItems(trailing:
-             PresentationLink(destination: ProfileHost()) {
-                 Image(systemName: "person.crop.circle")
-                     .imageScale(.large)
-                     .accessibility(label: Text("User Profile"))
-                     .padding()
-             }
-         )
+         .navigationBarItems(trailing: profileButton)
+         .sheet(isPresented: $showingProfile) {
+             ProfileHost()
+         }
      }
  }
```

## Animationの仕様変更

> Updated the APIs for creating animations. The basic animations are now named after the curve type — such as linear and easeInOut. The interpolation-based spring(mass:stiffness:damping:initialVelocity:) animation is now interpolatingSpring(mass:stiffness:damping:initialVelocity:), and fluidSpring(stiffness:dampingFraction:blendDuration:timestep:idleThreshold:) is now spring(response:dampingFraction:blendDuration:) or interactiveSpring(response:dampingFraction:blendDuration:), depending on whether or not the animation is driven interactively. (50280375)

spring(mass:stiffness:damping:initialVelocity:), fluidSpring(stiffness:dampingFraction:blendDuration:timestep:idleThreshold:)がdeprecatedになりました。
併せて、これらのメソッドに相当するメソッドが新たに追加されています。

 - interpolatingSpring(mass:stiffness:damping:initialVelocity:)
 - spring(response:dampingFraction:blendDuration:)
 - interactiveSpring(response:dampingFraction:blendDuration:)

### Landmarks/Supporting View/GraphCapsule.swift
```diff
  var animation: Animation {
-     Animation.spring(initialVelocity: 5)
+     Animation.spring(dampingFraction: 0.5)
      	.speed(2)
       	.delay(0.03 * Double(index))
  }
```

## その他の変更点
 - リファクタリング
   - LandmarkListへの遷移元（Home.swift）でNavigationViewが生成されているため、LandmarkListではNavigationViewが削除されました。

### Landmarks/LandmarkList.swift
```diff
  var body: some View {
-     NavigationView {
-         List {
-             Toggle(isOn: $userData.showFavoritesOnly) {
-                 Text("Show Favorites Only")
-             }
-             
-             ForEach(userData.landmarks) { landmark in
-                 if !self.userData.showFavoritesOnly || landmark.isFavorite {
-                     NavigationLink(
-                         destination: LandmarkDetail(landmark: landmark)
-                             .environmentObject(self.userData)
-                     ) {
-                         LandmarkRow(landmark: landmark)
-                     }
+     List {
+         Toggle(isOn: $userData.showFavoritesOnly) {
+             Text("Show Favorites Only")
+         }
+         
+         ForEach(userData.landmarks) { landmark in
+             if !self.userData.showFavoritesOnly || landmark.isFavorite {
+                 NavigationLink(
+                     destination: LandmarkDetail(landmark: landmark)
+                 ) {
+                     LandmarkRow(landmark: landmark)
                  }
              }
          }
-         .navigationBarTitle(Text("Landmarks"), displayMode: .large)
      }
+     .navigationBarTitle(Text("Landmarks"), displayMode: .large)
  }
```

---
# beta 2 → beta 3 での変更点

## 画面遷移に関する構造体の仕様変更
構造体名が `XXXButton` から `XXXLink` に変更されました。
SwiftUI Tutorials では以下の2つが利用されており、名前が変更されています。

|beta 2|beta 3|
|:-:|:-:|
|NavigationButton|NavigationLink|
|~~PresentationButton~~|~~PresentationLink~~|
※ PresentationLinkは`beta4`でdeprecatedになりました。

### Landmarks/Home.swift
```diff
-     NavigationButton(destination: LandmarkList()) {
+     NavigationLink(destination: LandmarkList()) {
          Text("See All")
      }
  }
  .navigationBarTitle(Text("Featured"))
  .navigationBarItems(trailing:
-     PresentationButton(destination: ProfileHost()) {
+     PresentationLink(destination: ProfileHost()) {
          Image(systemName: "person.crop.circle")
              .imageScale(.large)
              .accessibility(label: Text("User Profile"))
```

## ScrollViewの仕様変更
`beta 3`では`Axis.Set`と`showsIndicators`の組合せで振る舞いを指定するように仕様変更されているのですが、`beta 2`では指定できていたBounceの指定や細かい組合せが実現できないように見えます。

### `beta 2`の`ScrollView`のinit仕様
```swift
public init(isScrollEnabled: Bool = true, alwaysBounceHorizontal: Bool = false, alwaysBounceVertical: Bool = false, showsHorizontalIndicator: Bool = true, showsVerticalIndicator: Bool = true, content: () -> Content)
```

### `beta 3`の`ScrollView`のinit仕様
```swift
public init(_ axes: Axis.Set = .vertical, showsIndicators: Bool = true, content: () -> Content)
```

### Landmarks/CategoryRow.swift
```diff

- ScrollView(showsHorizontalIndicator: false) {
+ ScrollView(.horizontal, showsIndicators: false) {
      HStack(alignment: .top, spacing: 0) {
          ForEach(self.items.identified(by: \.name)) { landmark in
```

## TextFieldの仕様変更
`beta 2`での初期化方法が`deprecated`に変更され、`beta 3`では新たな初期化方法が追加されました。

### Landmarks/Profiles/ProfileEditor.swift
```diff

  HStack {
      Text("Username").bold()
      Divider()
-     TextField($profile.username)
+     TextField("Username", text: $profile.username)
  }
```

## UIWindowの仕様変更


SwiftUI Tutorialsで利用されていた初期化方法が、`beta2`と`beta3`では異なりました。
`beta 2`では、UIViewから継承したinitが利用されていましたが、`beta3`ではUIWindowとして定義されているinitが利用されており、より良い方法に変更されたようです。

### Landmarks/SceneDelegate.swift
```diff

- let window = UIWindow(frame: UIScreen.main.bounds)
- window.rootViewController = UIHostingController(rootView: CategoryHome().environmentObject(UserData()))
- self.window = window
- window.makeKeyAndVisible()
+ if let windowScene = scene as? UIWindowScene {
+     let window = UIWindow(windowScene: windowScene)
+     window.rootViewController = UIHostingController(rootView: CategoryHome().environmentObject(UserData()))
+     self.window = window
+     window.makeKeyAndVisible()
+ }
```

# 最後に
上記内容は、SwiftUI Tutorialsの変更点を元に調査を行い変更点をまとめました。
繰り返しとなりますが、本記事内で触れていない変更点などもありますので、詳細を知りたい場合は公式のリリースノートを参照してみてください。

 - [iOS & iPadOS 13 Beta 5 Release Notes](https://developer.apple.com/documentation/ios_ipados_release_notes/ios_ipados_13_beta_5_release_notes)
 - [SwiftUI Latest Beta](https://developer.apple.com/documentation/swiftui?changes=latest_beta)

<img width="1078" alt="Xcode 11.0 beta2 - Xcode 11.0 beta 3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63892/9a5c67f3-8035-eae3-fbd6-be96db35d779.png">