上一课我们修改了代码以适配在iPhone上使用,本课将把代码整理分类,开发一个多平台可运行的APP。 本课将是2021年度学习的最后一课,通过下面的学习路线图对比是否已完成了关键的学习的内容:
SwiftUI学习路线图
我们将使用Xcode提供的多平台模板来开始创建一个APP,所以我们将原来的工程改名为 EmojiArt iOS ,然后重新打开Xcode,新建一个Multiplatform 多平台的 Document APP工程。
1、新建项目,命名为EmojiArt。
Xcode自动将我们的代码分类好了,我们只需要将原来写好的各部分代码拖入到相应的位置即可。
Xcode自动将我们的代码分类好了,我们只需要将原来写好的各部分代码拖入到相应的位置即可。
2、配置Info.plist
Xcode为iOS与macOS分成生成了2个不同的Info.plist文件,这是因为2个平台的项目设置是有所区别的,很多设置都有特定的平台要求,macOS里还有一个macOS.entitlements文件,用于对安全访问的设置,有点类似于上节课里的隐私权限请求。有IOS里我们只有一个用户登陆使用,但在macOS里可以有多个用户,我们通过配置macOS.entitlements文件达到细分权限。
a.配置iOS的Info.plist(6:36)
i.打开iOS的Info.plist文件
ii.找到Document type项 -> 删除,此项是默认生成与com.example.plain-text文件关联的,可以打开纯文本文件。
iii.找到 Imported Type Identifiers 项 -> 删除,导入类型的标识符,我们不需要。
iv.打开老项目(EmojiArt iOS)里的Info.plist文件,我们需要将原来plist里的内容复制到新项目中来。
v.复制老项目里的Document type项,然后在本Info.plist文件中粘贴。
vi.复制老项目里的 Imported Type Identifiers 项,然后在本Info.plist文件中粘贴
vii.复制老项目里的 Exported Type Identifiers 项,然后在本Info.plist文件中粘贴
viii.复制老项目里的 Privacy - Camera Usage Description 项,然后在本Info.plist文件中粘贴
b.为macOS增加plist信息
i.打开macOS的Info.plist,按上面的iOS前2步操作,删除Document type与 Imported Type Identifiers 项。
ii.重复上面iOS里的第iv、vi、vii步骤即可。
iii.macOS里没有相机,所以我们不需要复制Privacy - Camera Usage Description项。
iv.在Document type的Handler rank下增加一行 Role:Stromg = "Editor" 这是与文档相关的公在Mac上必须参数,告诉系统我们是一个编辑器。如果不放置些参数,mac会报错。
3、整理并替换自动生成的文件
i.删除ContentView.swift文件
ii.替换EmojiArtDocument.swift文件内容,我们使用原来的内容全选替换即可。
iii.替换EmojiArtApp.swift文件内容,同上直接替换所有内容
iv.将除上面2个被替换了的文件以外的其它文件,按下面图示拖动到新的项目中来:
勾选iOS应用程序和macOS应用程序后这些代码都可以适用。我们现在已完成了整个应用程序的复制工作,尝试编译会报一大堆错误(9:15),然后我们将修复这些错误。
当我们选择编辑系统为macOS时,按cmd + b 尝试编译会报很多的错误,这是因为我们试图在Mac上做很多iOS才有的东西,因为它们运行机制与系统都有一些不同,我们需要替换一些类型才能使用。
1、解决UIImage兼容的问题
UIImage只存在于iOS中,在macOS中有一个叫NSImage的对应函数,在后面会发现以iOS里以UIxxx开头的功能,在Mac里只是换了个前缀以NSxxx开始。所以我们大部分时候可以使用typealias解决问题。 在macOS代码区里新建一个macOS的专属文件macOS.swift,写入解决UIImage的代码内容:
import SwiftUI
//共享区域的代码是用UIImage编写的,与NSImage非常相似,所以我们只需要typealias就能适配大部份时候
//但他们也有一点小区别,详细看后面
typealias UIImage = NSImage //将
//在macOS上,没有init(uiImage:)
extension Image {
init(uiImage: UIImage) {// 我们可以提供uiImage初始化
self.init(nsImage: uiImage)//改为init(nsImage:)
}
}
OptionalImage是用于我们背景设置的,调用的构造函数参数为uiImage,但在macOS里没有这个参数,上面说过以ui开始的我们都要换成ns,所以我们对Image进行扩展,代理uiImage的参数为nsImage。
2、解决PHPickerViewController错误
在macOS里我们不能使用UIKit代码去访问摄像头与相册库,所以我们需要将Camera.swift与PhotoLibrary.swift拖放到iOS代码区里,并展开右上角文件的Identity and Type(身份和类型)设置,取消与macOS的关联(12:20):
由于转移了这两个文件到iOS代码区,所以在macOS里会紧接着报这2个错误:Cannot find 'Camera' in scope与Cannot find 'PhotoLibrary' in scope 我们需要在专属文件macOS.swift文件里写入下面的代码即可(21:30):
typealias Camera = CantDoItPhotoPicker //解决Cannot find 'Camera' in scope
typealias PhotoLibrary = CantDoItPhotoPicker//解决Cannot find 'PhotoLibrary' in scope
// Camera和PhotoLibrary在Mac上不存在,创建一个"do nothing"视图,以便代码可以使用#if os(iOS)到处引用它们
//(在这个例子中是可执行的,因为isAvailable返回false)
struct CantDoItPhotoPicker: View {
var handlePickedImage: (UIImage?) -> Void//UIKit所需要的参数(上节课内容)
static let isAvailable = false//Mac上总是不可用(if Camera.isAvailable{ ... })
var body: some View {
EmptyView()//返回空视图
}
}
3、解决 'EditMode' is unavailable in macOS (13:09)
在macOS里EditMode是不可用的,所以我们只能使PaletteManager.swift只为iOS服务,像上面一样拖动到iOS代码区,并设置关联。
4、解决PaletteManager被移动后macOS里找不到对象(14:49)
我们可以使用 if os(iOS)这样的判断来处理,但如果代码过多处理起来可能很麻烦,所以我们这里使用一个通用处理方式,在专属文件macOS.swift里写入下面一行代码搞定:
//在macOS上没有PaletteManager 因为它使用编辑模式(在macOS上不可用)
typealias PaletteManager = EmptyView
在PaletteChooser.swift文件里我们限制不显示表情管理功能:
#if os(iOS)
AnimatedActionButton(title: "管理", systemImage: "slider.vertical.3") {
managing = true
}
#endif
5、解决‘horizontalSizeClass' is unavailable in macOS(12:32)
可以想像一下。Mac是电脑,大的有台式机,小的也是笔记本,我们不可以没有事就把电脑横竖切换吧,所以从硬件上这个参数就是不可能支持的。
#if os(iOS)
@Environment(.horizontalSizeClass) var horizontalSizeClass //从环境变量获取当前水平尺寸
var compact: Bool { horizontalSizeClass == .compact }//监听是否处于宽屏模式
#else
let compact = false //macOS里永远为false
#endif
6、解决 Cannot find 'UIDevice' in scope(17:40)
这是包装我们导航的可以使其关闭的功能,这个在mac上可以不用,因为它会像iPad一样,点击空白处就可以关闭弹出的窗口。所以我们需要将UtilityViews.swift里对View扩展的wrappedInNavigationViewToMakeDismissable与dismissable两个函数移动到我们专门为iOS新建的iOS.swift文件里。 在macOS.swift文件里增加下面的代码:
extension View {
func wrappedInNavigationViewToMakeDismissable(_ dismiss: (() -> Void)?) -> some View {
self// 在macOS里啥也不做,直接返回
}
}
7、解决 Value of type 'UIImage' (aka 'NSImage') has no member 'jpegData'
这也是macOS上的UIImage和NSImage的区别之一,在mac里是没有jpegData函数的,因此如果想要将图像转换为Data数据在macOS上需要使用tiffRepresentation来实现,所以我们将统一在iOS.swift和macOS.swift里新建一个imageData变量:
//方便的变量将一个UIImage转换成一个Data
extension UIImage {
//在macOS上,它将其转换函数是tiffRepresentation而不是iOS里的jpegData
var imageData: Data? { tiffRepresentation }
}
然后在iOS.swift里这样写即可:
extension UIImage {//iOS里依然是jpegData无压缩
var imageData: Data? { jpegData(compressionQuality: 1.0) }
}
这种解决方式是一个抽象概念,我们分另在iOS与macOS文件里定义一个imageData增加一个新的变量,这样就创建了可以在两个平台上以不同方式实现的抽象,最后我们需要将原来的代码替换一下:
if let imageData = image?.imageData {// 原来是image?.jpegData(compressionQuality: 1.0)
8、解决粘贴板问题:Cannot find 'UIPasteboard' in scope
Pasteboard在两个平台上肯定是不相同的,通过前缀UIxxx就可以看出来,macOS上有一个NSPasteboard,但与UIPasteboard并不完全相同,所以我们也要像上面一样做另一个关于粘贴板的抽象。 在iOS上我们新建一个结构体,并返回imageData与imageURL:
// 包含静态访问Pasteboard的结构体,以独立于平台的方式使用
struct Pasteboard {
static var imageData: Data? {
UIPasteboard.general.image?.imageData//imageData在上面定义的
}
static var imageURL: URL? {
UIPasteboard.general.url?.imageURL//imageURL来源于扩展
}
}
而在macOS里我们也是结构体,只是访问粘贴板的方法不一样了:
struct Pasteboard {
static var imageData: Data? {
//尝试获取tiff或png格式的图像数据
NSPasteboard.general.data(forType: .tiff) ?? NSPasteboard.general.data(forType: .png)
}
static var imageURL: URL? {
//(28:35)将NSURL转换URL类型,然后调用扩展里的imageURL筛选是否来自google图片搜索
(NSURL(from: NSPasteboard.general) as URL?)?.imageURL
}
}
最后替换为个抽象出来出来的结构体:
if let imageData = Pasteboard.imageData{ ... }//原来是UIPasteboard.general.image?.jpegData(compressionQuality: 1.0)
else if let url = Pasteboard.imageURL { ... }//原来是UIPasteboard.general.url?.imageURL
这样我们就抽象出了两个平台的粘贴方式。
9、解决No exact matches in call to instance method 'loadObjects'(30:00)
出现这个问题的原因是我们在macOS里拖拽图像的结果的并不是一个URL,在NSImages里没有项目提供者,所以我们目前解决方法使用if os来判断处理:
#if os(iOS)
if !found {
found = providers.loadObjects(ofType: UIImage.self) { image in
if let data = image.jpegData(compressionQuality: 1.0) {
autozoom = true // L14 only "auto zoom" when drag and drop happens
// L14 pass undo manager to Intent functions
document.setBackground(.imageData(data), undoManager: undoManager)
}
}
}
#endif
尝试编译运行,没有任何错误提示,程序也正常启动了,但是界面上还有一点小问题需要修复一下。
1、修复不能放下Emoji的问题。 这是因为macOS里不能使用plainText来获取文,我们应该使用utf8PlainText来获取,而且这是在iOS与masOS里通用的格式。
.onDrop(of: [.utf8PlainText,.url,.image], isTargeted: nil) //将plainText修改为utf8PlainText
2、取消“撤销”、“重做”按钮
因为在macOS里我们通常可以使用快捷键来完成,菜单上也会显示相关功能,我们只需要像上面代码一样使用if os(iOS)判断即可。
3、修复macOS上的按钮样式
我们在paletteControlButton上增加一个新的修改器.paletteControlButtonStyle(),并且只应用于macOS上。 我们在macOS.swift文件里找到扩展View的wrappedInNavigationViewToMakeDismissable方法(上面代码里有)的下面新增下面功能函数(37:22):
// 在macOS上调色板控制按钮需要稍微不同的样式,它还需要更大一点,以便它比滚动视图中的表情符号更大
func paletteControlButtonStyle() -> some View {
self.buttonStyle(PlainButtonStyle()).foregroundColor(.accentColor).padding(.vertical)
}
而在iOS上的代码为:
func paletteControlButtonStyle() -> some View {
self//啥也不做
}
4、修改popover弹出样式
在macOS里popover显示有点难看,我们在上面代码的的后面增加:
// 弹窗在macOS上没有水平填充,所以这将应用于所有的视图在弹出窗口中显示(可能这是不对的,但适用于演示)
func popoverPadding() -> some View {
self.padding(.horizontal)
}
然后iOS和上面一样。建一个空的功能,啥也不做:
func popoverPadding() -> some View {
self//iOS里没有啥可改的
}
在我们弹出的ppover上增加一个修改器:
.popover(item: $paletteToEdit) { palette in
PaletteEditor(palette: $store.palettes[palette])
.popoverPadding()//增加一个修改器
.wrappedInNavigationViewToMakeDismissable { paletteToEdit = nil }
}
5、修复不能获取远程URL的权限问题
在Mac上我们不能随意的读取远程URL地址,需要请求用户授权才可以,所以我们需要在macOS.entitlements里增加一行:com.apple.security.network.client:bool = "Yes"
CS193P Spring 2021课程笔记虽然只有16个课时,但花了1个多月的时间终于完成了。在笔记的过程中,发现以前不理解的问题,通过视频回放,知识点总结,基本上掌握了课程的内容。之所以把课程内容以笔记的方式记录下来,这将有利于我今后快速的复习课程里的知识点。每一行注释都是为了理解代码。在开发时如果忘记了代码可以通过笔记快速查找交复制,不需要在一个一个翻视频,然后再手打处理 。 课程中有很多现成的代码在项目中可以直接使用,只要熟读了代码记在心里,我们要用的时候就知道怎么去处理了。
最后附上 EmojiArt的全部源代码:http://www.55mx.com/data/attachment/app-source/EmojiArt.zip
除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:https://www.55mx.com/post/162
《CS193p2021学习笔记第十六课:Multiplatform 多平台(macOS + iOS)》的网友评论(0)