見出し画像

SwiftUI Tutorials - Interfacing with UIKit

UIKitとの連携方法です。画面遷移するためのUIPageViewController()をSwiftUIで使うために必要なことをやっていきます。

Create a View to Represent a UIPageViewController

UIKitのViewをSwiftUIで使う場合はUIViewControllerRepresentableプロトコルに準拠させることが必要です。

まず新規ファイルPageViewController.swift作り、UIViewControllerRepresentableプロトコルに準拠させます。

import SwiftUI
import UIKit

struct PageViewController<Page: View>: UIViewControllerRepresentable {
   var pages: [Page]
}

ページの元になるデータは配列に入れていきます。

UIViewControllerRepresentableで必要とされる要件を追加します。

func makeUIViewController(context: Context) -> UIPageViewController {
       let pageViewController = UIPageViewController(
           transitionStyle: .scroll,
           navigationOrientation: .horizontal)
       return pageViewController
   }
   
   
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
       pageViewController.setViewControllers(
           [UIHostingController(rootView: pages[0])], direction: .forward, animated: true)
   }

続きをする前に、ダウンロードしたResources directoryより画像をXcodeに入れておきます。

Landmark.swiftを編集します。

  var featureImage: Image? {
       isFeatured ? Image(imageName + "_feature") : nil
   }

を追加します。

次にFeatureCard.swiftを新規に作りSwiftUIファイルを作成し構造体FeatureCardを作ります。

struct FeatureCard: View {
   var landmark: Landmark
   var body: some View {
       landmark.featureImage?
           .resizable()
           .aspectRatio(3 / 2, contentMode: .fit)
           .overlay(TextOverlay(landmark: landmark))
   }
}
struct TextOverlay: View {
   var landmark: Landmark
   var gradient: LinearGradient {
       LinearGradient(
           gradient: Gradient(
               colors: [Color.black.opacity(0.6), Color.black.opacity(0)]),
           startPoint: .bottom,
           endPoint: .center)
   }
   var body: some View {
       ZStack(alignment: .bottomLeading) {
           Rectangle().fill(gradient)
           VStack(alignment: .leading) {
               Text(landmark.name)
                   .font(.title)
                   .bold()
               Text(landmark.park)
           }
           .padding()
       }
       .foregroundColor(.white)
   }
}

新規ファイルPageView.swiftを作り構造体PageViewでViewを作ります。

struct PageView<Page: View>: View {
   var pages: [Page]
   var body: some View {
       PageViewController(pages: pages)
   }
}


Create the View Controller’s Data Source

スワイプしてページを変更できるようにしていきます。

PageViewController.swiftを編集していきます。

SwiftUIでUIKitを表示するために

  class Coordinator: NSObject {
       var parent: PageViewController

       init(_ pageViewController: PageViewController) {
           parent = pageViewController
       }
   }

を定義します。

struct PageViewController<Page: View>: UIViewControllerRepresentable {
   var pages: [Page]
   
   func makeCoordinator() -> Coordinator {
       Coordinator(self)
   }
   
   func makeUIViewController(context: Context) -> UIPageViewController {
       let pageViewController = UIPageViewController(
           transitionStyle: .scroll,
           navigationOrientation: .horizontal)
       pageViewController.dataSource = context.coordinator
       return pageViewController
       
   }
   func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
       pageViewController.setViewControllers(
           [context.coordinator.controllers[0]], direction: .forward, animated: true)
   }
   class Coordinator: NSObject, UIPageViewControllerDataSource {
       var parent: PageViewController
       var controllers = [UIViewController]()
       init(_ pageViewController: PageViewController) {
           parent = pageViewController
           controllers = parent.pages.map { UIHostingController(rootView: $0) }
       }
       func pageViewController(
           _ pageViewController: UIPageViewController,
           viewControllerBefore viewController: UIViewController) -> UIViewController?
       {
           guard let index = controllers.firstIndex(of: viewController) else {
               return nil
           }
           if index == 0 {
               return controllers.last
           }
           return controllers[index - 1]
       }
       func pageViewController(
           _ pageViewController: UIPageViewController,
           viewControllerAfter viewController: UIViewController) -> UIViewController?
       {
           guard let index = controllers.firstIndex(of: viewController) else {
               return nil
           }
           if index + 1 == controllers.count {
               return controllers.first
           }
           return controllers[index + 1]
       }
   }
}

追加分ですが、

func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[context.coordinator.controllers[0]], direction: .forward, animated: true)
}

class Coordinator: NSObject, UIPageViewControllerDataSource {
var parent: PageViewController
var controllers = [UIViewController]()
init(_ pageViewController: PageViewController) {
parent = pageViewController
controllers = parent.pages.map { UIHostingController(rootView: $0) }
}

以上のコードを追加すると、PageView.swiftを実行してプレビューしてみると、画面遷移、画面をスワイプすることで切り替わるようになります。

Track the Page in a SwiftUI View’s State

View間でデータを追跡することができるように@State、@Bindingを付けた変数を宣言します。

PageViewController.swiftを編集していきます。まず変数宣言です。この変数でページ番号で表示する画面を決定します。

@Binding var currentPage: Int

そして、func updateUIViewController()を編集します。setViewControllersを更新することで@Bindingされた値を渡します。

   pageViewController.setViewControllers(
           [context.coordinator.controllers[currentPage]], direction: .forward, animated: true)

PageView.swiftを編集していきます。

struct PageView<Page: View>: View {
   var pages: [Page]
   
   @State private var currentPage = 0
   
   var body: some View {
       PageViewController(pages: pages, currentPage: $currentPage)
   }
}
@State private var currentPage = 0
PageViewController(pages: pages, currentPage: $currentPage)

を追加して currentPage の初期値、

@State private var currentPage = 0

を設定しています。そしてテキストを追加するためにコードを追加します。

 var body: some View {
       VStack {
           PageViewController(pages: pages, currentPage: $currentPage)
           Text("Current Page: \(currentPage)")
       }
   }
Text("Current Page: \(currentPage)")

次にプロトコルUIPageViewControllerDelegateに適合させます。

class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {}

必要なメソッドを追加します。ページ読み込み時にアニメーションを切り替えるたびに呼び出すために

parent.currentPage = index

とcurrentPageを更新します。

 func pageViewController(
           _ pageViewController: UIPageViewController,
           didFinishAnimating finished: Bool,
           previousViewControllers: [UIViewController],
           transitionCompleted completed: Bool) {
           if completed,
              let visibleViewController = pageViewController.viewControllers?.first,
              let index = controllers.firstIndex(of: visibleViewController) {
               parent.currentPage = index
           }
       }

最後にデータソースを追加します。

func makeUIViewController(context: Context) -> UIPageViewController {}

pageViewController.delegate = context.coordinator

を追加します。

Add a Custom Page Control

SwiftUIで組み上げられているCategoryHome.swiftにUIKitにUIViewRepresentableを適応させたものを入れ込みます。

UIViewRepresentable

同じライフサイクルで、UIViewControllerRepresentable

まず、新しいファイル、PageControl.swiftに以下のように UIViewRepresentableに適合させた構造体を作ります。必要なメソッドを書いていきます。

func makeUIView(context: Context)
func updateUIView
import SwiftUI
import UIKit

struct PageControl: UIViewRepresentable {
   var numberOfPages: Int
   @Binding var currentPage: Int
  
   func makeUIView(context: Context) -> UIPageControl {
       let control = UIPageControl()
       control.numberOfPages = numberOfPages
       return control
   }
  
   func updateUIView(_ uiView: UIPageControl, context: Context) {
       uiView.currentPage = currentPage
   }
}

次にPageView.swiftを編集します。

 var body: some View {
       ZStack(alignment: .bottomTrailing) {
           PageViewController(pages: pages, currentPage: $currentPage)
           PageControl(numberOfPages: pages.count, currentPage: $currentPage)
               .frame(width: CGFloat(pages.count * 18))
               .padding(.trailing)
       }
ZStack()
PageControl(numberOfPages: pages.count, currentPage: $currentPage)
.frame(width: CGFloat(pages.count * 18))
.padding(.trailing)

を修正しています。

次にPageControl.swiftに再度戻って、ユーザーがページを切り替えられるようにしていきます。

新しいメソッド

 func makeCoordinator() -> Coordinator {
       Coordinator(self)
   }

と、新しいCoordinatorを作ります。

  class Coordinator: NSObject {
       var control: PageControl
       init(_ control: PageControl) {
           self.control = control
       }
       @objc
       func updateCurrentPage(sender: UIPageControl) {
           control.currentPage = sender.currentPage
       }
   }

そしてもう一つ

 func makeUIView(context: Context) -> UIPageControl {
       let control = UIPageControl()
       control.numberOfPages = numberOfPages
       control.addTarget(
           context.coordinator,
           action: #selector(Coordinator.updateCurrentPage(sender:)),
           for: .valueChanged)
       return control
   }
control.addTarget(
context.coordinator,
action: #selector(Coordinator.updateCurrentPage(sender:)),
for: .valueChanged)

最後にCategoryHome.swiftに以下追加します。

 List {
          PageView(pages: modelData.features.map { FeatureCard(landmark: $0) })
             .aspectRatio(3 / 2, contentMode: .fit)
             .listRowInsets(EdgeInsets())
PageView(pages: modelData.features.map { FeatureCard(landmark: $0) })
.aspectRatio(3 / 2, contentMode: .fit)

新しい PageView()を追加します。全体です。

struct CategoryHome: View {
   @EnvironmentObject var modelData: ModelData
   @State private var showingProfile = false

   var body: some View {
       NavigationView {
           List {
               PageView(pages: modelData.features.map { FeatureCard(landmark: $0) })
                   .aspectRatio(3 / 2, contentMode: .fit)
                   .listRowInsets(EdgeInsets())
               ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                   CategoryRow(categoryName: key, items: modelData.categories[key]!)
               }
               .listRowInsets(EdgeInsets())
           }
           .listStyle(InsetListStyle())
           .navigationTitle("Featured")
           .toolbar {
               Button(action: { showingProfile.toggle() }) {
                   Image(systemName: "person.crop.circle")
                       .accessibilityLabel("User Profile")
               }
           }
           .sheet(isPresented: $showingProfile) {
               ProfileHost()
                   .environmentObject(modelData)
           }
       }
   }
}

この記事が気に入ったらサポートをしてみませんか?