SwiftUI与UIkit建立接口

SwiftUI与一切苹果平台上的现有UI框架无缝合作。例如,能够将UIKit视图和视图控制器放置在SwiftUI视图中,反之亦然。 本教程向您展现怎么将主屏幕上的特征地标转换为包装UIPageViewController和UIPageControl的实例。您将运用UIPageViewController来显现SwiftUI视图的旋转木马,并运用状况变量和绑定来和谐整个用户界面中的数据更新。
按照过程构建此项目,或下载完结的项目自行探索。

第一节 创立视图以表示UIPageViewController

要在SwiftUI中表示UIKit视图和视图控制器,您能够创立契合UIViewRepresentable和UIViewControllerRepresentaable协议的类型。您的自界说类型创立和装备它们所代表的UIKit类型,而SwiftUI办理它们的生命周期并在需要时更新它们。

第八篇 SwiftUI Landmarks 与UIkit建立接口

第一步 创立PageViewController.swift

在Views分组下新增一个分组PageView,创立一个swift文件PageViewController.swift,界说一个恪守UIViewControllerRepresentable协议的结构体PageViewController
页面视图控制器存储页面实例的数组,该数组有必要是视图的类型。这些是您用来在地标之间滚动的页面。接下来增加UIViewControllerRepresentable协议的两个要求

import Foundation
import SwiftUI
struct PageViewController<Page: View>: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIPageViewController
    var pages: [Page]   
}

第二步 增加完结makeUIViewController(context:)

增加一个makeUIViewController(context:)办法,该办法能够创立具有所需装备的UIPageViewController。 SwiftUI在预备好显现视图时一次性调用此办法,然后办理视图控制器的生命周期。

func makeUIViewController(context: Context) -> UIPageViewController {
    let pageViewController = UIPageViewController(
        transitionStyle: .scroll,
        navigationOrientation: .horizontal)
    return pageViewController
}

第三步 完结updateUIViewController(_:context:)

增加一个updateUIViewController(:context:)办法,该办法调用setViewControllers(:direction:animated:)以供给用于显现的视图控制器。
现在,您要创立UIHostingController,它在每次更新时托管页面SwiftUI视图。稍后,您将经过在页面视图控制器的生命周期内只初始化一次控制器来进步功率。

func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
    pageViewController.setViewControllers([UIHostingController(rootView: pages[0])], direction: .forward, animated: true)
}

第四步 增加特征地标的图片资源

将下载的项目文件的资源目录中的图画拖到应用程序的财物目录中。
地标的特征图画(假如存在)与常规图画具有不同的尺寸。

第八篇 SwiftUI Landmarks 与UIkit建立接口

第五步 将计算特性增加到Landmark结构中,该结构将回来特征图画(假如存在)。

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

第六步 创立一个展现特征地标图片的视图

创立swiftUI视图FeatureCard.swift,用来展现特征地标图片

import SwiftUI
struct FeatureCard: View {
    var landmark: Landmark
    var body: some View {
        landmark.featureImage?
            .resizable()
            .aspectRatio(3.0/2.0, contentMode: .fit)
    }
}
struct FeatureCard_Previews: PreviewProvider {
    static var previews: some View {
        FeatureCard(landmark: ModelData().features[0])
    }
}

第七步 在图画上叠加有关地标的文本信息。


struct TextOverLay: View {
    var landmark: Landmark
    var gradient: LinearGradient {
        .linearGradient(Gradient(
            colors: [.black.opacity(0.6), .black.opacity(0)]),
            startPoint: .bottom,
            endPoint: .center)
    }
    var body: some View {
        ZStack(alignment: .bottomLeading) {
            gradient
            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)
                    .bold()
                Text(landmark.park)
            }
            .padding()
        }
        .foregroundColor(.white)
    }
}

第八步 创立一个视图用来展现UIViewControllerRepresentable视图

创立一个SwiftUI视图PageView.swift,并更新PageView类型以将PageViewController声明为子视图。
预览失利,由于Xcode无法揣度Page的类型。

第九步 更新预览供给程序以传递所需的视图数组,预览即可开始作业。

import SwiftUI
struct PageView<Page: View>: View {
    var pages: [Page]
    var body: some View {
        PageViewController(pages: pages)
    }
}
struct PageView_Previews: PreviewProvider {
    static var previews: some View {
        PageView(pages: ModelData().features.map { FeatureCard(landmark: $0)})
            .aspectRatio(3/2, contentMode: .fit)
    }
}

第二节 创立视图控制器的数据源

在短短的几个过程中,您已经完结了很多作业——PageViewController运用UIPageViewController来显现SwiftUI视图中的内容。现在是时候让滑动交互从一页移动到另一页了。

第八篇 SwiftUI Landmarks 与UIkit建立接口

一个代表UIKit视图控制器的SwiftUI视图,能够界说一个由SwiftUI办理和供给并作为视图上下文一部分的和谐类Coordinator

第一步 声明一个和谐类

在PageViewController内部声明一个嵌套和谐类Coordinator。
SwiftUI办理UIViewControllerRepresentable类型的和谐器,并在调用上面创立的办法时将其作为上下文的一部分供给。


class Coordinator: NSObject {
    var parent: PageViewController
    init(_ pageViewController: PageViewController) {
        self.parent = pageViewController
    }
}

第二步 和谐器生成办法

将另一个办法增加到PageViewController以生成和谐器。

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

SwiftUI在makeUIViewController(context:)之前调用这个makeCoordinator()办法,以便在装备视图控制器时能够拜访coordinator对象。
您能够运用此和谐器来完结常见的Cocoa形式,例如委托、数据源以及经过方针操作响应用户事情。

第三步 运用视图的页面数组初始化和谐器中的控制器数组。

和谐器是存储这些控制器的好当地,由于体系只初始化它们一次,并且在您需要它们更新视图控制器之前。

第四步 恪守UIPageViewControllerDataSource协议

和谐器恪守UIPageViewControllerDataSource协议,并完结两个必需的办法。这两种办法建立了视图控制器之间的联系,以便您能够在它们之间来回滑动。

第五步 增加和谐器作为UIPageViewController的数据源代理

第一步、第二步、第三步、第四步、第五步 代码总和

import Foundation
import SwiftUI
struct PageViewController<Page: View>: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIPageViewController
    var pages: [Page]
    //生成和谐器
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    //生成UIPageViewController
    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        return pageViewController
    }
    //更新UIPageViewController,切丁第一个要展现的视图控制器
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        pageViewController.setViewControllers([context.coordinator.controllers[1]], direction: .forward, animated: true)
    }
    class Coordinator: NSObject, UIPageViewControllerDataSource {
        var parent: PageViewController
        var controllers = [UIViewController]()
        init(_ pageViewController: PageViewController) {
            self.parent = pageViewController
            controllers = parent.pages.map { UIHostingController(rootView: $0) }
        }
        // MARK: - UIPageViewControllerDataSource
        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]
        }
    }
}

第六步 回到PageView,敞开预览,测验滑动操作

第三节 在SwiftUI视图的状况下盯梢页面

要预备增加自界说UIPageControl,您需要一种从PageView中盯梢当时页面的办法。 为此,您将在PageView中声明一个@State特点,并将该特点的绑定传递到PageViewController视图。PageViewController会更新绑定以匹配可见页面。

第八篇 SwiftUI Landmarks 与UIkit建立接口

第一步 增加currentPage

在PageViewController中增加一个绑定特点currentPage。

@Binding var currentPage: Int

除了声明@Binding特点外,还更新对setViewControllers(_:direction:animated:)的调用,传递currentPage绑定的值。

func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
 pageViewController.setViewControllers([context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
}

第二步 声明@State变量

在PageView中声明一个currentPage特点,且用@State修饰,在创立PageViewController时传递过去。\

@State private var currentPage = 0

运用$将状况存储特点与绑定特点相关起来

PageViewController(pages: pages, currentPage: $currentPage)

第三步 改动currentPage的值来测验PageViewController

增加一个带有currentPage特点的文本视图,这样您就能够关注@State特点的值。

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

请注意,当您从一页滑动到另一页时,该值不会改动。

第四步 和谐器恪守UIPageViewControllerDelegate

在PageViewController.swift中,使和谐器契合UIPageViewController Delegate,并增加PageViewController(_:didFinishAnimationing:previousViewControllers:transitionCompleted-completed:Bool)办法。
由于SwiftUI在页面切换动画完结时调用此办法,因而能够找到当时视图控制器的索引并更新绑定。


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
    }
}

第六步 将和谐器赋值给UIPageViewController的delegate

除了数据源之外,还将和谐器分配为UIPageViewController的委托。 在绑定双向连接的情况下,每次滑动后,文本视图都会更新以显现正确的页码。

pageViewController.delegate = context.coordinator

第四节 增加自界说页面控件

您已经预备好将自界说UIPageControl增加到视图中,该控件封装在SwiftUI UIViewRepresentable视图中。

第八篇 SwiftUI Landmarks 与UIkit建立接口

第一步 创立PageControl

创立一个SwiftUI视图PageControl,恪守UIViewRepresentable协议
UIViewRepresentable和UIViewControllerRepresentaable类型具有相同的生命周期,其办法与它们的底层UIKit类型相对应。

import SwiftUI
struct PageControl: UIViewRepresentable {
    typealias UIViewType = UIPageControl
    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
    }  
}

第二步 用页面控件替换文本框

从VStack切换到ZStack进行布局。 由于您正在将页面计数和绑定传递到当时页面,所以页面控件已经显现了正确的值。

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

接下来,使页面控件具有交互性,这样用户就能够点击一侧或另一侧在页面之间移动。

第三步 创立和谐器

在PageControl中创立一个嵌套的Coordinator类型,并增加一个makeCoordinators()办法来创立和回来一个新的和谐器。
由于像UIPageControl这样的UIControl子类运用方针操作形式而不是委派,所以这个Coordinator完结了一个@objc办法来更新当时页面绑定。

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

第四步 绑定valueChanged事情

增加和谐器作为valueChanged事情的方针,指定updateCurrentPage(sender:)办法作为要履行的操作。

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

第五步 在CategoryHome中,将占位符功用图画替换为新的页面视图。

import SwiftUI
struct CategoryHome: View {
    @EnvironmentObject var modelData: ModelData
    @State private var showProfile = 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(.inset)
            .navigationTitle(Text("Featured"))
            .toolbar {
                Button {
                    showProfile.toggle()
                } label: {
                    Label("User Profile", systemImage: "person.crop.circle")
                }
            }
            .sheet(isPresented: $showProfile) {
                ProfileHost().environmentObject(modelData)
            }
        }
    }
}
struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome().environmentObject(ModelData())
    }
}

测验一切不同的交互-PageView展现了UIKit和SwiftUI视图和控制器怎么协同作业。

总结

到这里,Landmarks这个小项意图一切功用就算完结了。
同学们有哪里不清楚或作者有哪里写的不对的当地,欢迎谈论区提意见或讨论