SwiftUI框架详细解析 (二十) —— 基于SwiftUI的Document-Based App的创建(一)

版本记录

版本号 时间
V1.0 2021.01.07 星期四

前言

今天翻阅苹果的API文档,发现多了一个框架SwiftUI,这里我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. SwiftUI框架详细解析 (一) —— 基本概览(一)
2. SwiftUI框架详细解析 (二) —— 基于SwiftUI的闪屏页的创建(一)
3. SwiftUI框架详细解析 (三) —— 基于SwiftUI的闪屏页的创建(二)
4. SwiftUI框架详细解析 (四) —— 使用SwiftUI进行苹果登录(一)
5. SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二)
6. SwiftUI框架详细解析 (六) —— 基于SwiftUI的导航的实现(一)
7. SwiftUI框架详细解析 (七) —— 基于SwiftUI的导航的实现(二)
8. SwiftUI框架详细解析 (八) —— 基于SwiftUI的动画的实现(一)
9. SwiftUI框架详细解析 (九) —— 基于SwiftUI的动画的实现(二)
10. SwiftUI框架详细解析 (十) —— 基于SwiftUI构建各种自定义图表(一)
11. SwiftUI框架详细解析 (十一) —— 基于SwiftUI构建各种自定义图表(二)
12. SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)
13. SwiftUI框架详细解析 (十三) —— 基于SwiftUI创建Mind-Map UI(二)
14. SwiftUI框架详细解析 (十四) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(一)
15. SwiftUI框架详细解析 (十五) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(二)
16. SwiftUI框架详细解析 (十六) —— 基于SwiftUI简单App的Dependency Injection应用(一)
17. SwiftUI框架详细解析 (十七) —— 基于SwiftUI简单App的Dependency Injection应用(二)
18. SwiftUI框架详细解析 (十八) —— Firebase Remote Config教程(一)
19. SwiftUI框架详细解析 (十九) —— Firebase Remote Config教程(二)

开始

首先看下主要内容:

SwiftUI使创建与iOS文档交互系统一起使用的基于文档的应用程序比以往更加容易。 在本教程中,您将学习如何创建基于SwiftUI文档的meme-maker应用。内容来自翻译

接着看下写作环境:

Swift 5, iOS 14, Xcode 12

下面就是正文啦

Documents是计算的中心,基于SwiftUI文档的应用程序比以往更容易使用iOS文档交互系统。iOS文档交互系统基于将您的应用程序与Files应用程序的所有出色功能集成在一起,

在本教程中,您将使用MemeMaker,该应用程序可让您创建自己的memes并将其保留为自己的meme文档类型。

您将了解以下主题:

  • 什么是Uniform Type Identifiers (UTI)
  • 哪些组件包含SwiftUI基于文档的应用程序?
  • 您如何定义自己的具有唯一扩展名的文档?
  • 如何在iOS / iPadOSmacOS上运行基于SwiftUI文档的应用程序?

事不宜迟,该一起看下了。

打开已有的初始项目。

构建并运行。 这是该应用程序的外观:

点击右上角的+按钮以创建一个新文档。 文本编辑器将打开“ Hello,world!”。 如图所示。 将文本更改为SwiftUI rocks! 然后点击左上角的back button按钮关闭文档。

注意:在撰写本文时,存在一些与SwiftUI基于文档的应用程序有关的bug。 有时您会在导航栏中看不到后退按钮或添加项目按钮之类的按钮,因为它们默认为白色。

切换到Browse选项卡以查找刚刚创建的文档。 该选项卡如下所示:

通过点击打开新文件。文本编辑器打开,您可以阅读输入的文本。

这是创建memes编辑器的良好起点。您将修改此应用程序,以便它与memes文档类型一起使用,而不是处理原始文本。这是UTI的作用。


Defining Exported Type Identifiers

用Apple的话Apple’s words来说,Unique Type Identifier or UTI是“特定文件类型,数据类型,目录或bundle type的唯一标识符”。例如,JPEG图像是一种特殊的文件类型,由UTI字符串public.jpeg唯一标识。同样,UTI net.daringfireball.markdown可以唯一标识以流行的Markdown markup language编写的文本文件。

UTI的值是什么?由于UTI是唯一的标识符,因此它们为您的应用程序提供了一种明确的方式来告知操作系统它可以打开和创建哪种类型的文档。由于iOS并未附带对“meme”文档的内置支持,因此您需要在应用中为Meme文件添加新的UTI。这在Xcode中很简单。

在深入研究代码之前,您需要对项目设置进行一些更改。

在项目设置中选择MemeMaker(iOS)target,选择Info选项卡,然后展开Exported Type Identifiers部分。

在这里定义文档的类型和元数据。当前,这仍然是为文本文档设置的。

进行以下更改:

  • Description更改为A meme created with MemeMaker。您可以查看说明,例如在Finder的信息窗口中。
  • Identifier更改为com.raywenderlich.MemeMaker.meme。其他应用程序可以使用此标识符来导入您的文档。
  • Conforms更改为“public.data,public.content”。这些是UTI,它们描述了您的UTI使用的数据类型。用编程的话来说,您可以将它们视为您的UTI遵循的协议。您可以使用许多类型,例如public.datapublic.image。您可以在Apple’s documentationWikipedia中找到所有可用的UTI的列表。
  • Extension更改为meme。这是.meme文件扩展名,改扩展名已添加到使用MemeMaker创建的文档中。

很好! 现在,您可以使用新扩展名.meme创建文档了。


Using a DocumentGroup

DocumentGroup是展示用于处理文档的系统UI的场景。 您可以在上面的屏幕截图中看到它的外观。 SwiftUI使使用文档浏览器变得超级容易。 只需遵循MemeMakerApp.swift中的代码即可:

DocumentGroup(newDocument: MemeMakerDocument()) { file in
  ContentView(document: file.$document)
}

DocumentGroup在处理文档时有两个初始化程序:init(newDocument:editor :)init(viewing:viewer :)。 第一个允许您创建新文档和编辑现有文档,而第二个则只能查看文件。 因为您要创建和编辑memes,所以入门项目将使用第一个初始化程序。

初始化程序收到它应该显示的文档。 在这种情况下,您将初始化一个新的空MemeMakerDocument,将在以后使用。 初始化程序还会收到一个用于构建文件编辑视图的闭包。


Working With a File Document

FileDocument是应用可以读取和写入设备的文档的基本协议。该协议包含两个静态属性:readContentTypeswritableContentTypes。两者都是UTType数组,分别定义了文档可以读取和写入的类型。仅要求readyContentTypes,因为writableContentTypes也默认为readContentContentTypes

FileDocument还需要带有FileDocumentReadConfiguration的初始化程序。此配置将UTType形式的文档类型与包含其内容的FileWrapper捆绑在一起。

最后,任何符合FileDocument的类或结构体都需要实现fileWrapper(configuration :)。写入文档时会调用它,它以FileDocumentWriteConfiguration作为参数,类似于读取配置,但用于写入。

听起来可能需要做很多工作,但请放心。在本教程的这一部分中,您将了解如何使用这两种配置。

1. Defining Exported UTTypes

打开MemeMakerDocument.swift。在文件顶部,您会在UTType上找到扩展名,该扩展名定义了入门项目正在使用的类型。

用以下代码替换此扩展名:

extension UTType {
  static let memeDocument = UTType(
    exportedAs: "com.raywenderlich.MemeMaker.meme")
}

在上面的代码中,您将memeDocument定义为新的UTType,以便可以在下一步中使用它。

仍在MemeMakerDocument.swift中,找到readableContentTypes。 如前所述,这定义了应用可以读取和写入的UTType列表。 用以下新代码替换属性:

static var readableContentTypes: [UTType] { [.memeDocument] }

这会将您之前创建的新类型设置为MemeMakerDocument文档可以读取的类型。 由于writableContentTypes默认为readContentContentTypes,因此您无需添加它。

2. Creating the Data Model

在继续使用MemeMakerDocument之前,您需要定义其适用的meme。 在Shared组中创建一个名为Meme.swift的新Swift文件,并选中Targets中的两个复选框,以便将其同时包含在iOSmacOS target中。

添加以下代码:

struct Meme: Codable {
  var imageData: Data?
  var topText: String
  var bottomText: String
}

MemeMaker会将Meme保存到磁盘。 它符合Codable,因此您可以使用JSONEncoderJSONDecoder将其转换为Data并返回。 它还包装了表示一个Meme所需的所有信息:两个字符串和一个图像数据。

再次打开MemeMakerDocument.swift并在类的开头找到以下代码:

var text: String
  
init(text: String = "Hello, world!") {
  self.text = text
}

MemeMakerDocument现在可以保存实际的Meme而不是文本。 因此,将这些行替换为以下代码:

// 1
var meme: Meme

// 2
init(
  imageData: Data? = nil, 
  topText: String = "Top Text", 
  bottomText: String = "Bottom Text"
) {
  // 3 
  meme = Meme(
    imageData: imageData, 
    topText: topText, 
    bottomText: bottomText)
}

上面的代码就是这样:

  • 1) 这是由MemeMakerDocument实例表示的meme
  • 2) 您为MemeMakerDocument定义了一个初始化程序。 初始化程序接收图像的数据以及顶部和底部的文本。
  • 3) 最后,根据这些参数初始化一个新的Meme

此时,您将看到代码中的错误。 不用担心-在保存和加载文件时,还需要进行一些其他更改以对文档进行编码和解码。

3. Encoding and Decoding the Document

首先,对fileWrapper(configuration :)进行更改。 用以下几行替换方法主体:

let data = try JSONEncoder().encode(meme)
return .init(regularFileWithContents: data)

这会将meme转换为数据,并创建一个WriteConfiguration,系统将其用于将该文档写入磁盘。

接下来,将init(configuration :)的主体替换为以下代码:

guard let data = configuration.file.regularFileContents else {
  throw CocoaError(.fileReadCorruptFile)
}
meme = try JSONDecoder().decode(Meme.self, from: data)

打开现有文档时,应用程序将调用此初始化程序。 您尝试从给定的ReadConfiguration中获取数据,并将其转换为Meme的实例。 如果该过程失败,则初始化程序将引发系统要处理的错误。

现在,您已经添加了对自定义meme文档进行读写的支持。 但是,由于您没有显示Meme编辑器,因此用户仍然看不到其中任何一个。 您将在下一部分中解决该问题。


Providing a Custom Editor

目前,该应用程序使用TextEditor。 基于SwiftUI文档的多平台应用程序的模板从此视图开始。 用于显示可编辑和可滚动的文本。

TextEditor不适合创建和编辑模因,因此您将创建自己的视图来编辑MemeMakerDocument

在开始创建新的编辑器视图之前,您将删除旧的视图。 打开ContentView.swift并将主体替换为空视图:

Spacer()

这样可以确保在构建新编辑器时不会出现编译器错误。

1. Creating the Image Layer

编辑器将包含两个子视图。 您将在创建实际的编辑器之前创建它们。

第一个是ImageLayer,它代表图像。 在Shared中创建一个名为ImageLayer.swift的新SwiftUI View文件,并在Targets中选中MemeMaker(iOS)MemeMaker(macOS)的复选框。 用以下内容替换文件中的两个结构:

struct ImageLayer: View {
  // 1
  @Binding var imageData: Data?

  // 2
  var body: some View {
    NSUIImage.image(fromData: imageData ?? Data())
      .resizable()
      .aspectRatio(contentMode: .fit)
  }
}

// 3
struct ImageLayer_Previews: PreviewProvider {
  static let imageData = NSUIImage(named: "AppIcon")!.data

  static var previews: some View {
    ImageLayer(imageData: .constant(imageData))
      .previewLayout(.fixed(width: 100, height: 100))
  }
}

上面的代码正在执行以下操作:

  • 1) ImageLayer具有SwiftUI绑定到meme图像的数据。 在后续步骤中,MemeEditor将数据传递到此视图。
  • 2) 它的bodyNSUIImage组成,NSUIImage是您使用图像数据初始化的视图。 您可能想知道这种view是什么。 这是iOS上的UIImagemacOS上的NSImage的类型别名,以及扩展名。 它允许使用一种常见的图像类型,在两种平台上具有相同的方法和属性。 您可以在iOS组的NSUIImage_iOS.swift文件和macOS组的NSUIImage_macOS.swift中找到它。 根据您运行的是MemeMaker(iOS)还是MemeMaker(macOS),它使用正确的类型。
  • 3) 最后,添加预览以支持Xcode的预览功能。

查看预览以确保您的视图正在显示图像:

现在您正在显示图像,您可以继续显示文本!

2. Creating the Text Layer

TextLayer是第二个子视图,它将顶部和底部文本放置在图像上方。 同样,在Shared中创建一个新的SwiftUI View文件,并将其命名为TextLayer.swift。 切记将MemeMaker(iOS)MemeMaker(macOS)选中为Targets

将此替换为生成的TextLayer结构体:

struct TextLayer<ImageContent: View>: View {
  @Binding var meme: Meme
  let imageContent: () -> ImageContent
}

TextLayer具有两个属性:meme,保存显示的Meme; 和imageContentimageContent是一个闭包,用于在TextLayerbody中创建另一个视图。 请注意,您已将视图声明为通用结构,其中图像内容视图可以是遵循View的任何内容。

接下来,将body添加到视图中:

var body: some View {
  ZStack(alignment: .bottom) {
    ZStack(alignment: .top) {
      imageContent()
      MemeTextField(text: $meme.topText)
    }

    MemeTextField(text: $meme.bottomText)
  }
}

您可以在body中使用两个ZStack,以将顶部文本放置在图像顶部,将底部文本放置在图像底部。 为了显示图像,您调用传递给TextLayer视图的闭包。 要显示文本,请使用MemeTextField,这是在启动程序项目中设置的常规TextField,用于显示格式化的文本。

最后,用以下内容替换预览:

struct TextLayer_Previews: PreviewProvider {
  @State static var meme = Meme(
    imageData: nil,
    topText: "Top Text Test",
    bottomText: "Bottom Text Test"
  )

  static var previews: some View {
    TextLayer(meme: $meme) {
      Text("IMAGE")
        .frame(height: 100)
    }
  }
}

看一下预览:

目前,它看起来不算meme。 不用担心,在下一节中,您将结合图像和文本图层来创建MemeEditor

3. Creating a Meme Editor

您之前创建的所有文件均独立于平台。 但是MemeEditor将根据应用程序运行在iOS / iPadOS还是macOS上,使用特定于平台的不同方法来导入图像。

在后续步骤中,您将创建另一个MemeEditor,以在macOS上显示,但现在,从iOS和iPadOS版本开始。 创建一个新的SwiftUI视图文件MemeEditor_iOS.swift。 这次,它不应该在Shared组中,而应该在iOS中。 请记住仅检查MemeMaker(iOS)target

用以下代码替换文件中的视图:

struct MemeEditor: View {
  @Binding var meme: Meme
  @State var showingImagePicker = false
  @State private var inputImage: NSUIImage?
}

MemeEditor具有与其呈现的meme的绑定以及两个属性。 您将使用ShowingImagePicker来决定何时显示可以让用户选择图像的图像选择器。 然后,您将图像存储在inputImage中。

接下来,向该结构体添加一个新方法来存储输入图像:

func loadImage() {
  guard let inputImage = inputImage else { return }
  meme.imageData = inputImage.data
}

现在,您可以在视图内部添加body

var body: some View {
  // 1
  TextLayer(meme: $meme) {
    // 2
    Button {
      showingImagePicker = true
    } label: {
      if meme.imageData != nil {
        ImageLayer(imageData: $meme.imageData)
      } else {
        Text("Add Image")
          .foregroundColor(.white)
          .padding()
          .background(Color("rw-green"))
          .cornerRadius(30)
          .padding(.vertical, 50)
      }
    }
  }
  // 3
  .sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {
    UIImagePicker(image: $inputImage)
  }
}

这是body发生的事情:

  • 1) 首先,创建一个新的TextLayer并传递对meme的绑定和一个闭包以创建ImageLayer
  • 2) 在此闭包中,定义一个按钮,将其在点击时将showingImagePicker设置为true。 使用上面定义的ImageLayer作为label,或者如果该meme尚不包含图像,则显示一个按钮。
  • 3) 每当showingImagePicker设置为true时,使用sheet显示UIImagePickerUIImagePickerUIImagePickerController的包装,以使其可用于SwiftUI。 它允许用户从其设备中选择图像,并且每当关闭picker时都会调用loadImage

接下来,将文件中的预览替换为以下内容:

struct MemeEditor_Previews: PreviewProvider {
  @State static var meme = Meme(
    imageData: nil,
    topText: "Top Text Test",
    bottomText: "Bottom Text Test"
  )

  static var previews: some View {
    MemeEditor(meme: $meme)
  }
}

现在,您的预览应该显示对视图的测试:

最后,打开ContentView.swift。 用以下代码替换body的内容,该代码是专用的meme编辑器,而不是文本编辑器:

MemeEditor(meme: $document.meme)

在这里,您用新的MemeEditor替换了TextEditor。 您将文档的meme传递给MemeEditor,从而使用户能够操作和处理meme

最后,完成所有这些编码之后,MemeMaker准备在iPhone上运行! 选择MemeMaker(iOS)scheme并构建并运行。 创建一个新文档,如下所示:

现在,您可以选择一个有趣的图像,添加一些文字并提高您的meme制作技巧。 尝试创建一个像这样的有趣的meme

干得好!


Using the App on macOS

SwiftUI的一大优势是您可以在所有Apple平台上使用它。 但是,尽管您使用了NSUIImage,但仍需要进行一些更改才能在macOS上运行MemeMaker

1. Implementing a MemeEditor for macOS

由于MemeEditor使用UIImagePickerController,因此您无法在macOS上使用它。 相反,您将创建另一个版本的MemeEditor,该版本将在macOS上运行该应用程序时使用。 它将使用NSOpenPanel让用户选择图片作为meme的背景。

但是多亏了SwiftUI,大多数视图可以保持不变。 您可以重用ImageLayerTextLayer。 唯一的区别是用户如何选择图像。

在macOS组中创建一个新的SwiftUI View文件,并将其命名为MemeEditor_macOS.swift。 仅检查MemeMaker(macOS)target。 用以下代码替换该文件的内容:

import SwiftUI

struct MemeEditor: View {
  @Binding var meme: Meme

  var body: some View {
    VStack {
      if meme.imageData != nil {
        TextLayer(meme: $meme) {
          ImageLayer(imageData: $meme.imageData)
        }
      }

      Button(action: selectImage) {
        Text("Add Image")
      }
      .padding()
    }
    .frame(minWidth: 500, minHeight: 500)
  }

  func selectImage() {
    NSOpenPanel.openImage { result in
      guard case let .success(image) = result else { return }
      meme.imageData = image.data
    }
  }
}

在这里,您可以创建与之前为iOS创建的视图类似的视图。 不过,这一次,您添加了一个单独的按钮来调用selectImageselectImage使用NSOpenPanel让您的用户选择图像。 如果选择成功,则将新的图像数据存储在模因中。

最后,在文件底部添加一个预览:

struct MemeEditor_Previews: PreviewProvider {
  @State static var meme = Meme(
    imageData: nil, 
    topText: "Top Text", 
    bottomText: "Bottom Text"
  )

  static var previews: some View {
    MemeEditor(meme: $meme)
  }
}

构建并运行。 (您需要使用macOS 11.0或更高版本。)应用如下所示:

您可以在macOS上创建相同的meme

Mac应用程序无需任何额外的工作,就可以使用带有快捷方式的工作菜单。 例如,您可以使用Command-N创建一个新文档,并使用Command-S保存该文档,或者您可以使用Command-Z撤消上一次更改。

创建使用文档并同时在iOS和macOS上运行的应用有多么容易,这是否令人惊讶?

文档是许多优秀应用程序的核心部分。 现在,借助SwiftUI,为iOS,iPadOS和macOS构建基于文档的应用程序变得更加容易。

如果您想更深入地研究基于SwiftUI文档的应用程序,请参阅 Build document-based apps in SwiftUI

有关SwiftUI的更多信息,请查看SwiftUI: Getting StartedSwiftUI by Tutorials一书。

要创建基于文档的UIKit应用,请在Document-Based Apps Tutorial: Getting Started中找到更多信息。

后记

本篇主要讲述了基于SwiftUIDocument-Based App的创建,感兴趣的给个赞或者关注~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335